Stream API
The Stream API, introduced in Java 8, provides a functional, declarative model for processing sequences of elements. A Stream<T> is not a data structure — it carries no storage. It is a pipeline specification: a source that provides elements, zero or more intermediate operations that transform or filter the stream, and exactly one terminal operation that consumes the stream and produces a result or side effect. Streams are lazy: intermediate operations do not execute until the terminal operation is invoked, and execution is fused — elements flow through the entire pipeline one at a time (or in batches for parallel streams), avoiding intermediate collection. Streams are single-use: once a terminal operation has been invoked, the stream is consumed and cannot be reused. Java provides both reference streams (Stream<T>) and primitive streams (IntStream, LongStream, DoubleStream) that avoid boxing overhead. The Stream API covers sources (collections, arrays, files, generators), intermediate operations (filter, map, flatMap, sorted, distinct, limit, skip, peek, mapToInt, mapToObj), terminal operations (forEach, collect, reduce, count, findFirst, findAny, anyMatch, allMatch, noneMatch, toList, min, max), and collectors (toList, toSet, toMap, groupingBy, partitioningBy, joining, counting, summarizing). This entry covers the full lifecycle, every operation class, the short-circuit evaluation, the Spliterator model, collector design, and performance guidance.
Stream Sources, Pipeline Structure, and Lazy Evaluation
// ── Stream sources ────────────────────────────────────────────────────
// From Collection:
List<String> names = List.of("Alice", "Bob", "Carol", "Dave");
Stream<String> fromList = names.stream();
// From array:
String[] arr = {"x", "y", "z"};
Stream<String> fromArray = Arrays.stream(arr);
// From explicit elements:
Stream<Integer> ofElements = Stream.of(1, 2, 3, 4, 5);
// Primitive range (no boxing):
IntStream range = IntStream.range(0, 10); // 0,1,...,9
IntStream rangeClosed = IntStream.rangeClosed(1, 5); // 1,2,3,4,5
// File lines:
try (Stream<String> lines = Files.lines(Path.of("data.txt"), StandardCharsets.UTF_8)) {
lines.forEach(System.out::println);
}
// Infinite generator:
Stream<Double> randoms = Stream.generate(Math::random); // infinite — must limit()
// Infinite iterate:
Stream<Integer> powers = Stream.iterate(1, n -> n * 2); // 1, 2, 4, 8, ...
// Iterate with predicate (Java 9+):
Stream<Integer> bounded = Stream.iterate(1, n -> n <= 100, n -> n * 2); // stops at 64
// ── Pipeline: lazy evaluation demonstration ───────────────────────────
Stream<String> pipeline = names.stream()
.filter(s -> {
System.out.println("Filtering: " + s); // will print
return s.length() > 3;
})
.map(s -> {
System.out.println("Mapping: " + s); // will print
return s.toUpperCase();
});
// Nothing printed yet — pipeline is lazy, no computation has occurred
System.out.println("Terminal operation starting:");
List<String> result = pipeline.collect(Collectors.toList());
// NOW the filtering and mapping runs:
// Filtering: Alice Mapping: Alice
// Filtering: Bob (Bob filtered out — length = 3, not > 3)
// Filtering: Carol Mapping: Carol
// Filtering: Dave Mapping: Dave
System.out.println(result); // [ALICE, CAROL, DAVE]
// ── Short-circuit: stops early ────────────────────────────────────────
Optional<String> firstLong = names.stream()
.filter(s -> s.length() > 3) // filter runs for: Alice (pass), Bob (stop here)
.findFirst(); // stop after finding first match
// Only "Alice" is processed beyond the filter — "Carol" and "Dave" never seen
System.out.println(firstLong); // Optional[Alice]Intermediate Operations — Transforming and Filtering
// ── filter: keep elements matching predicate ──────────────────────────
List<String> filtered = names.stream()
.filter(s -> s.startsWith("A") || s.startsWith("C"))
.collect(Collectors.toList());
System.out.println(filtered); // [Alice, Carol]
// ── map: transform each element ───────────────────────────────────────
List<Integer> lengths = names.stream()
.map(String::length) // String → Integer (boxed)
.collect(Collectors.toList());
// mapToInt: avoid boxing ───────────────────────────────────────────────
int totalLength = names.stream()
.mapToInt(String::length) // String → int (no boxing)
.sum(); // IntStream.sum() — no boxing
System.out.println(totalLength);
// ── flatMap: one-to-many, flatten nested structure ────────────────────
List<List<String>> nested = List.of(
List.of("a", "b", "c"),
List.of("d", "e"),
List.of("f")
);
List<String> flat = nested.stream()
.flatMap(Collection::stream) // each inner List → its stream, then flatten
.collect(Collectors.toList());
System.out.println(flat); // [a, b, c, d, e, f]
// flatMap for splitting strings into words:
List<String> sentences = List.of("hello world", "foo bar baz");
List<String> words = sentences.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.collect(Collectors.toList());
System.out.println(words); // [hello, world, foo, bar, baz]
// ── sorted: stateful, buffers all elements ────────────────────────────
List<String> sortedNames = names.stream()
.sorted() // natural order (Comparable)
.collect(Collectors.toList());
System.out.println(sortedNames); // [Alice, Bob, Carol, Dave]
List<String> byLength = names.stream()
.sorted(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()))
.collect(Collectors.toList());
// ── distinct: remove duplicates ────────────────────────────────────────
List<Integer> withDups = List.of(1, 2, 2, 3, 3, 3, 4);
List<Integer> deduped = withDups.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(deduped); // [1, 2, 3, 4]
// ── limit and skip: pagination ────────────────────────────────────────
int page = 1, pageSize = 2;
List<String> page1 = names.stream()
.skip((long) page * pageSize) // skip page 1 = skip 2 elements
.limit(pageSize) // take next 2
.collect(Collectors.toList());
System.out.println(page1); // [Carol, Dave]
// ── peek: debugging (side-effect, don't use for logic) ────────────────
long count = names.stream()
.peek(s -> System.out.println("Before filter: " + s))
.filter(s -> s.length() > 3)
.peek(s -> System.out.println("After filter: " + s))
.count();Terminal Operations and Collectors
// ── Terminal operations ───────────────────────────────────────────────
long count = names.stream().filter(s -> s.length() > 3).count();
System.out.println(count); // 3
Optional<String> shortest = names.stream().min(Comparator.comparingInt(String::length));
Optional<String> longest = names.stream().max(Comparator.comparingInt(String::length));
System.out.println(shortest.orElse("")); // Bob
System.out.println(longest.orElse("")); // Carol (or Alice — tied at 5)
boolean anyA = names.stream().anyMatch(s -> s.startsWith("A")); // true
boolean allA = names.stream().allMatch(s -> s.startsWith("A")); // false
boolean noneZ = names.stream().noneMatch(s -> s.startsWith("Z")); // true
// reduce: sum lengths
int totalLen = names.stream()
.mapToInt(String::length)
.reduce(0, Integer::sum); // IntStream.reduce(0, IntBinaryOperator)
System.out.println(totalLen);
// reduce with Object accumulator:
String longest2 = names.stream()
.reduce("", (a, b) -> a.length() >= b.length() ? a : b);
System.out.println(longest2); // Carol
// ── Collectors: the comprehensive toolkit ─────────────────────────────
// toList (Java 16+): unmodifiable list preserving encounter order:
List<String> immList = names.stream().collect(Collectors.toUnmodifiableList());
// or since Java 16:
List<String> toListResult = names.stream().toList(); // unmodifiable
// toSet:
Set<String> nameSet = names.stream().collect(Collectors.toSet());
// toMap: String → length
Map<String, Integer> nameLengths = names.stream()
.collect(Collectors.toMap(
Function.identity(), // key: the string itself
String::length // value: its length
));
System.out.println(nameLengths); // {Alice=5, Bob=3, Carol=5, Dave=4}
// toMap with merge function (handles duplicate keys):
Map<Integer, String> byLength = names.stream()
.collect(Collectors.toMap(
String::length, // key: length
Function.identity(), // value: name
(a, b) -> a + "," + b // merge duplicate keys: "Alice,Carol" for length 5
));
// groupingBy: group into Map<K, List<T>>
Map<Integer, List<String>> byLengthGroup = names.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println(byLengthGroup);
// {3=[Bob], 4=[Dave], 5=[Alice, Carol]}
// groupingBy with downstream collector:
Map<Integer, Long> countByLength = names.stream()
.collect(Collectors.groupingBy(String::length, Collectors.counting()));
System.out.println(countByLength); // {3=1, 4=1, 5=2}
Map<Integer, String> joinedByLength = names.stream()
.collect(Collectors.groupingBy(
String::length,
Collectors.joining(", ") // join names of same length
));
System.out.println(joinedByLength); // {3=Bob, 4=Dave, 5=Alice, Carol}
// partitioningBy: split into true/false
Map<Boolean, List<String>> partition = names.stream()
.collect(Collectors.partitioningBy(s -> s.length() > 3));
System.out.println(partition.get(true)); // [Alice, Carol, Dave]
System.out.println(partition.get(false)); // [Bob]
// joining: concatenate strings
String joined = names.stream().collect(Collectors.joining(", ", "[", "]"));
System.out.println(joined); // [Alice, Bob, Carol, Dave]
// summarizingInt: all stats at once
IntSummaryStatistics stats = names.stream()
.collect(Collectors.summarizingInt(String::length));
System.out.printf("count=%d sum=%d min=%d max=%d avg=%.1f%n",
stats.getCount(), stats.getSum(), stats.getMin(),
stats.getMax(), stats.getAverage());
// count=4 sum=17 min=3 max=5 avg=4.3