Java Streams API
Master the Java Streams API — filter, map, collect, flatMap, reduce, and Collectors for expressive data processing pipelines.
What Is a Stream?
A Stream is a sequence of elements supporting functional-style aggregate operations. Streams do not store data — they pull from a source (collection, array, generator) and process it through a pipeline.
Source → intermediate ops (lazy) → terminal op (triggers execution)
List.of(1,2,3,4,5)
.stream() // source
.filter(n -> n % 2 == 0) // intermediate (lazy)
.map(n -> n * n) // intermediate (lazy)
.collect(toList()) // terminal → [4, 16]
Creating Streams
import java.util.stream.*;
import java.util.*;
// From a collection
List<String> names = List.of("Alice", "Bob", "Charlie");
Stream<String> s1 = names.stream();
// From an array
int[] numbers = {1, 2, 3, 4, 5};
IntStream s2 = Arrays.stream(numbers);
// From values directly
Stream<String> s3 = Stream.of("x", "y", "z");
// Empty stream
Stream<String> empty = Stream.empty();
// Infinite streams
Stream<Integer> naturals = Stream.iterate(1, n -> n + 1); // 1, 2, 3, ...
Stream<Integer> powers = Stream.iterate(1, n -> n * 2); // 1, 2, 4, 8, ...
Stream<Double> randoms = Stream.generate(Math::random); // random doubles
// Primitive streams (avoid boxing overhead)
IntStream ints = IntStream.range(1, 6); // 1, 2, 3, 4, 5
IntStream ints2 = IntStream.rangeClosed(1, 5); // same
LongStream longs = LongStream.of(10L, 20L, 30L);
DoubleStream doubles = DoubleStream.of(1.1, 2.2, 3.3);
Intermediate Operations
These return a new stream. They are lazy — nothing executes until a terminal op is called.
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)
.collect(Collectors.toList());
// [2, 4, 6, 8, 10]
// Multiple filters compose naturally
List<Integer> result = numbers.stream()
.filter(n -> n > 3)
.filter(n -> n % 2 != 0)
.collect(Collectors.toList());
// [5, 7, 9]
map
List<String> words = List.of("hello", "world", "java");
// Transform each element
List<String> upper = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// [HELLO, WORLD, JAVA]
// Map to a different type
List<Integer> lengths = words.stream()
.map(String::length)
.collect(Collectors.toList());
// [5, 5, 4]
// Map to extracted field
record Person(String name, int age) {}
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
);
List<String> peopleNames = people.stream()
.map(Person::name)
.collect(Collectors.toList());
// [Alice, Bob, Charlie]
flatMap
// Each element maps to a stream — all are flattened into one stream
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)
.collect(Collectors.toList());
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
// Splitting sentences into words
List<String> sentences = List.of("Hello World", "Java Streams", "are great");
List<String> allWords = sentences.stream()
.flatMap(sentence -> Arrays.stream(sentence.split(" ")))
.collect(Collectors.toList());
// [Hello, World, Java, Streams, are, great]
sorted, distinct, limit, skip, peek
List<Integer> nums = List.of(5, 3, 1, 4, 1, 5, 9, 2, 6, 5);
List<Integer> processed = nums.stream()
.distinct() // remove duplicates: [5,3,1,4,9,2,6]
.sorted() // natural order: [1,2,3,4,5,6,9]
.skip(2) // skip first 2: [3,4,5,6,9]
.limit(3) // take at most 3: [3,4,5]
.collect(Collectors.toList());
// Custom sort
List<Person> people = ...;
List<Person> sorted = people.stream()
.sorted(Comparator.comparing(Person::age).reversed())
.collect(Collectors.toList());
// peek — inspect elements without consuming the stream (debugging)
List<Integer> result2 = nums.stream()
.filter(n -> n > 3)
.peek(n -> System.out.println("after filter: " + n))
.map(n -> n * 2)
.peek(n -> System.out.println("after map: " + n))
.collect(Collectors.toList());
Terminal Operations
These trigger the pipeline and produce a result.
collect
import java.util.stream.*;
import java.util.*;
List<String> words = List.of("apple", "banana", "cherry", "apricot", "blueberry");
// toList() — Java 16+
List<String> list = words.stream().collect(Collectors.toList());
// or simply: words.stream().toList() (Java 16+, unmodifiable)
// toSet()
Set<String> set = words.stream().collect(Collectors.toSet());
// toMap()
Map<String, Integer> wordLengths = words.stream()
.collect(Collectors.toMap(
w -> w, // key
String::length // value
));
// groupingBy — group into a Map<K, List<V>>
Map<Character, List<String>> byFirstLetter = words.stream()
.collect(Collectors.groupingBy(w -> w.charAt(0)));
// {a=[apple, apricot], b=[banana, blueberry], c=[cherry]}
// counting per group
Map<Character, Long> countByLetter = words.stream()
.collect(Collectors.groupingBy(
w -> w.charAt(0),
Collectors.counting()
));
// {a=2, b=2, c=1}
// joining
String csv = words.stream()
.collect(Collectors.joining(", ", "[", "]"));
// [apple, banana, cherry, apricot, blueberry]
// partitioningBy — splits into true/false groups
Map<Boolean, List<String>> partition = words.stream()
.collect(Collectors.partitioningBy(w -> w.length() > 5));
// {false=[apple], true=[banana, cherry, apricot, blueberry]}
reduce
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Sum with reduce (identity + accumulator)
int sum = numbers.stream()
.reduce(0, Integer::sum); // 15
// Product
int product = numbers.stream()
.reduce(1, (a, b) -> a * b); // 120
// Max without identity — returns Optional
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
max.ifPresent(m -> System.out.println("Max: " + m)); // Max: 5
// Summing strings
List<String> words2 = List.of("Java", " ", "Streams");
String concat = words2.stream()
.reduce("", String::concat); // "Java Streams"
forEach, count, findFirst, anyMatch, allMatch, noneMatch
List<String> names = List.of("Alice", "Bob", "Charlie", "Diana");
// forEach — terminal, returns void
names.stream().forEach(System.out::println);
// count
long count = names.stream().filter(n -> n.length() > 4).count(); // 2
// findFirst — returns Optional<T>
Optional<String> first = names.stream()
.filter(n -> n.startsWith("C"))
.findFirst();
first.ifPresent(System.out::println); // Charlie
// findAny — may be faster in parallel streams
Optional<String> any = names.parallelStream()
.filter(n -> n.length() == 3)
.findAny();
// matching — all return boolean
boolean anyLong = names.stream().anyMatch(n -> n.length() > 6); // false
boolean allShort = names.stream().allMatch(n -> n.length() < 10); // true
boolean noneEmpty = names.stream().noneMatch(String::isEmpty); // true
Real-World Example — Processing Orders
import java.util.*;
import java.util.stream.*;
record Order(String id, String customer, double amount, String status) {}
public class OrderProcessor {
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("O1", "Alice", 150.00, "PAID"),
new Order("O2", "Bob", 50.00, "PENDING"),
new Order("O3", "Alice", 200.00, "PAID"),
new Order("O4", "Charlie", 300.00, "PAID"),
new Order("O5", "Bob", 75.00, "CANCELLED")
);
// Total revenue from paid orders
double revenue = orders.stream()
.filter(o -> "PAID".equals(o.status()))
.mapToDouble(Order::amount)
.sum();
System.out.println("Revenue: " + revenue); // 650.0
// Revenue per customer (paid orders only)
Map<String, Double> revenueByCustomer = orders.stream()
.filter(o -> "PAID".equals(o.status()))
.collect(Collectors.groupingBy(
Order::customer,
Collectors.summingDouble(Order::amount)
));
System.out.println(revenueByCustomer);
// {Alice=350.0, Charlie=300.0}
// Top 2 paid orders by amount
List<Order> top2 = orders.stream()
.filter(o -> "PAID".equals(o.status()))
.sorted(Comparator.comparingDouble(Order::amount).reversed())
.limit(2)
.collect(Collectors.toList());
top2.forEach(o -> System.out.println(o.id() + ": " + o.amount()));
// O4: 300.0
// O3: 200.0
// All customer names who have at least one paid order
Set<String> paidCustomers = orders.stream()
.filter(o -> "PAID".equals(o.status()))
.map(Order::customer)
.collect(Collectors.toSet());
System.out.println(paidCustomers); // [Alice, Charlie]
}
}
Primitive Streams — IntStream, LongStream, DoubleStream
Use primitive streams to avoid boxing overhead:
// IntStream has built-in sum, average, min, max, stats
IntStream range = IntStream.rangeClosed(1, 100);
System.out.println(range.sum()); // 5050
System.out.println(IntStream.rangeClosed(1, 100).average()); // OptionalDouble[50.5]
// Statistics summary
IntSummaryStatistics stats = IntStream.of(3, 1, 4, 1, 5, 9, 2, 6)
.summaryStatistics();
System.out.println(stats.getMin()); // 1
System.out.println(stats.getMax()); // 9
System.out.println(stats.getSum()); // 31
System.out.println(stats.getAverage()); // 3.875
// Boxing / unboxing between streams
IntStream intStream = IntStream.range(1, 6);
Stream<Integer> boxed = intStream.boxed(); // int → Integer
IntStream unboxed = boxed.mapToInt(Integer::intValue); // Integer → int Frequently Asked Questions
Are streams lazy? What does that mean?
Yes. Intermediate operations (filter, map, sorted, etc.) are lazy — they build a pipeline but do nothing until a terminal operation (collect, forEach, count, etc.) is called. This means a stream of a million elements with filter().map().findFirst() may only process a handful of elements before stopping.
Can I reuse a stream?
No. A stream can only be consumed once. After a terminal operation is called the stream is closed. If you need to process the same data twice, collect to a List and stream it again, or use a Supplier<Stream<T>> factory.
When should I use parallel streams?
Parallel streams split work across the ForkJoinPool and can speed up CPU-bound operations on large datasets. Avoid them for: small collections (overhead outweighs benefit), I/O-bound work, operations with side effects, or when order matters. Always benchmark — parallel is not always faster.
What is the difference between map and flatMap?
map applies a function to each element and wraps each result in the stream — one element in, one element out. flatMap applies a function that returns a stream per element, then flattens all those streams into one. Use flatMap when each element maps to zero-or-more results (e.g., a list of sentences mapped to individual words).