Stream Operations

The Stream API's real power comes from combining a small set of intermediate operations (filter, map, flatMap, sorted, ...) with a terminal operation (collect, reduce, forEach, ...) to build precise data pipelines.

Stream operations split into two families: intermediate operations, which transform a stream into another stream and are evaluated lazily, and terminal operations, which consume the stream and produce a concrete result or side effect, triggering the pipeline to actually run.

filter — Keep Matching Elements

filter(Predicate<T>) keeps only elements for which the predicate returns true. It is stateless — each element is evaluated independently.

List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evens = nums.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
System.out.println(evens); // [2, 4, 6]

map and flatMap — Transform Elements

map(Function<T, R>) transforms each element one-to-one into another value, producing a Stream<R>. flatMap(Function<T, Stream<R>>) transforms each element into its own stream and then flattens all of those streams into a single stream — essential when each input element produces zero, one, or many output elements (e.g. a list of lists, or a sentence into words).

map would leave a Stream<List<Integer>> (still nested); flatMap merges each inner list's stream into one flat stream.

List<String> names = List.of("ann", "bob");
List<Integer> lengths = names.stream().map(String::length).toList();
System.out.println(lengths); // [3, 3]

List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4, 5));
List<Integer> flat = nested.stream()
    .flatMap(List::stream)
    .toList();
System.out.println(flat); // [1, 2, 3, 4, 5]

distinct and sorted

distinct() removes duplicate elements, using equals() to compare — it is stateful, since it must remember what it has already seen. sorted() (natural ordering, elements must implement Comparable) or sorted(Comparator) orders the stream; it is stateful and must buffer the entire stream before emitting any element, since the first output element can depend on the last input.

List<Integer> result = Stream.of(3, 1, 2, 3, 1)
    .distinct()
    .sorted()
    .toList();
System.out.println(result); // [1, 2, 3]

peek — Observing Without Transforming

peek(Consumer<T>) performs an action on each element as it flows through, without changing the stream itself — useful for debugging a pipeline. It is intended for non-interfering, side-effect-free debugging actions; relying on peek for actual application logic is discouraged, and its execution is not guaranteed if a downstream short-circuiting operation stops the pipeline early.

long count = Stream.of(1, 2, 3)
    .peek(n -> System.out.println("saw " + n))
    .filter(n -> n > 1)
    .count();

limit and skip

limit(n) truncates the stream to at most n elements (short-circuiting — it can stop pulling from an infinite source). skip(n) discards the first n elements and emits the rest. Together they support simple pagination.

List<Integer> page2 = Stream.iterate(1, n -> n + 1)
    .skip(5)
    .limit(5)
    .toList();
System.out.println(page2); // [6, 7, 8, 9, 10]

takeWhile and dropWhile (Java 9+)

takeWhile(Predicate) takes elements from the start of the stream while the predicate holds, and stops (short-circuits) at the first element that fails it — even if later elements would have passed. dropWhile(Predicate) is the complement: it drops elements from the start while the predicate holds, then keeps everything from the first failure onward.

Unlike filter, takeWhile/dropWhile care about the *position* in the sequence — takeWhile stops permanently at the first failing element, it doesn't skip over it and keep checking later ones.

List<Integer> nums = List.of(1, 2, 3, 4, 1, 2);
System.out.println(nums.stream().takeWhile(n -> n < 4).toList()); // [1, 2, 3]
System.out.println(nums.stream().dropWhile(n -> n < 4).toList()); // [4, 1, 2]

forEach — Terminal Side Effect

forEach(Consumer<T>) performs an action for each element and returns nothing (void). It's a terminal operation — it consumes the stream. On a parallel stream, forEach does not guarantee encounter order; use forEachOrdered if order matters.

List.of("a", "b", "c").stream().forEach(System.out::println);

collect — Building a Result

collect(Collector) is the general-purpose terminal operation for accumulating stream elements into a result container — a List, Set, Map, String, or a custom structure — using a Collector (see the Collectors topic). It's the most common way to end a pipeline when you need a materialized result.

List<String> upper = Stream.of("a", "b").map(String::toUpperCase).collect(Collectors.toList());

reduce — Combining Elements

reduce combines stream elements into a single value using an associative accumulator function. Three overloads exist: reduce(BinaryOperator) returns Optional<T> (empty stream -> empty Optional); reduce(identity, BinaryOperator) returns T directly, using identity as both the starting value and the result for an empty stream; reduce(identity, accumulator, combiner) supports parallel reduction where types may differ between accumulation and combination steps.

int sum = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
System.out.println(sum); // 10

Optional<Integer> product = Stream.of(1, 2, 3, 4).reduce((a, b) -> a * b);
System.out.println(product.get()); // 24

count, min, max

count() returns the number of elements as a long. min(Comparator) and max(Comparator) return Optional<T> (empty for an empty stream), since there is no minimum/maximum of nothing.

List<Integer> nums = List.of(5, 3, 9, 1);
long n = nums.stream().count(); // 4
Optional<Integer> max = nums.stream().max(Integer::compareTo); // Optional[9]

findFirst and findAny

findFirst() returns an Optional<T> wrapping the first element encountered (or empty), respecting encounter order. findAny() returns any matching element — for a sequential stream this is typically the first, but for a parallel stream it may return whichever element becomes available first, trading determinism for potential speed. Both are short-circuiting.

Optional<Integer> first = Stream.of(3, 1, 4).filter(n -> n > 1).findFirst();
System.out.println(first.get()); // 3

anyMatch, allMatch, noneMatch

