Java Streams API: A Complete Guide
Master Java Streams for functional-style data processing. Covers stream creation, map, filter, reduce, collectors, parallel streams, and performance considerations.
What you'll learn
- ✓How to create streams from various sources
- ✓How to use map, filter, and reduce effectively
- ✓How to collect results with built-in and custom collectors
- ✓How parallel streams work and when to use them
- ✓Common mistakes and performance pitfalls
Prerequisites
- •Java basics (generics, lambdas)
- •Collections framework familiarity
The Streams API, introduced in Java 8, provides a functional approach to processing collections of data. A stream is a sequence of elements that supports sequential and parallel aggregate operations. It does not store data or modify the underlying collection — it is a pipeline for transforming and filtering data.
Creating streams
import java.util.*;
import java.util.stream.*;
// from a collection
List<String> names = List.of("Alice", "Bob", "Charlie", "Diana");
Stream<String> stream1 = names.stream();
// from values
Stream<String> stream2 = Stream.of("one", "two", "three");
// from an array
int[] numbers = {1, 2, 3, 4, 5};
IntStream stream3 = Arrays.stream(numbers);
// using Stream.generate (infinite)
Stream<Double> randoms = Stream.generate(Math::random).limit(5);
// using Stream.iterate
Stream<Integer> counting = Stream.iterate(0, n -> n + 2).limit(10);
// 0, 2, 4, 6, 8, 10, 12, 14, 16, 18
// from a string
IntStream chars = "hello".chars();
// using Stream.builder
Stream<String> built = Stream.<String>builder()
.add("a").add("b").add("c")
.build();
Intermediate operations
Intermediate operations return a new stream and are lazy — they do not execute until a terminal operation is invoked.
filter
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.toList();
// [2, 4, 6, 8, 10]
map
List<String> names = List.of("alice", "bob", "charlie");
List<String> upper = names.stream()
.map(String::toUpperCase)
.toList();
// [ALICE, BOB, CHARLIE]
// map to different type
List<Integer> lengths = names.stream()
.map(String::length)
.toList();
// [5, 3, 7]
flatMap
List<List<Integer>> nested = List.of(
List.of(1, 2, 3),
List.of(4, 5),
List.of(6, 7, 8, 9)
);
List<Integer> flat = nested.stream()
.flatMap(Collection::stream)
.toList();
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
// practical: split sentences into words
List<String> sentences = List.of("hello world", "foo bar baz");
List<String> words = sentences.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.toList();
// [hello, world, foo, bar, baz]
distinct, sorted, peek
List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6, 5, 3);
List<Integer> result = nums.stream()
.distinct() // remove duplicates
.sorted() // natural order
.peek(n -> System.out.print(n + " ")) // debug: prints each element
.toList();
// prints: 1 2 3 4 5 6 9
// result: [1, 2, 3, 4, 5, 6, 9]
// sort with custom comparator
List<String> names = List.of("Charlie", "Alice", "Bob");
List<String> sorted = names.stream()
.sorted(Comparator.comparingInt(String::length))
.toList();
// [Bob, Alice, Charlie]
Terminal operations
reduce
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// sum
int sum = numbers.stream()
.reduce(0, Integer::sum);
// 15
// product
int product = numbers.stream()
.reduce(1, (a, b) -> a * b);
// 120
// without identity (returns Optional)
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
max.ifPresent(m -> System.out.println("Max: " + m)); // Max: 5
// string concatenation
String joined = List.of("a", "b", "c").stream()
.reduce("", (a, b) -> a + b);
// "abc"
collect with Collectors
import java.util.stream.Collectors;
List<String> names = List.of("Alice", "Bob", "Charlie", "Alice", "Diana", "Bob");
// to set (removes duplicates)
Set<String> uniqueNames = names.stream()
.collect(Collectors.toSet());
// joining
String csv = names.stream()
.collect(Collectors.joining(", "));
// "Alice, Bob, Charlie, Alice, Diana, Bob"
// grouping
Map<Integer, List<String>> byLength = names.stream()
.collect(Collectors.groupingBy(String::length));
// {3=[Bob, Bob], 5=[Alice, Alice, Diana], 7=[Charlie]}
// counting per group
Map<String, Long> frequency = names.stream()
.collect(Collectors.groupingBy(s -> s, Collectors.counting()));
// {Alice=2, Bob=2, Charlie=1, Diana=1}
// partitioning (boolean grouping)
Map<Boolean, List<String>> partitioned = names.stream()
.collect(Collectors.partitioningBy(n -> n.length() > 4));
// {false=[Bob, Bob], true=[Alice, Charlie, Alice, Diana]}
// summarizing
IntSummaryStatistics stats = names.stream()
.collect(Collectors.summarizingInt(String::length));
System.out.println("Count: " + stats.getCount());
System.out.println("Average length: " + stats.getAverage());
System.out.println("Max length: " + stats.getMax());
toMap
record Person(String name, int age) {}
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
);
// simple toMap
Map<String, Integer> nameToAge = people.stream()
.collect(Collectors.toMap(Person::name, Person::age));
// {Alice=30, Bob=25, Charlie=35}
// handling duplicate keys
List<Person> withDupes = List.of(
new Person("Alice", 30),
new Person("Alice", 31)
);
Map<String, Integer> latest = withDupes.stream()
.collect(Collectors.toMap(
Person::name,
Person::age,
(existing, replacement) -> replacement // merge function
));
// {Alice=31}
Other terminal operations
List<Integer> nums = List.of(1, 2, 3, 4, 5);
// forEach
nums.stream().forEach(System.out::println);
// count
long count = nums.stream().filter(n -> n > 3).count(); // 2
// anyMatch, allMatch, noneMatch
boolean hasEven = nums.stream().anyMatch(n -> n % 2 == 0); // true
boolean allPositive = nums.stream().allMatch(n -> n > 0); // true
boolean noneNegative = nums.stream().noneMatch(n -> n < 0); // true
// findFirst, findAny
Optional<Integer> first = nums.stream()
.filter(n -> n > 3)
.findFirst(); // Optional[4]
// min, max
Optional<Integer> min = nums.stream().min(Comparator.naturalOrder()); // Optional[1]
Primitive streams
// IntStream, LongStream, DoubleStream avoid boxing overhead
IntStream.range(1, 6).forEach(System.out::println); // 1, 2, 3, 4, 5
IntStream.rangeClosed(1, 5).forEach(System.out::println); // 1, 2, 3, 4, 5
// aggregate operations
int sum = IntStream.rangeClosed(1, 100).sum(); // 5050
double avg = IntStream.of(1, 2, 3, 4, 5).average().orElse(0); // 3.0
// convert between streams
List<Integer> boxed = IntStream.range(1, 6)
.boxed()
.toList();
IntStream unboxed = List.of(1, 2, 3).stream()
.mapToInt(Integer::intValue);
Parallel streams
List<Integer> largeList = IntStream.rangeClosed(1, 1_000_000)
.boxed()
.toList();
// parallel processing
long count = largeList.parallelStream()
.filter(n -> isPrime(n))
.count();
// convert sequential to parallel
long sum = largeList.stream()
.parallel()
.mapToLong(Integer::longValue)
.sum();
static boolean isPrime(int n) {
if (n < 2) return false;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) return false;
}
return true;
}
When to use parallel streams
Parallel streams are not always faster. They help when:
- The data set is large (thousands of elements minimum).
- Each element requires significant computation.
- The operation is stateless and associative.
- There is no shared mutable state.
They hurt when:
- The data set is small (thread overhead dominates).
- Operations involve I/O (threads block).
- Order matters and you use
forEachOrdered. - The source is hard to split (like a LinkedList).
Real-world example: processing data
record Employee(String name, String department, double salary) {}
List<Employee> employees = List.of(
new Employee("Alice", "Engineering", 95000),
new Employee("Bob", "Engineering", 88000),
new Employee("Charlie", "Marketing", 72000),
new Employee("Diana", "Engineering", 102000),
new Employee("Eve", "Marketing", 68000),
new Employee("Frank", "Sales", 78000)
);
// average salary per department
Map<String, Double> avgSalary = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.averagingDouble(Employee::salary)
));
// {Engineering=95000.0, Marketing=70000.0, Sales=78000.0}
// highest paid per department
Map<String, Optional<Employee>> topPaid = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.maxBy(Comparator.comparingDouble(Employee::salary))
));
// names of employees earning above average
double overallAvg = employees.stream()
.mapToDouble(Employee::salary)
.average()
.orElse(0);
List<String> aboveAverage = employees.stream()
.filter(e -> e.salary() > overallAvg)
.map(Employee::name)
.sorted()
.toList();
// [Alice, Diana]
Common mistakes
Reusing streams. A stream can only be consumed once. Trying to reuse it throws IllegalStateException.
Stream<String> stream = names.stream();
stream.forEach(System.out::println); // works
stream.count(); // throws IllegalStateException
Side effects in intermediate operations. Do not mutate external state in map or filter. Use forEach for side effects at the end of the pipeline.
Using streams for everything. A simple for loop is clearer and faster for basic iteration. Streams shine for multi-step transformations, not for every loop in your code.
Streams provide a clean, composable way to process data. Learn the common operations, understand when parallel helps, and you will find them indispensable for data transformation tasks.
Related articles
- Java Java Streams and Lambdas: Functional Style on the JVM
Transform collections with lambdas and the Stream API. Map, filter, reduce, collectors, parallel streams, and the pitfalls of lazy pipelines.
- Java Advanced Java Stream API Patterns
Go beyond basic filtering and mapping with advanced Stream API patterns including custom collectors, parallel streams, and stateful operations.
- Java Advanced Stream Collectors and Custom Collectors in Java
Go beyond basic Java Stream collectors. Learn groupingBy compositions, teeing, custom Collector implementations, and performance considerations.
- Java Java Optional: Avoiding NullPointerException
Learn how to use Java Optional to write null-safe code. Covers creation, chaining with map and flatMap, best practices, and common anti-patterns to avoid.