Streams API
IntermediateProcess collections declaratively with filter, map, reduce, and dozens of other operations — lazy, composable, and optionally parallel.
Overview
A Stream is a sequence of elements supporting sequential and parallel bulk operations. Streams are lazy: intermediate operations (filter, map, sorted…) are not evaluated until a terminal operation (collect, count, forEach…) is called. This enables the JVM to fuse multiple operations in a single pass. Streams do not store data — they are a pipeline over a source (collection, array, I/O). Once consumed by a terminal operation, a stream cannot be reused.
Creating Streams & Intermediate Operations
Sources: collection.stream(), Arrays.stream(arr), Stream.of(...), Stream.iterate(...), Stream.generate(...), Files.lines(path).
Intermediate operations (lazy, return Stream): filter(Predicate) — keep matching elements map(Function) — transform each element flatMap(Function) — flatten nested streams distinct() — remove duplicates via equals sorted() / sorted(Comparator) — sort limit(n) — cap at n elements skip(n) — skip first n elements peek(Consumer) — inspect without altering (debug)
import java.util.*;
import java.util.stream.*;
public class StreamIntermediate {
public static void main(String[] args) {
List<String> words = List.of("hello","world","java","streams","api","java");
// filter + distinct + sorted + limit
List<String> result = words.stream()
.filter(w -> w.length() > 3) // hello, world, java, streams, java
.distinct() // hello, world, java, streams
.sorted() // hello, java, streams, world
.limit(3) // hello, java, streams
.collect(Collectors.toList());
System.out.println(result);
// map — transform each element
List<Integer> lengths = words.stream()
.map(String::length)
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(lengths); // [3, 4, 5, 7]
// flatMap — flatten List<List<T>> to Stream<T>
List<List<Integer>> nested = List.of(List.of(1,2), List.of(3,4), List.of(5));
List<Integer> flat = nested.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(flat); // [1, 2, 3, 4, 5]
// peek — debug intermediate values (doesn't consume)
long count = words.stream()
.peek(w -> System.out.print("before: " + w + " "))
.filter(w -> w.startsWith("j"))
.peek(w -> System.out.print("after: " + w + " "))
.count();
System.out.println("
Count: " + count); // 2
}
}Terminal Operations & reduce
Terminal operations consume the stream and produce a result: collect(Collector) — accumulate into collection/map/string count() — number of elements forEach(Consumer) — side effect per element findFirst() / findAny() — Optional of first/any match anyMatch / allMatch / noneMatch — short-circuit boolean min / max — Optional of boundary element reduce(identity, BinaryOperator) — fold all elements toArray() — Object[] or typed array
import java.util.*;
import java.util.stream.*;
public class TerminalOps {
public static void main(String[] args) {
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// count, min, max
System.out.println(nums.stream().count()); // 10
System.out.println(nums.stream().max(Integer::compareTo)); // Optional[10]
System.out.println(nums.stream().min(Integer::compareTo)); // Optional[1]
// reduce — fold: sum = 0+1+2+...+10
int sum = nums.stream().reduce(0, Integer::sum);
System.out.println(sum); // 55
// reduce without identity — returns Optional
Optional<Integer> product = nums.stream().reduce((a, b) -> a * b);
System.out.println(product.orElse(0)); // 3628800
// match operations — short-circuit
System.out.println(nums.stream().anyMatch(n -> n > 9)); // true
System.out.println(nums.stream().allMatch(n -> n > 0)); // true
System.out.println(nums.stream().noneMatch(n -> n > 10)); // true
// findFirst — Optional of first element matching filter
Optional<Integer> first = nums.stream()
.filter(n -> n % 3 == 0)
.findFirst();
System.out.println(first.orElse(-1)); // 3
// Numeric streams — avoid boxing
IntStream range = IntStream.rangeClosed(1, 5);
System.out.println(range.sum()); // 15
System.out.println(IntStream.rangeClosed(1, 5).average()); // OptionalDouble[3.0]
// mapToInt for sum without boxing
List<String> words = List.of("hi", "hello", "hey");
int totalLen = words.stream().mapToInt(String::length).sum();
System.out.println(totalLen); // 10
}
}Parallel Streams & Laziness
Call .parallelStream() or .stream().parallel() to enable multi-threaded processing. The common ForkJoinPool splits the source and merges results. Parallel streams shine on CPU-intensive, independent, stateless operations over large data sets.
Avoid parallel streams when: operations have side effects, the source is not efficiently splittable (LinkedList), the pipeline is short, or ordering must be preserved (use forEachOrdered).
Laziness: intermediate operations are not run until a terminal operation is called. Stream.iterate + limit is the canonical example — the limit stops generation early.
import java.util.stream.*;
import java.util.List;
public class ParallelAndLazy {
public static void main(String[] args) {
// Parallel stream — splits work across ForkJoin threads
long count = LongStream.rangeClosed(1, 10_000_000)
.parallel()
.filter(n -> n % 2 == 0)
.count();
System.out.println(count); // 5000000
// Laziness — iterate is infinite; limit stops it
List<Integer> first10Squares = Stream.iterate(1, n -> n + 1)
.map(n -> n * n)
.limit(10)
.collect(Collectors.toList());
System.out.println(first10Squares); // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
// Stream.generate — infinite random stream
List<Double> randoms = Stream.generate(Math::random)
.limit(5)
.collect(Collectors.toList());
System.out.println(randoms.size()); // 5
// Parallel with ordering preserved
List<Integer> nums = List.of(5, 3, 1, 4, 2);
nums.parallelStream()
.sorted()
.forEachOrdered(System.out::print); // 12345 — always in order
System.out.println();
// Pitfall: parallel stream with shared mutable state
// int[] counter = {0};
// IntStream.range(0,1000).parallel().forEach(i -> counter[0]++); // RACE CONDITION
// Use: IntStream.range(0,1000).parallel().count() instead
}
}Interactive Visualization
stream.filter(n → n%2==0).map(n → n*n).sorted().collect(toList())Key Points to Remember
- Streams are lazy — intermediate operations run only when a terminal operation is called
- A stream cannot be reused after a terminal operation; create a new one from the source
- flatMap flattens Stream<Stream<T>> or Stream<List<T>> into a single Stream<T>
- Use mapToInt/mapToLong/mapToDouble + sum/average to avoid boxing overhead on numeric operations
- Parallel streams use ForkJoinPool — avoid when operations have side effects or shared state
- Stream.iterate() + limit() is the clean way to generate finite sequences
Practice Streams API in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat is the difference between intermediate and terminal stream operations?
What is lazy evaluation in Java Streams?
What is the difference between map() and flatMap()?
When should you NOT use parallel streams?
What is the difference between findFirst() and findAny()?
Ask Aria about Streams API
Your personal AI tutor — ask anything about this concept