Index

Basic Stream Operations

Java Streams

4.1 Filtering Elements (filter())

The filter() method in Java Streams is used to select elements that match a given predicateโ€”a condition expressed as a boolean test. It is an intermediate, lazy operation, meaning it doesn't execute until a terminal operation is called.

Filtering is one of the most common stream operations and is useful for removing unwanted values, ignoring nulls, or selecting data that meets specific criteria.

Syntax

Stream<T> filter(Predicate<? super T> predicate)

The method returns a new stream consisting of the elements that match the provided predicate.

Example 1: Filter Strings by Condition

Filter names starting with "A":

import java.util.List;

public class FilterExample1 {
    public static void main(String[] args) {
        List.of("Alice", "Bob", "Andrew", "Charlie")
            .stream()
            .filter(name -> name.startsWith("A"))
            .forEach(System.out::println);
        // Output: Alice, Andrew
    }
}

Example 2: Remove Null or Empty Strings

import java.util.List;

public class FilterExample2 {
    public static void main(String[] args) {
        List.of("Java", "", null, "Streams", " ")
            .stream()
            .filter(s -> s != null && !s.trim().isEmpty())
            .forEach(System.out::println);
        // Output: Java, Streams
    }
}

Example 3: Filter Numbers Based on Condition

Select even numbers from a list:

import java.util.List;

public class FilterExample3 {
    public static void main(String[] args) {
        List.of(1, 2, 3, 4, 5, 6)
            .stream()
            .filter(n -> n % 2 == 0)
            .forEach(System.out::println);
        // Output: 2, 4, 6
    }
}

Laziness of filter()

Because filter() is lazy, no elements are actually tested until a terminal operation like forEach() or collect() is invoked. This allows efficient and optimized processing, especially when combined with short-circuiting methods like limit().

Filtering is foundational to stream processing and helps write clear, expressive, and functional-style code.

Index

4.2 Mapping Elements (map())

The map() operation in Java Streams is used to transform each element in a stream into another form. It performs a one-to-one mapping, meaning that for every input element, exactly one output element is produced.

This method is essential for data transformation in a pipeline, whether you're converting types, extracting object fields, or applying computations.

Syntax

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

It takes a Function that transforms elements from type T to type R.

Example 1: Mapping Strings to Their Lengths

import java.util.List;

public class MapExample1 {
    public static void main(String[] args) {
        List.of("Java", "Stream", "API")
            .stream()
            .map(String::length)
            .forEach(System.out::println);
        // Output: 4, 6, 3
    }
}

Each string is mapped to its integer length.

Example 2: Extracting Object Fields

Suppose you have a list of Person objects and want to get their names:

import java.util.List;

class Person {
    String name;
    int age;
    Person(String name, int age) {
        this.name = name; this.age = age;
    }
}

public class MapExample2 {
    public static void main(String[] args) {
        List<Person> people = List.of(
            new Person("Alice", 30),
            new Person("Bob", 25)
        );

        people.stream()
              .map(p -> p.name)
              .forEach(System.out::println);
        // Output: Alice, Bob
    }
}

Example 3: Type Conversion and Handling Nulls

Mapping string numbers to integers:

import java.util.List;

public class MapExample3 {
    public static void main(String[] args) {
        List.of("1", "2", "three", "4")
            .stream()
            .map(s -> {
                try {
                    return Integer.parseInt(s);
                } catch (NumberFormatException e) {
                    return null;
                }
            })
            .filter(n -> n != null)
            .forEach(System.out::println);
        // Output: 1, 2, 4
    }
}

This example highlights a common pitfall: returning null in a map() function. While possible, it must be handled carefullyโ€”typically with a filter() step to remove nulls.

Key Takeaways

Mapping is central to making stream pipelines powerful and expressive.

Index

4.3 Sorting Elements (sorted())

The sorted() method in Java Streams is used to order elements in a stream. It can sort using natural ordering (like alphabetical or numerical) or a custom Comparator. Sorting is a stateful intermediate operation, which means it needs to examine the entire stream before it can produce resultsโ€”this makes it inherently less lazy than operations like map() or filter().

Syntax

Stream<T> sorted();                          // Natural order (Comparable)
Stream<T> sorted(Comparator<? super T> c);   // Custom Comparator

Stable Sorting

Javaโ€™s sorted() operation is stable, meaning if two elements are considered equal under the sorting criteria, their original order is preserved. This is important when chaining multiple sorts or preserving input consistency.

Example 1: Natural Sorting (Strings)

import java.util.List;

public class SortedExample1 {
    public static void main(String[] args) {
        List.of("Banana", "Apple", "Cherry")
            .stream()
            .sorted()
            .forEach(System.out::println);
        // Output: Apple, Banana, Cherry
    }
}

Here, strings are sorted alphabetically using their natural order (defined by Comparable).

