The Stream API

SQL-like pipelines over data: filter, map, sort, reduce, and collect — sequentially or in parallel

What this chapter is about. The stream API (JDK 8, package java.util.stream) lets you build pipelines of operations that search, filter, map, sort, and aggregate data from a source such as a collection or an array. It is designed around lambda expressions, resembles database queries in concept, and many operations can run in parallel for a big speed-up on large data sets.

1. The Big Idea

A stream is a conduit for data — a sequence of objects flowing from a source. Key facts:

  • A stream never stores data; it moves data, optionally transforming it along the way.
  • A stream operation does not modify the source. Sorting a stream produces a new sorted stream; the source's order is unchanged.
  • This "stream" is unrelated to the I/O streams covered earlier — here it always means a java.util.stream object.
Prerequisites (per the book): a solid grasp of generics (Ch. 14), lambda expressions (Ch. 15), the Collections Framework (Ch. 20), and the basics of parallel execution (Ch. 29).

2. Why This Topic Matters

  • Replaces verbose loops with declarative, composable operations ("what", not "how").
  • Chains of intermediate operations read like a query: source.stream().filter(...).map(...).sorted().collect(...).
  • Switching to parallel execution is often a one-word change (parallelStream() or .parallel()).
  • Streams and collections convert back and forth, so you can operate through a stream and repackage as a collection.

3. Stream Interfaces

InterfaceRole
BaseStream<T, S>Foundation for all streams. Extends AutoCloseable (so a stream can be used in try-with-resources — but only streams over closable sources, e.g. a file, actually need closing). Methods: close(), isParallel(), iterator(), spliterator(), parallel(), sequential(), onClose(), unordered().
Stream<T>The general stream for reference types. Adds filter, map, sorted, forEach, reduce, collect, min, max, count, toArray, toList, …
IntStream, LongStream, DoubleStreamPrimitive-type streams (Stream works only on object references). Similar capabilities plus conveniences like boxed().

3.1 Terminal vs. intermediate operations

Intermediate operationTerminal operation
ReturnsA new stream (so they chain into a pipeline)A result or a side effect (e.g. min(), forEach())
TimingLazy — not performed until a terminal operation runsTriggers the pipeline; consumes the stream
ReuseOnce consumed, the stream cannot be used again

Examples: intermediatefilter, map, sorted, parallel, unordered; terminalforEach, min, max, count, reduce, collect, toArray, iterator, toList.

3.2 Stateless vs. stateful

