What Are Java Streams?
Introduced in Java 8, the Streams API provides a declarative way to process sequences of elements. Rather than writing verbose loops, you describe what you want to do — filter, transform, collect — and the API handles the how. Streams also make it easy to parallelize operations with a single method call.
Importantly, a Stream is not a data structure. It doesn't store data — it processes data from a source (a collection, array, or I/O channel) through a pipeline of operations.
Creating a Stream
// From a collection
List<String> names = List.of("Alice", "Bob", "Charlie", "Diana");
Stream<String> stream = names.stream();
// From an array
Stream<Integer> nums = Arrays.stream(new Integer[]{1, 2, 3, 4});
// Generated stream
Stream<Double> randoms = Stream.generate(Math::random).limit(5);
The Three Parts of a Stream Pipeline
- Source — Where data comes from (collection, array, etc.)
- Intermediate operations — Transform the stream; they are lazy (don't execute until a terminal operation is called)
- Terminal operation — Triggers execution and produces a result or side effect
Core Intermediate Operations
filter()
Keeps only elements matching a predicate:
List<String> longNames = names.stream()
.filter(name -> name.length() > 4)
.collect(Collectors.toList());
// ["Alice", "Charlie", "Diana"]
map()
Transforms each element:
List<String> upperNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// ["ALICE", "BOB", "CHARLIE", "DIANA"]
sorted()
Sorts elements (natural order or with a comparator):
List<String> sorted = names.stream()
.sorted()
.collect(Collectors.toList());
distinct() and limit()
Stream.of(1, 2, 2, 3, 3, 3)
.distinct() // removes duplicates
.limit(4) // takes at most 4 elements
.forEach(System.out::println);
Core Terminal Operations
collect()
Accumulates stream elements into a collection:
List<String> result = names.stream()
.filter(n -> n.startsWith("A"))
.collect(Collectors.toList());
reduce()
Reduces a stream to a single value:
int sum = IntStream.of(1, 2, 3, 4, 5)
.reduce(0, Integer::sum);
// 15
count(), findFirst(), anyMatch()
long count = names.stream().filter(n -> n.length() > 3).count();
Optional<String> first = names.stream().filter(n -> n.startsWith("C")).findFirst();
boolean hasAlice = names.stream().anyMatch(n -> n.equals("Alice")); // true
Parallel Streams
You can parallelize stream processing with a single change:
names.parallelStream()
.filter(name -> name.length() > 4)
.forEach(System.out::println);
Parallel streams split the workload across available CPU cores. Use them for large datasets and computationally intensive operations. For small collections, the overhead of parallelism can actually be slower than sequential processing.
Streams vs. Loops: When to Use Which
| Situation | Prefer |
|---|---|
| Simple iteration with side effects | Traditional loop |
| Transforming/filtering collections | Streams |
| Chaining multiple operations | Streams |
| Need break/continue logic | Traditional loop |
| Large dataset, CPU-bound processing | Parallel streams |
Key Takeaways
- Streams make collection processing more readable and concise.
- Intermediate operations are lazy — they only run when a terminal operation is called.
- A stream can only be consumed once; create a new one if you need to re-process.
- Use method references (
String::toUpperCase) to make lambdas even more concise.