Java Streams API: Lazy Pipelines, map vs flatMap and Collectors
Codevity Admin · 10/25/2026
Codevity Admin · 10/25/2026
The Stream API (Java 8) lets you describe what to do with a collection instead of writing the loop by hand. A stream is not a data structure: it does not store elements, it pulls them from a source (a collection, an array, a generator) through a pipeline of operations.
import java.util.*;
import java.util.stream.*;
public class Pipeline {
public static void main(String[] args) {
List<String> names = List.of("asha", "ben", "carla", "dev", "esha");
List<String> result = names.stream() // source
.filter(n -> n.length() > 3) // intermediate: keep long names
.map(String::toUpperCase) // intermediate: transform
.sorted() // intermediate: sort
.toList(); // terminal: produce the result (Java 16)
System.out.println(result); // [ASHA, CARLA, ESHA]
System.out.println(names.stream().mapToInt(String::length).sum()); // 19
System.out.println(IntStream.rangeClosed(1, 5).sum()); // 15
System.out.println(names.stream().anyMatch(n -> n.startsWith("c"))); // true
System.out.println(Stream.of(1, 2, 3, 4).reduce(0, Integer::sum)); // 10
}
}| Intermediate operations | Terminal operations | |
|---|---|---|
| What they return | Another Stream | A result or void |
| Execution | Lazy: they run nothing yet | Start the whole pipeline |
| Examples | filter, map, flatMap, sorted, distinct, limit, skip, peek | forEach, collect, toList, count, reduce, anyMatch, findFirst, min, max |
Intermediate operations only describe the work. Nothing runs until a terminal operation is called, and elements flow through the pipeline one at a time, which lets short-circuiting operations such as findFirst and limit stop early.
import java.util.stream.*;
public class Laziness {
public static void main(String[] args) {
Stream<String> pipeline = Stream.of("a", "b", "c")
.filter(x -> {
System.out.println("filtering " + x);
return true;
});
System.out.println("pipeline built, nothing has run yet");
long count = pipeline.count(); // the terminal operation starts the work
System.out.println(count); // 3
}
}The output shows pipeline built... first, then the three filtering lines, then 3.
map transforms each element into one element.flatMap transforms each element into a stream and flattens all of those streams into one.import java.util.*;
import java.util.stream.*;
public class Collecting {
public static void main(String[] args) {
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3), List.of(4, 5));
List<Integer> flat = nested.stream().flatMap(List::stream).map(n -> n * 10).toList();
System.out.println(flat); // [10, 20, 30, 40, 50]
List<String> words = List.of("apple", "avocado", "banana", "blueberry", "cherry");
Map<Character, List<String>> byInitial = words.stream().collect(Collectors.groupingBy(w -> w.charAt(0)));
System.out.println(byInitial); // {a=[apple, avocado], b=[banana, blueberry], c=[cherry]}
Map<Integer, Long> countByLength = words.stream()
.collect(Collectors.groupingBy(String::length, TreeMap::new, Collectors.counting()));
System.out.println(countByLength); // {5=1, 6=2, 7=1, 9=1}
String initials = words.stream().map(w -> w.substring(0, 1)).distinct().collect(Collectors.joining(", ", "[", "]"));
System.out.println(initials); // [a, b, c]
}
}When groupingBy uses a custom map factory such as TreeMap::new, assign the result to a variable with its type (as above) before printing it. Passing it straight to println confuses the compiler's type inference.
Common Collectors: toList(), toSet(), toMap(key, value, mergeFunction), groupingBy(classifier, downstream), partitioningBy(predicate), counting(), summingInt, averagingInt, mapping, joining.
import java.util.stream.*;
public class Reuse {
public static void main(String[] args) {
Stream<Integer> numbers = Stream.of(1, 2, 3);
numbers.forEach(n -> { }); // first terminal operation: fine
try {
numbers.forEach(n -> { }); // a second one on the same stream
} catch (IllegalStateException e) {
System.out.println(e.getMessage()); // stream has already been operated upon or closed
}
}
}Create a new stream from the source each time you need one, or keep the results in a collection.
Collectors.toMap(key, value) throws IllegalStateException ("Duplicate key ...") when two elements produce the same key. Supply a merge function to decide what to do. See the list to map snippet and the group by department snippet.
parallelStream() splits the work across the common fork-join pool. It helps only for large, CPU-bound, stateless work on data that splits well. It hurts for small collections (overhead), for I/O (it blocks the shared pool), and with shared mutable state (race conditions). Measure before you use it.
, and avoid boxing and add , and . Convert with , and .
IntStreamLongStreamDoubleStreamsum()average()range()mapToIntmapToObjboxed()peek(System.out::println)