6.2 Stream API
The Stream API, introduced in Java 8 and enhanced in subsequent versions, provides a functional approach to processing sequences of elements. Streams enable declarative, composable operations on data while abstracting away iteration details.
What is a Stream?
A stream is not a data structure—it's a pipeline of operations on a data source. Key characteristics:
- No storage: Streams don't store elements
- Functional: Operations produce results without modifying the source
- Lazy: Intermediate operations are evaluated only when needed
- Possibly unbounded: Can represent infinite sequences
- Consumable: Can be used only once
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
// Create a stream and process it
List<String> shortNames = names.stream()
.filter(name -> name.length() <= 5)
.map(String::toUpperCase)
.toList();
System.out.println(shortNames); // [ALICE, BOB, DAVID]
Creating Streams
From Collections:
List<String> list = List.of("a", "b", "c");
Stream<String> stream = list.stream();
Set<Integer> set = Set.of(1, 2, 3);
Stream<Integer> setStream = set.stream();
From Arrays:
String[] array = {"x", "y", "z"};
Stream<String> arrayStream = Arrays.stream(array);
Stream<String> partialStream = Arrays.stream(array, 1, 3); // [y, z]
From Values:
Stream<String> streamOf = Stream.of("one", "two", "three");
Stream<String> empty = Stream.empty();
From Generators:
// Infinite stream using iterate
Stream<Integer> evens = Stream.iterate(0, n -> n + 2);
// Must limit: evens.limit(5).toList() -> [0, 2, 4, 6, 8]
// Infinite stream using generate
Stream<Double> randoms = Stream.generate(Math::random);
// randoms.limit(3).toList()
// Bounded iterate (Java 9+)
Stream<Integer> bounded = Stream.iterate(0, n -> n < 10, n -> n + 1);
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
From Files:
try (Stream<String> lines = Files.lines(Path.of("data.txt"))) {
lines.filter(line -> !line.isBlank())
.forEach(System.out::println);
}
From Ranges:
IntStream.range(0, 5).forEach(System.out::println); // 0, 1, 2, 3, 4
IntStream.rangeClosed(1, 5).forEach(System.out::println); // 1, 2, 3, 4, 5
Intermediate Operations
Intermediate operations return a new stream and are lazy—they're not executed until a terminal operation is invoked.
Filter: Remove elements that don't match a predicate
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.toList(); // [2, 4, 6, 8]
Map: Transform each element
List<String> names = List.of("alice", "bob", "charlie");
List<String> upper = names.stream()
.map(String::toUpperCase)
.toList(); // [ALICE, BOB, CHARLIE]
List<Integer> lengths = names.stream()
.map(String::length)
.toList(); // [5, 3, 7]
FlatMap: Transform and flatten
List<List<String>> nested = List.of(
List.of("a", "b"),
List.of("c", "d"),
List.of("e")
);
List<String> flattened = nested.stream()
.flatMap(List::stream)
.toList(); // [a, b, c, d, e]
// Practical example: get all tags from articles
record Article(String title, List<String> tags) {}
List<Article> articles = List.of(
new Article("Java Streams", List.of("java", "streams")),
new Article("Spring Boot", List.of("java", "spring"))
);
List<String> allTags = articles.stream()
.flatMap(article -> article.tags().stream())
.distinct()
.toList(); // [java, streams, spring]
MapMulti: Conditional flattening (Java 16+)
// More efficient than flatMap for conditional expansion
List<Integer> expanded = List.of(1, 2, 3, 4).stream()
.<Integer>mapMulti((n, consumer) -> {
consumer.accept(n);
if (n % 2 == 0) {
consumer.accept(n * 10);
}
})
.toList(); // [1, 2, 20, 3, 4, 40]
Distinct: Remove duplicates
List<Integer> numbers = List.of(1, 2, 2, 3, 3, 3, 4);
List<Integer> unique = numbers.stream()
.distinct()
.toList(); // [1, 2, 3, 4]
Sorted: Order elements
List<String> names = List.of("Charlie", "Alice", "Bob");
// Natural order
List<String> sorted = names.stream()
.sorted()
.toList(); // [Alice, Bob, Charlie]
// Custom comparator
List<String> byLength = names.stream()
.sorted(Comparator.comparingInt(String::length))
.toList(); // [Bob, Alice, Charlie]
// Reverse order
List<String> reversed = names.stream()
.sorted(Comparator.reverseOrder())
.toList(); // [Charlie, Bob, Alice]
Peek: Perform action without changing elements (mainly for debugging)
List<String> result = names.stream()
.peek(name -> System.out.println("Original: " + name))
.map(String::toUpperCase)
.peek(name -> System.out.println("Uppercase: " + name))
.toList();
Limit and Skip: Control stream size
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> first3 = numbers.stream()
.limit(3)
.toList(); // [1, 2, 3]
List<Integer> skip3 = numbers.stream()
.skip(3)
.toList(); // [4, 5, 6, 7, 8]
// Pagination
int page = 2;
int pageSize = 3;
List<Integer> pageData = numbers.stream()
.skip((long) (page - 1) * pageSize)
.limit(pageSize)
.toList(); // [4, 5, 6]
TakeWhile and DropWhile: Conditional limit/skip (Java 9+)
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 1, 2);
// Take elements while condition is true
List<Integer> taken = numbers.stream()
.takeWhile(n -> n < 4)
.toList(); // [1, 2, 3]
// Drop elements while condition is true
List<Integer> dropped = numbers.stream()
.dropWhile(n -> n < 4)
.toList(); // [4, 5, 1, 2]
Terminal Operations
Terminal operations trigger stream processing and produce a result.
forEach: Process each element
List<String> names = List.of("Alice", "Bob");
names.stream().forEach(System.out::println);
// ForEachOrdered: maintains order for parallel streams
names.parallelStream().forEachOrdered(System.out::println);
toList: Collect to immutable list (Java 16+)
List<String> result = stream.toList();
// Equivalent to: stream.collect(Collectors.toUnmodifiableList())
toArray: Collect to array
String[] array = stream.toArray(String[]::new);
Object[] objects = stream.toArray();
reduce: Combine elements
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
// Max
Optional<Integer> max = numbers.stream()
.reduce(Integer::max); // Optional[5]
// String concatenation
String concatenated = List.of("a", "b", "c").stream()
.reduce("", (a, b) -> a + b); // "abc"
collect: Accumulate into collections or custom results
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
String joined = stream.collect(Collectors.joining(", "));
count: Count elements
long count = stream.filter(n -> n > 5).count();
anyMatch, allMatch, noneMatch: Test predicates
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0); // true
boolean allPositive = numbers.stream().allMatch(n -> n > 0); // true
boolean noneNegative = numbers.stream().noneMatch(n -> n < 0); // true
findFirst, findAny: Get elements
Optional<String> first = names.stream().findFirst();
Optional<String> any = names.stream().findAny();
min, max: Find extremes
List<Integer> numbers = List.of(3, 1, 4, 1, 5, 9);
Optional<Integer> min = numbers.stream().min(Integer::compareTo); // 1
Optional<Integer> max = numbers.stream().max(Integer::compareTo); // 9
// With custom comparator
record Person(String name, int age) {}
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25)
);
Optional<Person> youngest = people.stream()
.min(Comparator.comparingInt(Person::age)); // Bob
Real-World Example: Order Processing
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
record Order(
String id,
String customerId,
LocalDate date,
List<OrderItem> items,
String status
) {
BigDecimal total() {
return items.stream()
.map(OrderItem::lineTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
record OrderItem(String productId, int quantity, BigDecimal price) {
BigDecimal lineTotal() {
return price.multiply(BigDecimal.valueOf(quantity));
}
}
class OrderAnalytics {
private final List<Order> orders;
public OrderAnalytics(List<Order> orders) {
this.orders = orders;
}
// Find high-value orders (>= $1000)
public List<Order> getHighValueOrders() {
return orders.stream()
.filter(order -> order.total()
.compareTo(BigDecimal.valueOf(1000)) >= 0)
.toList();
}
// Get orders by date range
public List<Order> getOrdersByDateRange(LocalDate start, LocalDate end) {
return orders.stream()
.filter(order -> !order.date().isBefore(start))
.filter(order -> !order.date().isAfter(end))
.sorted(Comparator.comparing(Order::date))
.toList();
}
// Calculate total revenue
public BigDecimal getTotalRevenue() {
return orders.stream()
.filter(order -> order.status().equals("completed"))
.map(Order::total)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// Get top customers by order count
public List<Map.Entry<String, Long>> getTopCustomers(int limit) {
return orders.stream()
.collect(Collectors.groupingBy(
Order::customerId,
Collectors.counting()
))
.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(limit)
.toList();
}
// Get most popular products
public List<Map.Entry<String, Integer>> getMostPopularProducts(int limit) {
return orders.stream()
.flatMap(order -> order.items().stream())
.collect(Collectors.groupingBy(
OrderItem::productId,
Collectors.summingInt(OrderItem::quantity)
))
.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(limit)
.toList();
}
// Calculate average order value
public BigDecimal getAverageOrderValue() {
return orders.stream()
.map(Order::total)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(
BigDecimal.valueOf(orders.size()),
2,
BigDecimal.ROUND_HALF_UP
);
}
}
// Usage example
void demonstrateOrderAnalytics() {
List<Order> orders = List.of(
new Order(
"ORD001",
"CUST001",
LocalDate.of(2025, 1, 1),
List.of(
new OrderItem("PROD001", 2, BigDecimal.valueOf(50)),
new OrderItem("PROD002", 1, BigDecimal.valueOf(100))
),
"completed"
),
new Order(
"ORD002",
"CUST001",
LocalDate.of(2025, 1, 2),
List.of(
new OrderItem("PROD003", 5, BigDecimal.valueOf(200))
),
"completed"
)
);
var analytics = new OrderAnalytics(orders);
// Find high-value orders
List<Order> highValue = analytics.getHighValueOrders();
System.out.println("High value orders: " + highValue.size());
// Get total revenue
BigDecimal revenue = analytics.getTotalRevenue();
System.out.println("Total revenue: $" + revenue);
// Top customers
List<Map.Entry<String, Long>> topCustomers = analytics.getTopCustomers(5);
System.out.println("Top customers: " + topCustomers);
}
Parallel Streams
Parallel streams can improve performance for CPU-intensive operations on large datasets:
List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000)
.boxed()
.toList();
// Sequential
long sum = numbers.stream()
.mapToLong(Integer::longValue)
.sum();
// Parallel - automatically uses ForkJoinPool
long parallelSum = numbers.parallelStream()
.mapToLong(Integer::longValue)
.sum();
When to use parallel streams:
- Large datasets (thousands+ elements)
- CPU-intensive operations
- No shared mutable state
- Operations are independent
When to avoid:
- Small datasets (overhead not worth it)
- I/O operations (database, file, network)
- Operations with side effects
- Order-dependent operations
Stream Performance Tips
Use primitive streams for boxed types
// Slower - boxing overhead int sum = list.stream() .mapToInt(Integer::intValue) .sum(); // Faster - no boxing IntStream.range(0, 1000).sum();Short-circuit operations early
// Put filter before expensive operations list.stream() .filter(x -> x.isActive()) // cheap filter first .map(x -> expensiveTransform(x)) // expensive operation .toList();Avoid creating unnecessary intermediate collections
// Don't do this List<String> temp = list.stream().filter(...).toList(); List<String> result = temp.stream().map(...).toList(); // Do this instead List<String> result = list.stream() .filter(...) .map(...) .toList();Use
anyMatch/noneMatchinstead offilter().count()// Slower boolean hasActive = list.stream() .filter(User::isActive) .count() > 0; // Faster (short-circuits) boolean hasActive = list.stream() .anyMatch(User::isActive);
Common Stream Patterns
Grouping and partitioning:
Map<Boolean, List<Integer>> partitioned = numbers.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
// {true=[2, 4, 6], false=[1, 3, 5]}
Map<String, List<User>> byRole = users.stream()
.collect(Collectors.groupingBy(User::role));
Finding statistics:
IntSummaryStatistics stats = numbers.stream()
.mapToInt(Integer::intValue)
.summaryStatistics();
System.out.println("Count: " + stats.getCount());
System.out.println("Sum: " + stats.getSum());
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
System.out.println("Average: " + stats.getAverage());
Joining strings:
String csv = names.stream()
.collect(Collectors.joining(", "));
String quoted = names.stream()
.collect(Collectors.joining("', '", "'", "'"));
// 'Alice', 'Bob', 'Charlie'