Java Streams API
The Streams API provides a declarative way to process sequences of data using pipelines of operations such as filtering, mapping, sorting, reducing, and collecting.
What is a Stream?
A stream is a sequence of elements processed through a pipeline. A stream does not store data itself; it reads from a source such as a collection, array, file, or generated sequence.
Stream Pipeline
A typical pipeline has three parts:
- Source — for example a
List. - Intermediate operations — such as
filter,map, andsorted. - Terminal operation — such as
collect,forEach,reduce, orcount.
List<String> result = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.sorted()
.toList();Intermediate Operations
| Operation | Purpose |
|---|---|
filter | Keeps elements that satisfy a predicate. |
map | Transforms each element into another value. |
flatMap | Flattens nested streams into one stream. |
sorted | Produces elements in sorted order. |
distinct | Removes duplicates according to equals. |
limit | Keeps only the first N elements. |
skip | Skips the first N elements. |
peek | Observes elements, usually for debugging. |
Terminal Operations
| Operation | Result |
|---|---|
toList / collect | Builds a result collection. |
forEach | Performs an action for each element. |
count | Returns the number of elements. |
reduce | Combines elements into one result. |
findFirst | Returns the first element as an Optional. |
anyMatch | Checks whether any element matches a predicate. |
allMatch | Checks whether all elements match a predicate. |
noneMatch | Checks whether no elements match a predicate. |
Lazy Evaluation
Most intermediate operations are lazy. They describe work but do not process the source until a terminal operation starts the pipeline.
Stream<String> pipeline = names.stream()
.filter(name -> {
System.out.println("checking " + name);
return name.length() > 3;
});
// Nothing is processed yet.
long count = pipeline.count();Streams Are Single-Use
After a terminal operation, the stream is consumed and cannot be reused.
Stream<String> stream = names.stream();
long count = stream.count();
// IllegalStateException:
// stream.forEach(System.out::println);Mapping and Filtering Example
List<String> names =
List.of("Alice", "Bob", "Charlie", "Anna");
List<String> result = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.toList();
System.out.println(result);
// [ALICE, ANNA]Reducing
reduce combines many elements into one value.
int sum = List.of(1, 2, 3, 4, 5)
.stream()
.reduce(0, Integer::sum);Collectors
Collectors provide reusable reduction strategies for grouping, joining, counting, averaging, and building collections.
Map<Integer, List<String>> byLength =
names.stream()
.collect(Collectors.groupingBy(String::length));String joined = names.stream()
.collect(Collectors.joining(", "));Primitive Streams
Java provides IntStream, LongStream, and DoubleStream to avoid unnecessary boxing when processing primitive numeric values.
int total = IntStream.rangeClosed(1, 100)
.sum();Optional Results
Optional<String> first =
names.stream()
.filter(name -> name.startsWith("Z"))
.findFirst();
first.ifPresent(System.out::println);Side Effects
Stream operations are easiest to understand when lambdas avoid modifying shared external state.
// Better:
List<String> upper =
names.stream()
.map(String::toUpperCase)
.toList();Using external mutable collections inside forEach can make code harder to reason about and becomes especially risky with parallel streams.
Parallel Streams
long count = numbers.parallelStream()
.filter(n -> expensiveCheck(n))
.count();Parallel streams divide work across threads, but they are not automatically faster. Performance depends on workload size, operation cost, data structure, ordering constraints, available processors, and thread-pool contention.
Common Mistakes
- Trying to reuse a stream after a terminal operation.
- Using
peekas the main business operation instead of for observation/debugging. - Adding side effects to operations that should behave like transformations.
- Assuming parallel streams always improve performance.
- Forgetting that operations such as
sortedmay require buffering many elements.
Conclusion
Streams are best understood as lazy, single-use processing pipelines. Learn the difference between sources, intermediate operations, and terminal operations, then add collectors, reductions, primitive streams, and carefully measured parallelism.