Skip to main content

Streams

Source: Effective Java, 3rd Edition — Chapter 7 by Joshua Bloch (Items 45–48)

The streams API (Java 8) models sequences and pipelines: source → intermediate ops → terminal op. Evaluation is lazy until a terminal runs; a pipeline without one is a silent no-op. This lesson: use streams judiciously, stay side-effect-free, return collections when appropriate, and parallelize with care.

Item 45: Use streams judiciously​

A pipeline transforms data stage by stage. Streams excel at map, filter, reduce, collect, and search — especially over collections. They fit poorly when you need to mutate locals, break/continue/return from enclosing code, or throw checked exceptions from lambdas.

Don’t over-stream: the anagram example becomes unreadable when alphabetizing inline in groupingBy. Prefer a named helper (alphabetize) and a clear pipeline:

words.collect(groupingBy(Main::alphabetize))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(g -> System.out.println(g.size() + ": " + g));

Avoid String.chars() for char processing — elements are int, not char. Combine streams and loops where each shines. Name stream sources as plural nouns (primes()). When an earlier value is lost after map, invert the mapping later (Mersenne exponents via bitLength()).

Quiz

Using streams judiciously

1 / 2

Stream pipeline with no terminal operation

Pick an answer to see if you are right.

Item 46: Prefer side-effect-free functions in streams​

Stream code should use pure stages — results depend only on input, no mutating external state. This is wrong:

Map<String, Long> freq = new HashMap<>();
words.forEach(word -> freq.merge(word.toLowerCase(), 1L, Long::sum)); // smell

Prefer collectors:

freq = words.collect(groupingBy(String::toLowerCase, counting()));

Use forEach only to display results, not to perform the computation. Key collectors: toList, toSet, toMap, groupingBy, joining. Static-import Collectors.* for readability.

Item 47: Prefer Collection to Stream as a return type​

Public APIs that return sequences should support both iteration and stream pipelines when feasible. Collection (via Iterable + stream()) is usually the best return type for in-memory sequences. Stream alone frustrates for-each users (Stream does not extend Iterable). Adapters help:

public static <E> Iterable<E> iterableOf(Stream<E> stream) { return stream::iterator; }
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}

Return Stream or Iterable when the sequence is large, lazy, or impractical to materialize (e.g. all sublists of a list — quadratic memory as Collection). Custom AbstractList / AbstractCollection can represent huge logical sequences (power set) compactly.

Quiz

Return types for sequences

1 / 2

Public API returning only Stream<E> for a finite in-memory list

Pick an answer to see if you are right.

Item 48: Use caution when making streams parallel​

parallel() splits work across the common fork-join pool — not a free speedup. Disasters include Stream.iterate + limit pipelines (Mersenne primes hang with high CPU). Good splittable sources: ArrayList, HashMap, arrays, LongStream.range. collect (mutable reduction) parallelizes poorly.

Parallelism requires associative, non-interfering, stateless lambdas — sequential bugs may become catastrophic under parallel. Use forEachOrdered to preserve encounter order. Parallelize only after correctness tests and realistic benchmarks (Item 67). SplittableRandom beats ThreadLocalRandom for parallel random streams.

Quiz

Parallel streams

1 / 2

primes().map(...).filter(...).limit(20).parallel() on Stream.iterate

Pick an answer to see if you are right.

Items 45–48 map​

ItemMove
45Streams for map/filter/collect; helpers + mix with iteration
46Side-effect-free collect; forEach to print, not to compute
47Return Collection when feasible; Stream/Iterable when lazy/huge
48parallel() only with evidence; mind iterate, collect, state

Chapter 7 complete. Next: Method Design (Chapter 8, Items 49–52).