Stream API
A stream processes a sequence of elements through a pipeline:
- Source:
list.stream(),Stream.of(...),IntStream.range(...) - Intermediate operations, which are lazy:
filter,map,flatMap,sorted,distinct,limit - A terminal operation, which triggers the work:
collect,toList(),forEach,reduce,count,anyMatch
Nothing runs until the terminal operation, and a stream can be consumed only once.
Additions over the years: takeWhile and dropWhile (Java 9), Stream.toList() and mapMulti (Java 16), and gatherers for custom intermediate operations (final in Java 24).
Creating streams
Streams come from collections, arrays, values, ranges, generators and files.
orders.stream();
Stream.of("a", "b", "c");
Arrays.stream(new int[]{3, 1, 2});
IntStream.rangeClosed(1, 5); // 1..5
Stream.iterate(1, n -> n * 2).limit(10); // 1, 2, 4, ...
Files.lines(Path.of("app.log")); // close it (try-with-resources)Intermediate operations
These return a new stream and run lazily: filter, map, flatMap (flatten nested collections), distinct, sorted, limit, skip, peek (for debugging), and takeWhile/dropWhile (Java 9).
List<String> tags = courses.stream()
.flatMap(c -> c.tags().stream()) // List<List<String>> -> Stream<String>
.map(String::toLowerCase)
.distinct()
.sorted()
.toList();Terminal operations
A terminal operation runs the pipeline and produces a result: collect, toList (Java 16), forEach, count, min/max, reduce, anyMatch/allMatch/noneMatch, and findFirst/findAny (which return an Optional).
long paid = orders.stream().filter(Order::isPaid).count();
boolean anyBig = orders.stream().anyMatch(o -> o.amount() > 10_000);
Optional<Order> latest = orders.stream().max(Comparator.comparing(Order::placedAt));Primitive streams
IntStream, LongStream and DoubleStream avoid boxing and add sum, average and summaryStatistics. Convert with mapToInt and back with boxed.
int total = orders.stream().mapToInt(Order::quantity).sum();
OptionalDouble avg = orders.stream().mapToDouble(Order::amount).average();
IntSummaryStatistics stats = IntStream.of(4, 8, 15).summaryStatistics();Laziness, short-circuiting and single use
Nothing happens until a terminal operation runs, and operations like limit, findFirst and anyMatch stop early. A stream can be consumed only once; reusing it throws IllegalStateException.
Stream<String> s = names.stream().filter(n -> {
System.out.println("checking " + n); // not printed yet
return n.startsWith("A");
});
s.findFirst(); // now it runs, and stops at the first match
// s.count(); // IllegalStateException: already usedreduce
reduce folds all elements into one value using an identity value and an associative function. For sums and joins, prefer the specialised sum() and Collectors.joining.
int product = IntStream.rangeClosed(1, 5).reduce(1, (a, b) -> a * b); // 120
BigDecimal total = items.stream().map(Item::price).reduce(BigDecimal.ZERO, BigDecimal::add);Parallel streams
parallelStream() splits work across the common ForkJoinPool. It helps only for large, CPU-heavy, independent work, and hurts for small collections, I/O or shared mutable state. Measure before using it, and never modify shared variables inside.
long primes = LongStream.rangeClosed(2, 5_000_000)
.parallel()
.filter(Maths::isPrime)
.count();Stream gatherers (Java 24)
Gatherers add custom intermediate operations. The built-in ones include windowFixed (batches) and windowSliding (moving windows).
List<List<Integer>> batches = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowFixed(2))
.toList(); // [[1, 2], [3, 4], [5]]Example
record Order(String customer, String city, double amount) {}
List<Order> orders = List.of(
new Order("Asha", "Pune", 1200), new Order("Ravi", "Delhi", 300),
new Order("Asha", "Pune", 800), new Order("Kabir", "Delhi", 2500));
List<String> bigSpenders = orders.stream()
.filter(o -> o.amount() > 1000)
.map(Order::customer)
.distinct()
.toList(); // Java 16+: [Asha, Kabir]
Map<String, Double> revenueByCity = orders.stream()
.collect(Collectors.groupingBy(Order::city,
Collectors.summingDouble(Order::amount))); // {Pune=2000.0, Delhi=2800.0}
double avg = orders.stream().mapToDouble(Order::amount).average().orElse(0);Common mistake
Calling a terminal operation twice on the same stream throws IllegalStateException ("stream has already been operated upon or closed"). Create a new stream instead.
Under the hood
Streams process one element at a time through the whole pipeline, and short-circuiting operations (findFirst, limit, anyMatch) stop early. parallelStream() uses the shared common ForkJoinPool; it helps only for large, CPU-bound work on easily split sources such as ArrayList or arrays, and it hurts for I/O or small lists. Stream.toList() returns an unmodifiable list, whereas Collectors.toList() currently returns a mutable one.
Check yourself
A stream has filter and map but no terminal operation. What happens?
How this connects
Where this leads
Part of Java from zero, Job-ready backend developer, Crack the Java interview.
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.