Example 2: Sorting Integers with a Comparator

import java.util.List;
import java.util.Comparator;

public class SortedExample2 {
    public static void main(String[] args) {
        List.of(5, 2, 9, 1)
            .stream()
            .sorted(Comparator.reverseOrder())
            .forEach(System.out::println);
        // Output: 9, 5, 2, 1
    }
}

This example demonstrates sorting numbers in descending order using a custom comparator.

Example 3: Sorting Complex Objects by Multiple Fields

import java.util.List;
import java.util.Comparator;

class Person {
    String name;
    int age;
    Person(String name, int age) {
        this.name = name; this.age = age;
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class SortedExample3 {
    public static void main(String[] args) {
        List<Person> people = List.of(
            new Person("Alice", 30),
            new Person("Bob", 25),
            new Person("Alice", 22)
        );

        people.stream()
              .sorted(Comparator.comparing((Person p) -> p.name)
                                .thenComparing(p -> p.age))
              .forEach(System.out::println);
        // Output:
        // Alice (22)
        // Alice (30)
        // Bob (25)
    }
}

This demonstrates a stable, multi-level sort: first by name, then by age.

Summary

Index

4.4 Distinct Elements (distinct())

The distinct() method in Java Streams is used to remove duplicate elements from a stream. It retains only the first occurrence of each element, as determined by the equals() method. This makes distinct() an effective way to enforce uniqueness in a stream pipeline.

How It Works

Example 1: Distinct Strings

import java.util.List;

public class DistinctExample1 {
    public static void main(String[] args) {
        List.of("apple", "banana", "apple", "cherry", "banana")
            .stream()
            .distinct()
            .forEach(System.out::println);
        // Output: apple, banana, cherry
    }
}

Example 2: Distinct on Custom Objects (Override equals() and hashCode())

import java.util.List;
import java.util.Objects;

class Person {
    String name;
    int age;
    Person(String name, int age) {
        this.name = name; this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person p)) return false;
        return age == p.age && Objects.equals(name, p.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class DistinctExample2 {
    public static void main(String[] args) {
        List<Person> people = List.of(
            new Person("Alice", 30),
            new Person("Bob", 25),
            new Person("Alice", 30)
        );

        people.stream()
              .distinct()
              .forEach(System.out::println);
        // Output: Alice (30), Bob (25)
    }
}

Without properly overriding equals() and hashCode(), the above example would treat all objects as distinctโ€”even if their contents are identical.

Example 3: Distinct Primitive Values

Use boxed() to convert primitive streams to object streams:

import java.util.stream.IntStream;

public class DistinctExample3 {
    public static void main(String[] args) {
        IntStream.of(1, 2, 2, 3, 3, 3)
                 .boxed()
                 .distinct()
                 .forEach(System.out::println);
        // Output: 1, 2, 3
    }
}

Performance Considerations

Using distinct() effectively allows for clean, deduplicated data streams, especially when handling input from user lists, files, or APIs.

Index

4.5 Limiting and Skipping Elements (limit(), skip())

The limit() and skip() methods in Java Streams provide control over how many elements are processed or where processing starts in a stream. These operations are particularly useful in scenarios like pagination, data sampling, or working with infinite streams.

limit(n)

skip(n)

Both are intermediate, lazy operations that are evaluated only when a terminal operation is invoked.

Example 1: Limiting Results After Filtering

import java.util.List;

public class LimitExample {
    public static void main(String[] args) {
        List.of("apple", "apricot", "banana", "avocado", "almond", "blueberry")
            .stream()
            .filter(s -> s.startsWith("a"))
            .limit(3)
            .forEach(System.out::println);
        // Output: apple, apricot, avocado
    }
}

Here, we filter for strings starting with "a" and take only the first 3 matching results.

Example 2: Pagination with skip() and limit()

import java.util.List;

public class PaginationExample {
    public static void main(String[] args) {
        List<String> items = List.of("Item1", "Item2", "Item3", "Item4", "Item5", "Item6");

        int page = 2;
        int pageSize = 2;

        items.stream()
             .skip((page - 1) * pageSize)
             .limit(pageSize)
             .forEach(System.out::println);
        // Output (Page 2): Item3, Item4
    }
}

This simulates pagination, retrieving page 2 of a 2-item-per-page listing.

Example 3: Handling Infinite Streams

limit() is essential when working with infinite streams to avoid non-terminating execution.

import java.util.stream.Stream;

public class InfiniteStreamExample {
    public static void main(String[] args) {
        Stream.iterate(1, n -> n + 1)
              .filter(n -> n % 2 == 0)
              .skip(3)       // Skip first 3 even numbers: 2, 4, 6
              .limit(5)      // Take next 5: 8, 10, 12, 14, 16
              .forEach(System.out::println);
    }
}

Summary

Index