A stateless operation processes each element independently (filter() with a stateless predicate). A stateful operation may depend on other elements (sorted() — an element's position depends on the rest). This distinction matters for parallel streams, where a stateful operation may need more than one pass.

4. How to Obtain a Stream

// From any collection (Collection got these default methods in JDK 8): default Stream<E> stream() // sequential default Stream<E> parallelStream() // parallel if possible // From an array: static <T> Stream<T> Arrays.stream(T[] array) // sequential Stream<Address> addrStrm = Arrays.stream(addresses); // primitive overloads return IntStream / LongStream / DoubleStream // Other sources: many stream operations return a new stream; // BufferedReader.lines() returns a Stream<String> for a text source.

5. A Simple Stream Example

import java.util.*; import java.util.stream.*; class StreamDemo { public static void main(String[] args) { ArrayList<Integer> myList = new ArrayList<>(); myList.add(7); myList.add(18); myList.add(10); myList.add(24); myList.add(17); myList.add(5); System.out.println("Original list: " + myList); // min() -- terminal, consumes the stream; returns Optional<Integer> Stream<Integer> myStream = myList.stream(); Optional<Integer> minVal = myStream.min(Integer::compare); if(minVal.isPresent()) System.out.println("Minimum value: " + minVal.get()); // Need a NEW stream - the previous one was consumed by min(). myStream = myList.stream(); Optional<Integer> maxVal = myStream.max(Integer::compare); if(maxVal.isPresent()) System.out.println("Maximum value: " + maxVal.get()); // sorted() -- intermediate; forEach() -- terminal Stream<Integer> sortedStream = myList.stream().sorted(); System.out.print("Sorted stream: "); sortedStream.forEach((n) -> System.out.print(n + " ")); System.out.println(); // filter() -- intermediate; keep odd values Stream<Integer> oddVals = myList.stream().sorted().filter((n) -> (n % 2) == 1); System.out.print("Odd values: "); oddVals.forEach((n) -> System.out.print(n + " ")); System.out.println(); // Two filters pipelined oddVals = myList.stream().filter((n) -> (n % 2) == 1) .filter((n) -> n > 5); System.out.print("Odd values greater than 5: "); oddVals.forEach((n) -> System.out.print(n + " ")); } }
Expected output
Original list: [7, 18, 10, 24, 17, 5] Minimum value: 5 Maximum value: 24 Sorted stream: 5 7 10 17 18 24 Odd values: 5 7 17 Odd values greater than 5: 7 17

Reading the code

  • min(Comparator) / max(Comparator) return an Optional<T> — it either holds a value or is empty. Check with isPresent(), read with get() (or orElseThrow() on JDK 10+). Here a method reference Integer::compare supplies the comparator.
  • min(), max(), forEach() are terminal — each consumes its stream, so a fresh myList.stream() is needed for the next operation.
  • sorted() and filter() are intermediate — each returns a new stream, so they chain. forEach(Consumer) runs an action per element (the lambda implements Consumer.accept()).
  • filter(Predicate) keeps elements for which the predicate's test() returns true; filtering a filtered stream again is fine.

6. Reduction Operations

min(), max(), and count() are special-case reductions — they reduce a stream to one value. The general form is reduce() (always terminal):

Optional<T> reduce(BinaryOperator<T> accumulator) T reduce(T identityVal, BinaryOperator<T> accumulator)
  • accumulator combines two values into one. In reduce() its first argument holds the running result, its second is the next element.
  • identityVal is a value that leaves any element unchanged under the operation: 0 for addition, 1 for multiplication. The one-argument form returns an Optional; the two-argument form returns a plain T.
// Product of the elements, two ways: Optional<Integer> productObj = myList.stream().reduce((a, b) -> a * b); if(productObj.isPresent()) System.out.println("Product as Optional: " + productObj.get()); // 2570400 int product = myList.stream().reduce(1, (a, b) -> a * b); System.out.println("Product as int: " + product); // 2570400 // Product of only the EVEN values: int evenProduct = myList.stream().reduce(1, (a, b) -> { if(b % 2 == 0) return a * b; else return a; });
The accumulator must be: stateless (no reliance on external state), non-interfering (does not modify the source), and associative(10 * 2) * 7 must equal 10 * (2 * 7). Associativity is essential for correct parallel reduction.

7. Parallel Streams

Request parallel processing with Collection.parallelStream(), or call parallel() on a sequential stream (defined by BaseStream). Switch back with sequential(). Parallelism happens only if the environment supports it.

Optional<Integer> productObj = myList.parallelStream().reduce((a, b) -> a * b); // same result; multiplications may run in different threads

Every operation on a parallel stream should be stateless, non-interfering, and associative so that the parallel result equals the sequential result.

7.1 The three-argument reduce() (accumulator + combiner)

<U> U reduce(U identityVal, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)

combiner merges two partial results produced by the accumulator. When accumulator and combiner do the same thing you can often use the simpler two-argument form, but sometimes they must differ:

// Product of the SQUARE ROOTS of a list of doubles, in parallel: double productOfSqrRoots = myList.parallelStream().reduce( 1.0, (a, b) -> a * Math.sqrt(b), // accumulator: fold in sqrt(b) (a, b) -> a * b // combiner: multiply two partial products ); // WRONG in parallel: accumulator == combiner would take sqrt of a partial product // double bad = myList.parallelStream().reduce(1.0, (a, b) -> a * Math.sqrt(b));

The buggy version happens to work for a sequential stream (no partial results to combine); it fails for a parallel one.

Ordering: if the source is ordered the stream is ordered. For parallel streams, allowing an unordered stream (unordered()) can boost performance because partitions need not coordinate. forEach() may not preserve order on a parallel stream — use forEachOrdered() if order matters.

8. Mapping

map() applies a function to each element, yielding a new stream (intermediate; the mapping function must be stateless and non-interfering):

<R> Stream<R> map(Function<? super T, ? extends R> mapFunc) // Function.apply(T) -> R
// Product of square roots, computed via map() then a simple 2-arg reduce(): Stream<Double> sqrtRootStrm = myList.stream().map((a) -> Math.sqrt(a)); double productOfSqrRoots = sqrtRootStrm.reduce(1.0, (a, b) -> a * b);
// Keep only selected fields: NamePhoneEmail -> NamePhone (drop the e-mail) Stream<NamePhone> nameAndPhone = myList.stream().map((a) -> new NamePhone(a.name, a.phonenum)); // Pipeline filter + map: only "James", as NamePhone Stream<NamePhone> jamesOnly = myList.stream() .filter((a) -> a.name.equals("James")) .map((a) -> new NamePhone(a.name, a.phonenum));

8.1 Mapping to primitive streams

IntStream mapToInt(ToIntFunction<? super T> mapFunc) LongStream mapToLong(ToLongFunction<? super T> mapFunc) DoubleStream mapToDouble(ToDoubleFunction<? super T> mapFunc)
// ArrayList<Double> -> IntStream of ceilings IntStream cStrm = myList.stream().mapToInt((a) -> (int) Math.ceil(a)); // [1.1, 3.6, 9.2, 4.7, 12.1, 5.0] -> 2 4 10 5 13 5

For "one element → many elements" mappings there are flatMap(), flatMapToInt(), … (and, since JDK 16, mapMulti() variants).

9. Collecting

collect() turns a stream into a container (a mutable reduction). Terminal operation. Simplest form uses a ready-made Collector from Collectors:

<R, A> R collect(Collector<? super T, A, R> collectorFunc) static <T> Collector<T, ?, List<T>> Collectors.toList() static <T> Collector<T, ?, Set<T>> Collectors.toSet()
Stream<NamePhone> nameAndPhone = myList.stream().map((a) -> new NamePhone(a.name, a.phonenum)); List<NamePhone> npList = nameAndPhone.collect(Collectors.toList()); // (need a fresh stream) ... Set<NamePhone> npSet = nameAndPhone2.collect(Collectors.toSet());

9.1 The three-argument collect()

<R> R collect(Supplier<R> target, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)
  • target (Supplier) creates the result container.
  • accumulator adds one element to the container; combiner merges two partial containers. Both must be stateless, non-interfering, and associative.
// Collect into a LinkedList, using lambdas: LinkedList<NamePhone> npList = nameAndPhone.collect( () -> new LinkedList<>(), (list, element) -> list.add(element), (listA, listB) -> listA.addAll(listB)); // Or, more concisely, with constructor / method references: HashSet<NamePhone> npSet = nameAndPhone.collect( HashSet::new, HashSet::add, HashSet::addAll);
Since JDK 16, Stream.toList() returns an unmodifiable List directly — use it when you don't need a mutable result.

10. Iterators and Streams

A stream is not storage, but you can still iterate it. iterator() and spliterator() are terminal operations.

10.1 Iterator

Iterator<String> itr = myList.stream().iterator(); while(itr.hasNext()) System.out.println(itr.next()); // Alpha Beta Gamma Delta Phi Omega

10.2 Spliterator (JDK 8)

Better than Iterator for parallel processing. Three methods matter here:

MethodWhat it does
boolean tryAdvance(Consumer<? super T> action)Runs action on the next element and advances; returns false when none remain. Combines hasNext() + next().
void forEachRemaining(Consumer<? super T> action)Applies action to every unprocessed element — no explicit loop.
Spliterator<T> trySplit()Splits the remaining elements in two, returning a new Spliterator for one partition (null if it can't split).
Spliterator<String> splitItr = myList.stream().spliterator(); while(splitItr.tryAdvance((n) -> System.out.println(n))); // whole stream // or, collectively: splitItr.forEachRemaining((n) -> System.out.println(n)); // split demo: Spliterator<String> splitItr2 = splitItr.trySplit(); if(splitItr2 != null) splitItr2.forEachRemaining((n) -> System.out.println(n)); // Alpha Beta Gamma splitItr.forEachRemaining((n) -> System.out.println(n)); // Delta Phi Omega

Manual splitting rarely helps in simple code; it matters for large parallel data sets — but usually a predefined Stream method on a parallel stream is better than handling Spliterator yourself.

11. Visual Mental Model

SOURCE (collection / array) a stream never stores data; | .stream() / .parallelStream() it never changes the source v [ intermediate ops - LAZY, return a new stream ] filter(Predicate) map(Function) sorted() parallel() unordered() | v [ terminal op - eager, CONSUMES the stream ] forEach / min / max / count / reduce / collect / toArray / toList / iterator | v RESULT (a value, an Optional, an array, or - via collect() - a NEW collection) reduce(): identity (+ -> 0, * -> 1), accumulator (running, next), [combiner for parallel]; accumulator = stateless + non-interfering + associative

12. Important Comparisons

Intermediate operationTerminal operation
ResultA new streamA value / side effect
EvaluationLazyEager; consumes the stream
Examplesfilter, map, sortedreduce, collect, forEach, min, count
reduce()collect()
ProducesA single value / Optional (immutable reduction)A mutable container (mutable reduction)
Typical usesum, product, min/max by rulebuild a List, Set, Map, string
IteratorSpliterator
AdvancehasNext() + next()tryAdvance() (both in one)
Bulkmanual loopforEachRemaining()
ParallelismnotrySplit() partitions the data
Sequential streamParallel stream
Obtainstream() / .sequential()parallelStream() / .parallel()
Requirementsaccumulators stateless etc. (good practice)operations must be stateless, non-interfering, associative
Orderingfollows sourceconsider unordered() / forEachOrdered()

13. Common Beginner Mistakes

Reusing a stream after a terminal operation — call source.stream() again for a new pipeline.
Expecting an intermediate op to do work on its own — nothing runs until a terminal op.
A non-associative or stateful accumulator in reduce() — wrong results in parallel.
Wrong identity value — use 0 for sum, 1 for product.
accumulator == combiner when they must differ (e.g. product of square roots in parallel).
Modifying the source from inside a lambda (interfering operation).
Assuming forEach() keeps order on a parallel stream — use forEachOrdered().
Ignoring Optional from min()/max()/reduce() — check isPresent() first.

14. Best Practices

  • Build a pipeline: chain intermediate operations, end with exactly one terminal operation.
  • Keep lambdas stateless and non-interfering; make reduction/collect functions associative.
  • Prefer map() for transformation, then a simple two-argument reduce() — often avoids needing a combiner.
  • Use primitive streams (mapToInt, …) when working with numbers to avoid boxing.
  • Use collect(Collectors.toList()/toSet()) for the common cases; Stream.toList() when an unmodifiable list is fine.
  • Go parallel only for large data and only when every operation is stateless/associative; consider unordered().
  • Handle Optional results explicitly.
  • Close streams over external resources (files) with try-with-resources; collection streams need no closing.

15. "Remember" Points

Key takeaways
  • A stream is a conduit: no storage, never mutates the source.
  • Intermediate ops return a new stream and are lazy; terminal ops produce a result and consume the stream (use once).
  • Get streams from Collection.stream()/parallelStream() or Arrays.stream().
  • min/max/reduce return Optional; reduce's accumulator must be stateless, non-interfering, associative.
  • Parallel streams need those same guarantees; use the 3-arg reduce/collect (with a combiner) when partial results merge differently.
  • map() transforms elements; collect() builds a collection (mutable reduction); Stream.toList() gives an unmodifiable list.
  • Iterate a stream with Iterator or, for parallelism, Spliterator (tryAdvance, forEachRemaining, trySplit).

16. Interview / Revision Questions

  • Define "stream". How does it differ from a collection and from an I/O stream?
  • Contrast intermediate and terminal operations. What does "lazy" mean here?
  • Why can't you reuse a stream after calling min()?
  • What is Optional, and how do you read a value from it safely?
  • Give the three forms of reduce(). What is the identity value for addition and for multiplication?
  • What three properties must a reduction accumulator have, and why does associativity matter for parallel streams?
  • How do you obtain a parallel stream? How do you switch back to sequential?
  • When must the accumulator and combiner in a 3-argument reduce() differ? Give an example.
  • What does map() do, and what interface does its argument implement? Name the primitive-mapping variants.
  • What is a "mutable reduction"? Show two ways to collect a stream into a list.
  • What does Stream.toList() (JDK 16) return that collect(Collectors.toList()) historically did not?
  • Compare Iterator and Spliterator. What do tryAdvance(), forEachRemaining(), and trySplit() do?

17. Practice Exercises

  • Pipeline: From a List<Integer> print the sorted odd values greater than 10 using stream().sorted().filter(...).filter(...).forEach(...).
  • Reduction: Compute the sum (identity 0) and product (identity 1) of a list two ways — the Optional form and the identity form.
  • Even product: Use a block-lambda accumulator in reduce(1, ...) to multiply only the even elements.
  • Parallel + combiner: Compute the product of the square roots of a List<Double> with parallelStream().reduce(1.0, accumulator, combiner); then show the accumulator-only version gives the wrong answer in parallel but the right one sequentially.
  • Mapping: Map a List<NamePhoneEmail> to a stream of NamePhone; then pipeline filter + map to keep only one person.
  • Primitive stream: Map a List<Double> to an IntStream of ceilings and print them.
  • Collecting: Collect a mapped stream into a List and a Set with Collectors; then into a LinkedList and a HashSet using the 3-argument collect() (lambdas, then method references).
  • Spliterator: Iterate a stream with tryAdvance(), then with forEachRemaining(), then split it with trySplit() and print each partition.

18. Quick Memory Map

THE STREAM API (java.util.stream) | |-- Stream = conduit for data : no storage, never mutates source | |-- Interfaces: BaseStream -> Stream<T> ; IntStream / LongStream / DoubleStream | ops: INTERMEDIATE (new stream, lazy) vs TERMINAL (result, consumes stream) | stateless vs stateful (sorted = stateful) | |-- Obtain: coll.stream() / coll.parallelStream() / Arrays.stream(arr) / reader.lines() | |-- Common ops | filter(Predicate) map(Function) / mapToInt|Long|Double / flatMap | sorted() forEach(Consumer) / forEachOrdered() | min|max(Comparator) -> Optional ; count() | reduce(acc) -> Optional | reduce(identity, acc) identity: + -> 0, * -> 1 | reduce(identity, accumulator, combiner) (parallel / different merge) | acc must be stateless + non-interfering + associative | collect(Collectors.toList()/toSet()) | collect(Supplier, accumulator, combiner) // mutable reduction | toArray() ; toList() (JDK16, unmodifiable) | |-- Parallel: .parallel() / .sequential() ; unordered() ; forEachOrdered() | `-- Iterate: iterator() (hasNext/next) spliterator(): tryAdvance(Consumer) | forEachRemaining(Consumer) | trySplit()

19. Complete Chapter Revision

  1. A stream is a sequence of objects flowing from a source; it stores nothing and never modifies the source.
  2. Interfaces: BaseStreamStream<T> for references, plus IntStream/LongStream/DoubleStream for primitives.
  3. Intermediate operations return a new stream and are lazy; terminal operations produce a result and consume the stream (which then can't be reused). Operations are stateless or stateful (sorted() is stateful).
  4. Obtain streams via Collection.stream()/parallelStream() or Arrays.stream().
  5. The simple example demonstrates min/max (returning Optional, read via isPresent()/get()), sorted(), filter(), and forEach(), including pipelined filters.
  6. Reductions collapse a stream to one value. reduce() has three forms; identity is 0 for sum, 1 for product; the accumulator must be stateless, non-interfering, associative.
  7. Parallel streams (parallelStream() / parallel()) require those guarantees; the 3-argument reduce() adds a combiner for merging partial results, which must sometimes differ from the accumulator. Consider unordered() and forEachOrdered().
  8. map() transforms each element to a new stream; mapToInt/Long/Double() produce primitive streams; flatMap() handles one-to-many.
  9. collect() performs a mutable reduction into a container — via a Collector (Collectors.toList()/toSet()) or the 3-argument (supplier, accumulator, combiner) form; Stream.toList() gives an unmodifiable list.
  10. Streams can be iterated with Iterator or, for parallel work, Spliterator (tryAdvance(), forEachRemaining(), trySplit()).

20. Final Takeaway

The stream API lets you express data processing as a pipeline: chain lazy intermediate operations (filter, map, sorted), finish with one terminal operation (reduce, collect, forEach), and — when the data is large and every operation is stateless and associative — flip to a parallel stream for a multicore speed-up, all without touching the underlying source.