These are short-circuiting predicates over the whole stream: anyMatch returns true as soon as one element satisfies the predicate; allMatch returns false as soon as one element fails it (and true for an empty stream — vacuous truth); noneMatch returns true if no element matches (also vacuously true for an empty stream).

List<Integer> nums = List.of(2, 4, 6);
System.out.println(nums.stream().allMatch(n -> n % 2 == 0));  // true
System.out.println(nums.stream().anyMatch(n -> n > 5));       // true
System.out.println(nums.stream().noneMatch(n -> n < 0));      // true

Short-Circuiting Operations

Some operations don't need to process the whole stream to produce a result — they stop early once the answer is known. This matters for performance, and it's what makes infinite streams (Stream.iterate without a bound) usable at all.

Short-circuiting : anyMatch, allMatch, noneMatch, findFirst, findAny, limit, takeWhile
Not short-circuiting (full traversal required): filter, map, sorted, distinct, count, reduce, collect, forEach

Key Takeaways

filter/map/flatMap transform; flatMap specifically flattens nested streams into one.

distinct and sorted are stateful and may need to buffer the whole stream.

takeWhile/dropWhile are position-aware and stop at the first failing/passing element, unlike filter.

reduce, min, max, findFirst/findAny return Optional (except the identity-based reduce overload) because the stream might be empty.

anyMatch/allMatch/noneMatch and findFirst/findAny/limit are short-circuiting and can avoid processing the entire stream.

List<List<Integer>> lists = ...; Stream<List<Integer>> s = lists.stream().map(List::stream); // wrong type, still nested

Stream<Integer> s = lists.stream().flatMap(List::stream); // flattened map() here would produce a Stream<Stream<Integer>>-shaped mismatch; flatMap merges each inner stream into a single flat stream of the element type.

int sum = Stream.<Integer>empty().reduce(Integer::sum).get(); // NoSuchElementException int sum = Stream.<Integer>empty().reduce(0, Integer::sum); // 0

The no-identity reduce overload returns Optional.empty() for an empty stream, and calling get() on it throws. Use the identity overload when you have a sensible default.

nums.stream().filter(n -> n < 3).forEach(...); // scans and discards everything after the first mismatch too nums.stream().takeWhile(n -> n < 3).forEach(...); // stops at the first n >= 3 filter checks every element regardless of position; takeWhile stops permanently once the predicate first fails, which is usually the intent for a sorted or ordered prefix.

What is the difference between map and flatMap?

map transforms each element into exactly one output element (one-to-one). flatMap transforms each element into its own stream and flattens all those streams into a single result stream, so it handles one-to-many (or one-to-zero) transformations, like turning a list of lists into a single flat list.

Why do min, max, findFirst, findAny, and the no-identity reduce all return Optional?

Because the stream might be empty, in which case there is no minimum, maximum, first element, or reduced result to return — Optional makes that absence explicit instead of returning null or throwing.

How does takeWhile differ from filter?

filter evaluates every element independently and keeps all that match, regardless of position. takeWhile stops at the very first element that fails the predicate and discards everything after it, even if later elements would pass — it is position-sensitive and short-circuiting.

Why is sorted() considered a stateful intermediate operation?

Because producing the correctly sorted output for even the first element may require seeing every other element first (to know it's truly the smallest/first in order) — so sorted() must buffer the whole stream before it can emit anything.

What's the difference between findFirst and findAny, and when might they behave differently?

findFirst always returns the first element in encounter order. findAny returns any matching element and, on parallel streams, may return a different one each run since it's optimized to return as soon as any thread finds a match, not necessarily the first one.

Why does allMatch return true on an empty stream?

It's a vacuous truth: allMatch means 'no element fails the predicate', and with zero elements there is no element to fail it, so the condition holds trivially.

Which stream operations are short-circuiting, and why does that matter for infinite streams?

anyMatch, allMatch, noneMatch, findFirst, findAny, limit, and takeWhile are short-circuiting — they can stop pulling elements before reaching the end. That's what makes it safe to call limit(n) or findFirst() on an infinite stream like Stream.iterate(...); without one, the pipeline would never terminate.

Write a pipeline that takes List<String> words, flattens each word into its individual characters as a List<Character>, and removes duplicates.

List<String> words = List.of("cat", "act", "tab"); List<Character> chars = words.stream() .flatMap(w -> w.chars().mapToObj(c -> (char) c)) .distinct() .toList(); System.out.println(chars); // [c, a, t, b]

[c, a, t, b] — flatMap turns each word into a stream of its characters, all are merged into one stream, and distinct() removes repeats, keeping first-seen order.

What does this print?

[1, 3, 5] — takeWhile takes odd numbers from the start and stops permanently at the first even number (2), even though 5 is odd; it never looks past the first failure.

Write code using reduce to find the longest string in a List<String> (empty list should produce Optional.empty()).

List<String> words = List.of("a", "bbb", "cc"); Optional<String> longest = words.stream() .reduce((a, b) -> a.length() >= b.length() ? a : b); System.out.println(longest); // Optional[bbb]

Optional[bbb] — reduce compares pairs, keeping the longer string each time; for an empty stream it would correctly return Optional.empty().

What does this print, and why?

false — anyMatch on an empty stream has no element that can satisfy the predicate, so it correctly returns false (the mirror case of allMatch returning true vacuously on an empty stream).

List<Integer> nums = List.of(1, 3, 5, 2, 4, 6);
List<Integer> result = nums.stream().takeWhile(n -> n % 2 != 0).toList();
System.out.println(result);
boolean result = Stream.<Integer>of().anyMatch(n -> n > 0);
System.out.println(result);