Streams in Java
Published on June 18, 2026
A Stream is not a data structure — it’s a pipeline. You describe what you want, and the stream figures out how to compute it lazily.
What is a Stream?
A Stream<T> is a sequence of elements that supports a pipeline of operations. The key properties:
- Not a data structure — it doesn’t store elements. It pulls them from a source (collection, array, generator).
- Lazy — intermediate operations are not executed until a terminal operation is called.
- Single-use — once consumed, a stream cannot be reused.
A pipeline has three parts:
source → intermediate operations (lazy) → terminal operation (triggers execution)
Creating streams
// From a collection
List<String> names = List.of("Alice", "Bob", "Charlie");
Stream<String> s = names.stream();
// From an array
int[] arr = {1, 2, 3};
IntStream s = Arrays.stream(arr);
// From values directly
Stream<String> s = Stream.of("a", "b", "c");
// Infinite stream — generate
Stream<Double> randoms = Stream.generate(Math::random);
// Infinite stream — iterate (like a for loop)
Stream<Integer> naturals = Stream.iterate(0, n -> n + 1); // 0, 1, 2, 3, ...
// Finite iterate (Java 9+)
Stream<Integer> first10 = Stream.iterate(0, n -> n < 10, n -> n + 1);
Intermediate operations
These are lazy — they return a new stream and don’t execute until a terminal operation is called.
filter — keep elements matching a predicate
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
map — transform each element
List<String> upperNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// Map to a different type
List<Integer> lengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
flatMap — flatten a stream of streams
Use when each element maps to multiple elements. map would give you a Stream<List<T>>; flatMap flattens it to Stream<T>.
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4), List.of(5));
// map gives Stream<List<Integer>> — not what we want
// flatMap gives Stream<Integer>
List<Integer> flat = nested.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
// [1, 2, 3, 4, 5]
Another common use — splitting strings into words:
List<String> sentences = List.of("hello world", "foo bar");
List<String> words = sentences.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.collect(Collectors.toList());
// ["hello", "world", "foo", "bar"]
sorted — sort elements
// Natural order
list.stream().sorted()
// Custom comparator
list.stream().sorted(Comparator.comparingInt(Student::getAge))
// Descending
list.stream().sorted(Comparator.comparingInt(Student::getAge).reversed())
distinct — remove duplicates
List<Integer> unique = List.of(1, 2, 2, 3, 3, 3).stream()
.distinct()
.collect(Collectors.toList());
// [1, 2, 3]
limit and skip
// First 5 elements
stream.limit(5)
// Skip first 5, take the rest
stream.skip(5)
// Pagination: page 2, size 10
stream.skip(10).limit(10)
peek — inspect elements without consuming the stream
Useful for debugging. Does not alter the stream.
list.stream()
.filter(n -> n > 0)
.peek(n -> System.out.println("after filter: " + n))
.map(n -> n * 2)
.collect(Collectors.toList());
Terminal operations
These trigger the pipeline to execute and produce a result. After calling one, the stream is consumed.
collect — gather results into a collection
The most common terminal operation. Takes a Collector — covered in the next section.
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
forEach — consume each element
names.stream().forEach(System.out::println);
Note: forEach does not guarantee order on parallel streams. Use forEachOrdered if order matters.
count
long count = names.stream().filter(n -> n.startsWith("A")).count();
findFirst and findAny
Both return an Optional<T>. findFirst returns the first element in encounter order; findAny may return any element (faster on parallel streams).
Optional<String> first = names.stream()
.filter(n -> n.length() > 3)
.findFirst();
first.ifPresent(System.out::println);
String val = first.orElse("none");
anyMatch, allMatch, noneMatch
Short-circuit — stop as soon as the answer is known.
boolean hasAdult = students.stream().anyMatch(s -> s.getAge() >= 18);
boolean allAdults = students.stream().allMatch(s -> s.getAge() >= 18);
boolean noneMinor = students.stream().noneMatch(s -> s.getAge() < 18);
min and max
Return Optional<T>.
Optional<Student> youngest = students.stream()
.min(Comparator.comparingInt(Student::getAge));
Optional<Integer> maxVal = numbers.stream().max(Integer::compare);
reduce — fold elements into a single value
// Sum
int sum = numbers.stream().reduce(0, Integer::sum);
// Product
int product = numbers.stream().reduce(1, (a, b) -> a * b);
// Without identity — returns Optional
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Collectors
Collectors is a utility class with factory methods for common collection strategies.
toList, toSet, toUnmodifiableList
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
List<String> immutable = stream.collect(Collectors.toUnmodifiableList());
toMap
// Map from name to age
Map<String, Integer> nameToAge = students.stream()
.collect(Collectors.toMap(Student::getName, Student::getAge));
// Handle duplicate keys with a merge function
Map<String, Integer> nameToAge = students.stream()
.collect(Collectors.toMap(
Student::getName,
Student::getAge,
(existing, newVal) -> existing // keep existing on collision
));
groupingBy — group elements by a classifier
// Group students by department
Map<String, List<Student>> byDept = students.stream()
.collect(Collectors.groupingBy(Student::getDepartment));
// Group and count
Map<String, Long> countByDept = students.stream()
.collect(Collectors.groupingBy(
Student::getDepartment,
Collectors.counting()
));
// Group and collect only names
Map<String, List<String>> namesByDept = students.stream()
.collect(Collectors.groupingBy(
Student::getDepartment,
Collectors.mapping(Student::getName, Collectors.toList())
));
partitioningBy — split into two groups (true/false)
Map<Boolean, List<Student>> partition = students.stream()
.collect(Collectors.partitioningBy(s -> s.getAge() >= 18));
List<Student> adults = partition.get(true);
List<Student> minors = partition.get(false);
joining — concatenate strings
String result = names.stream().collect(Collectors.joining());
// "AliceBobCharlie"
String result = names.stream().collect(Collectors.joining(", "));
// "Alice, Bob, Charlie"
String result = names.stream().collect(Collectors.joining(", ", "[", "]"));
// "[Alice, Bob, Charlie]"
counting, summingInt, averagingInt
long count = stream.collect(Collectors.counting());
int total = stream.collect(Collectors.summingInt(Student::getAge));
double avg = stream.collect(Collectors.averagingInt(Student::getAge));
Primitive streams
For int, long, and double, use IntStream, LongStream, DoubleStream to avoid boxing overhead. They have extra methods like sum(), average(), range().
// Sum of 1 to 100
int sum = IntStream.rangeClosed(1, 100).sum();
// Average age
OptionalDouble avg = students.stream()
.mapToInt(Student::getAge)
.average();
// Box back to Stream<Integer> if needed
Stream<Integer> boxed = IntStream.range(0, 10).boxed();
Common patterns
Filter then transform:
List<String> result = students.stream()
.filter(s -> s.getAge() > 20)
.map(Student::getName)
.sorted()
.collect(Collectors.toList());
Frequency map (word count):
Map<String, Long> freq = words.stream()
.collect(Collectors.groupingBy(w -> w, Collectors.counting()));
Flatten and deduplicate:
Set<String> allTags = posts.stream()
.flatMap(p -> p.getTags().stream())
.collect(Collectors.toSet());
Top N elements:
List<Student> top3 = students.stream()
.sorted(Comparator.comparingInt(Student::getScore).reversed())
.limit(3)
.collect(Collectors.toList());
Check if a value exists and get it:
students.stream()
.filter(s -> s.getName().equals("Alice"))
.findFirst()
.ifPresent(s -> System.out.println(s.getAge()));
Parallel streams
Call .parallelStream() instead of .stream() to split work across threads. Useful for CPU-bound operations on large collections.
long count = bigList.parallelStream()
.filter(expensiveOperation)
.count();
Caveats:
- Not always faster — parallelism has overhead. Benchmark before using.
- Order is not guaranteed unless you use
forEachOrderedorsorted. - Avoid stateful lambdas (e.g., modifying a shared variable) — they cause race conditions.
Quick reference
| Operation | Type | What it does |
|---|---|---|
filter(pred) |
Intermediate | Keep matching elements |
map(fn) |
Intermediate | Transform each element |
flatMap(fn) |
Intermediate | Transform and flatten |
sorted(cmp) |
Intermediate | Sort elements |
distinct() |
Intermediate | Remove duplicates |
limit(n) |
Intermediate | Take first n |
skip(n) |
Intermediate | Skip first n |
peek(fn) |
Intermediate | Inspect without consuming |
collect(c) |
Terminal | Gather into collection |
forEach(fn) |
Terminal | Consume each element |
count() |
Terminal | Count elements |
findFirst() |
Terminal | First element as Optional |
anyMatch(pred) |
Terminal | Short-circuit OR |
allMatch(pred) |
Terminal | Short-circuit AND |
reduce(id, fn) |
Terminal | Fold into single value |
min(cmp) / max(cmp) |
Terminal | Minimum / maximum as Optional |
Tags: java, streams, functional-programming