Imagine an assembly line in a factory. Raw materials enter at one end, pass through a series of workstations — each performing a specific operation like cutting, painting, or inspecting — and a finished product comes out the other end. The assembly line does not store the materials; it processes them as they flow through.
The Java Stream API, introduced in Java 8, works exactly like that assembly line. A Stream is a sequence of elements that supports a pipeline of operations to process data declaratively — you describe what you want, not how to do it step by step.
Key characteristics of Streams:
Every Stream pipeline has three parts:
| Part | Description | Example |
|---|---|---|
| Source | Where the data comes from | list.stream(), Arrays.stream(arr) |
| Intermediate operations | Transform the stream (lazy, return a new Stream) | filter(), map(), sorted() |
| Terminal operation | Produces a result or side effect (triggers execution) | collect(), forEach(), count() |
import java.util.Arrays;
import java.util.List;
public class StreamIntro {
public static void main(String[] args) {
List names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
// Stream pipeline: source -> intermediate ops -> terminal op
long count = names.stream() // Source: create stream from list
.filter(n -> n.length() > 3) // Intermediate: keep names longer than 3 chars
.map(String::toUpperCase) // Intermediate: convert to uppercase
.count(); // Terminal: count remaining elements
System.out.println("Count: " + count);
// Output: Count: 3
// The original list is unchanged
System.out.println("Original: " + names);
// Output: Original: [Alice, Bob, Charlie, David, Eve]
}
}
Before you can process data with the Stream API, you need to create a Stream. Java provides multiple ways to do this depending on your data source.
The most common way. Every class that implements Collection (List, Set, Queue) has a stream() method.
import java.util.*;
public class StreamFromCollections {
public static void main(String[] args) {
// From a List
List list = List.of("Java", "Python", "Go");
list.stream().forEach(System.out::println);
// From a Set
Set set = Set.of(1, 2, 3, 4, 5);
set.stream().filter(n -> n % 2 == 0).forEach(System.out::println);
// From a Map (via entrySet, keySet, or values)
Map map = Map.of("Alice", 90, "Bob", 85);
map.entrySet().stream()
.filter(e -> e.getValue() > 87)
.forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
// Output: Alice: 90
}
}
import java.util.Arrays;
import java.util.stream.Stream;
public class StreamFromArrays {
public static void main(String[] args) {
String[] colors = {"Red", "Green", "Blue"};
// Using Arrays.stream()
Arrays.stream(colors).forEach(System.out::println);
// Partial array: from index 1 (inclusive) to 3 (exclusive)
Arrays.stream(colors, 1, 3).forEach(System.out::println);
// Output: Green, Blue
// Using Stream.of()
Stream.of("One", "Two", "Three").forEach(System.out::println);
// From a primitive array -- returns IntStream, not Stream
int[] numbers = {10, 20, 30};
int sum = Arrays.stream(numbers).sum();
System.out.println("Sum: " + sum); // Output: Sum: 60
}
}
import java.util.stream.Stream;
import java.util.stream.IntStream;
import java.util.List;
public class StreamFactoryMethods {
public static void main(String[] args) {
// Stream.empty() -- useful as a return value instead of null
Stream empty = Stream.empty();
System.out.println("Empty count: " + empty.count()); // Output: Empty count: 0
// Stream.of() -- create from individual elements
Stream languages = Stream.of("Java", "Python", "Go");
// Stream.generate() -- infinite stream from a Supplier
// MUST use limit() or it runs forever!
Stream.generate(Math::random)
.limit(3)
.forEach(n -> System.out.printf("%.2f%n", n));
// Stream.iterate() -- infinite stream with a seed and unary operator
// Java 8 style (no predicate -- must use limit)
Stream.iterate(1, n -> n * 2)
.limit(5)
.forEach(System.out::println);
// Output: 1, 2, 4, 8, 16
// Java 9+ style (with predicate -- like a for loop)
Stream.iterate(1, n -> n <= 100, n -> n * 2)
.forEach(System.out::println);
// Output: 1, 2, 4, 8, 16, 32, 64
// IntStream.range() and rangeClosed()
IntStream.range(1, 5).forEach(System.out::println); // 1, 2, 3, 4
IntStream.rangeClosed(1, 5).forEach(System.out::println); // 1, 2, 3, 4, 5
}
}
You can create a Stream of lines from a file using Files.lines(). This is memory-efficient because it reads lines lazily rather than loading the entire file into memory.
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.stream.Stream;
public class StreamFromFiles {
public static void main(String[] args) {
// Files.lines() returns a Stream -- one element per line
// Use try-with-resources because the stream must be closed
try (Stream lines = Files.lines(Paths.get("data.txt"))) {
lines.filter(line -> !line.isBlank())
.map(String::trim)
.forEach(System.out::println);
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
}
}
| Method | Returns | Use Case |
|---|---|---|
collection.stream() |
Stream<T> |
Most common — stream from any Collection |
Arrays.stream(array) |
Stream<T> or IntStream |
Stream from an array |
Stream.of(a, b, c) |
Stream<T> |
Stream from individual values |
Stream.empty() |
Stream<T> |
Empty stream (null-safe return) |
Stream.generate(supplier) |
Stream<T> |
Infinite stream from a Supplier |
Stream.iterate(seed, op) |
Stream<T> |
Infinite stream with iterative computation |
IntStream.range(a, b) |
IntStream |
Range of ints [a, b) |
IntStream.rangeClosed(a, b) |
IntStream |
Range of ints [a, b] |
Files.lines(path) |
Stream<String> |
Lazy line-by-line file reading |
Intermediate operations transform a Stream into another Stream. They are lazy — nothing happens until a terminal operation triggers the pipeline. You can chain as many intermediate operations as you need.
filter(Predicate<T>) keeps only the elements that match the given condition. Think of it as a sieve — elements that pass the test go through; those that do not are discarded.
import java.util.List;
import java.util.stream.Collectors;
public class FilterExample {
public static void main(String[] args) {
List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Keep only even numbers
List evens = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println("Evens: " + evens);
// Output: Evens: [2, 4, 6, 8, 10]
// Chaining multiple filters (equivalent to && in the predicate)
List result = numbers.stream()
.filter(n -> n > 3)
.filter(n -> n < 8)
.collect(Collectors.toList());
System.out.println("Between 3 and 8: " + result);
// Output: Between 3 and 8: [4, 5, 6, 7]
// Filter with objects
List names = List.of("Alice", "Bob", "Charlie", "Ana", "Albert");
List aNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println("A-names: " + aNames);
// Output: A-names: [Alice, Ana, Albert]
}
}
map(Function<T, R>) transforms each element from type T to type R. It applies the given function to every element and produces a new Stream of the results. This is one of the most frequently used operations.
import java.util.List;
import java.util.stream.Collectors;
public class MapExample {
public static void main(String[] args) {
List names = List.of("alice", "bob", "charlie");
// Transform: String -> String (uppercase)
List upper = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upper);
// Output: [ALICE, BOB, CHARLIE]
// Transform: String -> Integer (get length)
List lengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(lengths);
// Output: [5, 3, 7]
// Transform: Integer -> String
List numbers = List.of(1, 2, 3);
List labels = numbers.stream()
.map(n -> "Item #" + n)
.collect(Collectors.toList());
System.out.println(labels);
// Output: [Item #1, Item #2, Item #3]
}
}
flatMap(Function<T, Stream<R>>) is used when each element maps to multiple elements (a stream of values). It “flattens” nested structures into a single stream. This is essential when you have lists of lists, or when a mapping function returns a collection for each element.
import java.util.List;
import java.util.stream.Collectors;
public class FlatMapExample {
public static void main(String[] args) {
// Problem: We have a list of lists and want a single flat list
List> nested = List.of(
List.of("Java", "Kotlin"),
List.of("Python", "Ruby"),
List.of("Go", "Rust")
);
// Using map() -- gives Stream>, NOT what we want
// Using flatMap() -- gives Stream, flattened!
List flat = nested.stream()
.flatMap(List::stream) // Each list becomes a stream, all merged
.collect(Collectors.toList());
System.out.println(flat);
// Output: [Java, Kotlin, Python, Ruby, Go, Rust]
// Real-world: extracting all words from sentences
List sentences = List.of("Hello World", "Java Streams are powerful");
List words = sentences.stream()
.flatMap(s -> List.of(s.split(" ")).stream())
.collect(Collectors.toList());
System.out.println(words);
// Output: [Hello, World, Java, Streams, are, powerful]
// Real-world: customers with multiple orders
// Each customer has a list of orders; we want all orders in one stream
// customer.stream().flatMap(c -> c.getOrders().stream())
}
}
sorted() sorts elements in natural order (for types implementing Comparable). You can also pass a custom Comparator for complex sorting.
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class SortedExample {
public static void main(String[] args) {
// Natural order (ascending)
List numbers = List.of(5, 3, 8, 1, 9, 2);
List sorted = numbers.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sorted);
// Output: [1, 2, 3, 5, 8, 9]
// Reverse order
List descending = numbers.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
System.out.println(descending);
// Output: [9, 8, 5, 3, 2, 1]
// Sorting strings by length
List names = List.of("Charlie", "Bob", "Alice", "Eve");
List byLength = names.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList());
System.out.println(byLength);
// Output: [Bob, Eve, Alice, Charlie]
// Sorting by length, then alphabetically for ties
List byLengthThenAlpha = names.stream()
.sorted(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()))
.collect(Collectors.toList());
System.out.println(byLengthThenAlpha);
// Output: [Bob, Eve, Alice, Charlie]
}
}
distinct() removes duplicate elements from the stream. It relies on the equals() and hashCode() methods to determine equality. For custom objects, you must override these methods for distinct() to work correctly.
import java.util.List;
import java.util.stream.Collectors;
public class DistinctExample {
public static void main(String[] args) {
List numbers = List.of(1, 2, 3, 2, 4, 3, 5, 1);
List unique = numbers.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(unique);
// Output: [1, 2, 3, 4, 5]
// With strings (equals/hashCode already implemented)
List words = List.of("hello", "world", "hello", "java", "world");
List uniqueWords = words.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueWords);
// Output: [hello, world, java]
}
}
peek(Consumer<T>) allows you to perform a side effect on each element without modifying the stream. Its primary use is debugging — inspecting elements at a certain stage of the pipeline. Avoid using peek() for business logic; it may not execute if the pipeline is optimized away.
import java.util.List;
import java.util.stream.Collectors;
public class PeekExample {
public static void main(String[] args) {
List result = List.of("one", "two", "three", "four")
.stream()
.filter(s -> s.length() > 3)
.peek(s -> System.out.println("After filter: " + s))
.map(String::toUpperCase)
.peek(s -> System.out.println("After map: " + s))
.collect(Collectors.toList());
// Output:
// After filter: three
// After map: THREE
// After filter: four
// After map: FOUR
System.out.println("Result: " + result);
// Output: Result: [THREE, FOUR]
}
}
limit(n) truncates the stream to at most n elements. skip(n) discards the first n elements. Together, they form a powerful pagination pattern.
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class LimitSkipExample {
public static void main(String[] args) {
List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// First 3 elements
List firstThree = numbers.stream()
.limit(3)
.collect(Collectors.toList());
System.out.println("First 3: " + firstThree);
// Output: First 3: [1, 2, 3]
// Skip first 7 elements
List lastThree = numbers.stream()
.skip(7)
.collect(Collectors.toList());
System.out.println("Last 3: " + lastThree);
// Output: Last 3: [8, 9, 10]
// Pagination pattern: page 2, page size 3 (items 4, 5, 6)
int pageSize = 3;
int pageNumber = 2; // 1-based
List page = numbers.stream()
.skip((long) (pageNumber - 1) * pageSize)
.limit(pageSize)
.collect(Collectors.toList());
System.out.println("Page 2: " + page);
// Output: Page 2: [4, 5, 6]
}
}
These operations convert a Stream<T> to a primitive stream (IntStream, LongStream, DoubleStream). Primitive streams avoid autoboxing overhead and provide specialized methods like sum(), average(), and max().
import java.util.List;
public class MapToPrimitiveExample {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie", "David");
// mapToInt: get lengths as IntStream
int totalChars = names.stream()
.mapToInt(String::length)
.sum();
System.out.println("Total characters: " + totalChars);
// Output: Total characters: 20
// average returns OptionalDouble
names.stream()
.mapToInt(String::length)
.average()
.ifPresent(avg -> System.out.printf("Average length: %.1f%n", avg));
// Output: Average length: 5.0
// mapToDouble: useful for decimal calculations
List prices = List.of(100, 200, 300);
double totalWithTax = prices.stream()
.mapToDouble(p -> p * 1.08)
.sum();
System.out.printf("Total with tax: %.2f%n", totalWithTax);
// Output: Total with tax: 648.00
}
}
| Operation | Input | Output | Purpose |
|---|---|---|---|
filter(Predicate) |
Stream<T> |
Stream<T> |
Keep elements matching condition |
map(Function) |
Stream<T> |
Stream<R> |
Transform each element |
flatMap(Function) |
Stream<T> |
Stream<R> |
Flatten nested streams |
sorted() |
Stream<T> |
Stream<T> |
Sort elements |
distinct() |
Stream<T> |
Stream<T> |
Remove duplicates |
peek(Consumer) |
Stream<T> |
Stream<T> |
Debug / inspect |
limit(long) |
Stream<T> |
Stream<T> |
Truncate to n elements |
skip(long) |
Stream<T> |
Stream<T> |
Skip first n elements |
mapToInt(Function) |
Stream<T> |
IntStream |
Convert to primitive int stream |
Terminal operations are the final step of a stream pipeline. They trigger the execution of all intermediate operations and produce a result (a value, a collection, or a side effect). Once a terminal operation is called, the stream is consumed and cannot be reused.
forEach(Consumer<T>) performs an action on each element. It is the stream equivalent of a for-each loop. Note that forEach does not guarantee order when used with parallel streams. Use forEachOrdered() if order matters.
import java.util.List;
public class ForEachExample {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie");
// Simple forEach
names.stream().forEach(System.out::println);
// Output: Alice, Bob, Charlie
// forEach with lambda
names.stream().forEach(name -> System.out.println("Hello, " + name + "!"));
// Output:
// Hello, Alice!
// Hello, Bob!
// Hello, Charlie!
// Warning: forEach on parallel stream -- order NOT guaranteed
names.parallelStream().forEach(System.out::println);
// Output: order may vary!
// Use forEachOrdered to maintain encounter order
names.parallelStream().forEachOrdered(System.out::println);
// Output: Alice, Bob, Charlie (guaranteed order)
}
}
collect() is the most versatile terminal operation. It transforms the stream elements into a collection, string, or other summary result using a Collector. The Collectors utility class provides dozens of ready-made collectors.
import java.util.*;
import java.util.stream.Collectors;
public class CollectExample {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie", "Alice", "David");
// Collect to List
List list = names.stream()
.filter(n -> n.length() > 3)
.collect(Collectors.toList());
System.out.println("List: " + list);
// Output: List: [Alice, Charlie, Alice, David]
// Collect to Set (removes duplicates)
Set set = names.stream()
.collect(Collectors.toSet());
System.out.println("Set: " + set);
// Output: Set: [Bob, Alice, Charlie, David]
// Collect to unmodifiable List (Java 10+)
List immutable = names.stream()
.collect(Collectors.toUnmodifiableList());
// Collect to Map (name -> length)
Map nameToLength = names.stream()
.distinct()
.collect(Collectors.toMap(
name -> name, // key mapper
String::length // value mapper
));
System.out.println("Map: " + nameToLength);
// Output: Map: {Alice=5, Bob=3, Charlie=7, David=5}
// Joining strings
String joined = names.stream()
.distinct()
.collect(Collectors.joining(", "));
System.out.println("Joined: " + joined);
// Output: Joined: Alice, Bob, Charlie, David
// Joining with prefix and suffix
String formatted = names.stream()
.distinct()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println("Formatted: " + formatted);
// Output: Formatted: [Alice, Bob, Charlie, David]
}
}
reduce() combines all elements of a stream into a single result by repeatedly applying a binary operation. It is the building block behind sum(), max(), and count() — those are all specialized reductions.
import java.util.List;
import java.util.Optional;
public class ReduceExample {
public static void main(String[] args) {
List numbers = List.of(1, 2, 3, 4, 5);
// With identity value: returns int (never empty)
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println("Sum: " + sum);
// Output: Sum: 15
// Without identity: returns Optional (might be empty)
Optional product = numbers.stream()
.reduce((a, b) -> a * b);
product.ifPresent(p -> System.out.println("Product: " + p));
// Output: Product: 120
// Finding the maximum
Optional max = numbers.stream()
.reduce(Integer::max);
System.out.println("Max: " + max.orElse(0));
// Output: Max: 5
// String concatenation with reduce
List words = List.of("Java", "Stream", "API");
String sentence = words.stream()
.reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b);
System.out.println(sentence);
// Output: Java Stream API
// How reduce works step-by-step for sum:
// Step 1: identity(0) + 1 = 1
// Step 2: 1 + 2 = 3
// Step 3: 3 + 3 = 6
// Step 4: 6 + 4 = 10
// Step 5: 10 + 5 = 15
}
}
import java.util.List;
import java.util.Optional;
public class CountFindExample {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie", "David", "Eve");
// count() -- number of elements
long count = names.stream()
.filter(n -> n.length() > 3)
.count();
System.out.println("Names longer than 3: " + count);
// Output: Names longer than 3: 3
// findFirst() -- first element in encounter order, returns Optional
Optional first = names.stream()
.filter(n -> n.startsWith("C"))
.findFirst();
System.out.println("First C-name: " + first.orElse("none"));
// Output: First C-name: Charlie
// findAny() -- any matching element (useful in parallel streams)
Optional any = names.parallelStream()
.filter(n -> n.length() == 3)
.findAny();
System.out.println("Any 3-letter name: " + any.orElse("none"));
// Output: Any 3-letter name: Bob (or Eve in parallel)
}
}
These are short-circuiting terminal operations that return a boolean. They stop processing as soon as the answer is determined.
import java.util.List;
public class MatchExample {
public static void main(String[] args) {
List numbers = List.of(2, 4, 6, 8, 10);
// anyMatch: is there at least one element > 7?
boolean hasLarge = numbers.stream().anyMatch(n -> n > 7);
System.out.println("Any > 7? " + hasLarge);
// Output: Any > 7? true
// allMatch: are ALL elements even?
boolean allEven = numbers.stream().allMatch(n -> n % 2 == 0);
System.out.println("All even? " + allEven);
// Output: All even? true
// noneMatch: are there NO negative numbers?
boolean noNegatives = numbers.stream().noneMatch(n -> n < 0);
System.out.println("No negatives? " + noNegatives);
// Output: No negatives? true
}
}
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
public class MinMaxToArrayExample {
public static void main(String[] args) {
List names = List.of("Charlie", "Bob", "Alice", "David");
// min -- requires a Comparator
Optional shortest = names.stream()
.min(Comparator.comparingInt(String::length));
System.out.println("Shortest: " + shortest.orElse("none"));
// Output: Shortest: Bob
// max
Optional longest = names.stream()
.max(Comparator.comparingInt(String::length));
System.out.println("Longest: " + longest.orElse("none"));
// Output: Longest: Charlie
// toArray -- convert stream to array
String[] nameArray = names.stream()
.filter(n -> n.length() > 3)
.toArray(String[]::new);
System.out.println("Array length: " + nameArray.length);
// Output: Array length: 3
}
}
| Operation | Return Type | Purpose |
|---|---|---|
forEach(Consumer) |
void |
Perform action on each element |
collect(Collector) |
R |
Accumulate into a collection or summary |
reduce(identity, BinaryOp) |
T |
Combine all elements into one value |
count() |
long |
Count elements |
findFirst() |
Optional<T> |
First element (encounter order) |
findAny() |
Optional<T> |
Any element (optimized for parallel) |
anyMatch(Predicate) |
boolean |
At least one matches? |
allMatch(Predicate) |
boolean |
All match? |
noneMatch(Predicate) |
boolean |
None match? |
min(Comparator) |
Optional<T> |
Minimum element |
max(Comparator) |
Optional<T> |
Maximum element |
toArray() |
Object[] or T[] |
Convert to array |
The Collectors class is the powerhouse of the Stream API. Beyond basic toList() and toSet(), it provides sophisticated collectors for grouping, partitioning, summarizing, and more. Mastering these collectors will dramatically improve the expressiveness of your code.
groupingBy() groups stream elements by a classification function, producing a Map<K, List<T>>. This is the stream equivalent of SQL's GROUP BY.
import java.util.*;
import java.util.stream.Collectors;
public class GroupingByExample {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie", "Anna", "Ben", "Chris");
// Group by first letter
Map> byFirstLetter = names.stream()
.collect(Collectors.groupingBy(name -> name.charAt(0)));
System.out.println(byFirstLetter);
// Output: {A=[Alice, Anna], B=[Bob, Ben], C=[Charlie, Chris]}
// Group by string length
Map> byLength = names.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println(byLength);
// Output: {3=[Bob, Ben], 4=[Anna], 5=[Alice, Chris], 7=[Charlie]}
// groupingBy with downstream collector: count per group
Map countByLetter = names.stream()
.collect(Collectors.groupingBy(
name -> name.charAt(0),
Collectors.counting()
));
System.out.println(countByLetter);
// Output: {A=2, B=2, C=2}
// groupingBy with downstream collector: join names per group
Map joinedByLetter = names.stream()
.collect(Collectors.groupingBy(
name -> name.charAt(0),
Collectors.joining(", ")
));
System.out.println(joinedByLetter);
// Output: {A=Alice, Anna, B=Bob, Ben, C=Charlie, Chris}
// Multi-level grouping: group by length, then by first letter
Map>> multiLevel = names.stream()
.collect(Collectors.groupingBy(
String::length,
Collectors.groupingBy(name -> name.charAt(0))
));
System.out.println(multiLevel);
// Output: {3={B=[Bob, Ben]}, 4={A=[Anna]}, 5={A=[Alice], C=[Chris]}, 7={C=[Charlie]}}
}
}
partitioningBy() is a special case of groupingBy() that splits elements into exactly two groups based on a Predicate -- a true group and a false group. The result is always Map<Boolean, List<T>>.
import java.util.*;
import java.util.stream.Collectors;
public class PartitioningByExample {
public static void main(String[] args) {
List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Partition into even and odd
Map> evenOdd = numbers.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
System.out.println("Even: " + evenOdd.get(true));
System.out.println("Odd: " + evenOdd.get(false));
// Output:
// Even: [2, 4, 6, 8, 10]
// Odd: [1, 3, 5, 7, 9]
// Partition with downstream collector: count each group
Map counts = numbers.stream()
.collect(Collectors.partitioningBy(
n -> n > 5,
Collectors.counting()
));
System.out.println("Greater than 5: " + counts.get(true));
System.out.println("5 or less: " + counts.get(false));
// Output:
// Greater than 5: 5
// 5 or less: 5
}
}
When collecting to a Map, duplicate keys cause an IllegalStateException. You must provide a merge function to handle collisions.
import java.util.*;
import java.util.stream.Collectors;
public class ToMapMergeExample {
public static void main(String[] args) {
List words = List.of("hello", "world", "hello", "java", "world");
// Problem: duplicate keys without merge function throws exception
// Solution: provide a merge function
Map wordCount = words.stream()
.collect(Collectors.toMap(
word -> word, // key: the word itself
word -> 1, // value: count of 1
Integer::sum // merge: add counts for duplicate keys
));
System.out.println(wordCount);
// Output: {hello=2, world=2, java=1}
// Collecting to a specific Map implementation (LinkedHashMap preserves order)
Map orderedCount = words.stream()
.collect(Collectors.toMap(
word -> word,
word -> 1,
Integer::sum,
LinkedHashMap::new // supplier for the Map type
));
System.out.println(orderedCount);
// Output: {hello=2, world=2, java=1}
}
}
summarizingInt(), summarizingLong(), and summarizingDouble() collect comprehensive statistics in a single pass -- count, sum, min, max, and average.
import java.util.*;
import java.util.stream.Collectors;
public class SummarizingExample {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie", "David", "Eve");
IntSummaryStatistics stats = names.stream()
.collect(Collectors.summarizingInt(String::length));
System.out.println("Count: " + stats.getCount()); // 5
System.out.println("Sum: " + stats.getSum()); // 24
System.out.println("Min: " + stats.getMin()); // 3
System.out.println("Max: " + stats.getMax()); // 7
System.out.printf("Average: %.1f%n", stats.getAverage()); // 4.8
}
}
Parallel streams split the data into multiple chunks and process them simultaneously on different threads using the ForkJoinPool. This can significantly speed up processing of large datasets on multi-core machines -- but parallelism is not free and can hurt performance when used incorrectly.
import java.util.List;
import java.util.stream.IntStream;
public class ParallelStreamExample {
public static void main(String[] args) {
List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Method 1: parallelStream() from collection
long sum1 = numbers.parallelStream()
.mapToLong(Integer::longValue)
.sum();
// Method 2: .parallel() on an existing stream
long sum2 = numbers.stream()
.parallel()
.mapToLong(Integer::longValue)
.sum();
System.out.println("Sum1: " + sum1 + ", Sum2: " + sum2);
// Output: Sum1: 55, Sum2: 55
// Demonstrating parallel execution with thread names
System.out.println("--- Sequential ---");
IntStream.range(1, 5).forEach(i ->
System.out.println(i + " on " + Thread.currentThread().getName()));
System.out.println("--- Parallel ---");
IntStream.range(1, 5).parallel().forEach(i ->
System.out.println(i + " on " + Thread.currentThread().getName()));
// Parallel output shows different thread names (ForkJoinPool.commonPool-worker-*)
}
}
| Use Parallel When | Avoid Parallel When |
|---|---|
| Large datasets (100,000+ elements) | Small datasets (overhead > benefit) |
| CPU-intensive operations per element | I/O-bound operations (network, file) |
| Operations are independent (no shared state) | Operations depend on encounter order |
| Source is easy to split (ArrayList, arrays) | Source is hard to split (LinkedList, Stream.iterate) |
| Stateless intermediate operations | Stateful operations (sorted, distinct, limit) |
Common mistake: Using parallel streams with shared mutable state. This leads to race conditions and incorrect results.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class ParallelStreamDanger {
public static void main(String[] args) {
// WRONG: modifying a shared list from a parallel stream
List unsafeList = new ArrayList<>();
IntStream.range(0, 1000)
.parallel()
.forEach(unsafeList::add); // Race condition!
System.out.println("Unsafe size: " + unsafeList.size());
// Output: might be less than 1000 or throw ArrayIndexOutOfBoundsException!
// RIGHT: use collect() instead
List safeList = IntStream.range(0, 1000)
.parallel()
.boxed()
.collect(Collectors.toList());
System.out.println("Safe size: " + safeList.size());
// Output: Safe size: 1000
}
}
Many terminal stream operations return an Optional -- a container that may or may not hold a value. This forces you to handle the "no result" case explicitly, preventing NullPointerException.
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
public class OptionalWithStreams {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie");
// findFirst returns Optional
Optional first = names.stream()
.filter(n -> n.startsWith("Z"))
.findFirst();
// Handle the Optional
String result = first.orElse("No match found");
System.out.println(result);
// Output: No match found
// ifPresent -- only execute if a value exists
names.stream()
.filter(n -> n.startsWith("C"))
.findFirst()
.ifPresent(name -> System.out.println("Found: " + name));
// Output: Found: Charlie
// map on Optional -- transform the value if present
Optional length = names.stream()
.filter(n -> n.startsWith("A"))
.findFirst()
.map(String::length);
System.out.println("Length: " + length.orElse(0));
// Output: Length: 5
// orElseThrow -- throw exception if empty
// names.stream().filter(n -> n.startsWith("Z")).findFirst()
// .orElseThrow(() -> new IllegalArgumentException("No Z names"));
// Java 9+: Optional.stream() -- converts Optional to a 0-or-1 element stream
// Useful for flatMapping a stream of Optionals
List> optionals = List.of(
Optional.of("Hello"),
Optional.empty(),
Optional.of("World")
);
List values = optionals.stream()
.flatMap(Optional::stream)
.toList();
System.out.println(values);
// Output: [Hello, World]
}
}
Java provides three specialized stream types for primitives: IntStream, LongStream, and DoubleStream. These avoid the overhead of autoboxing (converting int to Integer and back) and provide specialized methods like sum(), average(), and summaryStatistics().
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.stream.IntStream;
import java.util.stream.DoubleStream;
import java.util.List;
public class PrimitiveStreamExample {
public static void main(String[] args) {
// IntStream creation
IntStream range = IntStream.rangeClosed(1, 10);
// sum, average, min, max
int sum = IntStream.rangeClosed(1, 10).sum();
System.out.println("Sum 1-10: " + sum);
// Output: Sum 1-10: 55
OptionalDouble avg = IntStream.of(85, 90, 78, 92, 88).average();
System.out.println("Average: " + avg.orElse(0));
// Output: Average: 86.6
OptionalInt max = IntStream.of(85, 90, 78, 92, 88).max();
System.out.println("Max: " + max.orElse(0));
// Output: Max: 92
// summaryStatistics() -- all stats in one pass
var stats = IntStream.of(85, 90, 78, 92, 88).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.printf("Avg: %.1f%n", stats.getAverage());
// boxed() -- convert IntStream to Stream
List boxedList = IntStream.rangeClosed(1, 5)
.boxed()
.toList();
System.out.println("Boxed: " + boxedList);
// Output: Boxed: [1, 2, 3, 4, 5]
// mapToObj -- convert each int to an object
List labels = IntStream.rangeClosed(1, 3)
.mapToObj(i -> "Item " + i)
.toList();
System.out.println("Labels: " + labels);
// Output: Labels: [Item 1, Item 2, Item 3]
// Converting between Stream and primitive streams
List names = List.of("Alice", "Bob", "Charlie");
IntStream lengths = names.stream().mapToInt(String::length);
System.out.println("Total chars: " + lengths.sum());
// Output: Total chars: 15
}
}
This section demonstrates practical, real-world patterns you will use repeatedly in production code. These patterns solve common data-processing problems elegantly with streams.
import java.util.*;
import java.util.stream.Collectors;
public class CommonPatterns {
record Product(String name, String category, double price) {}
public static void main(String[] args) {
List products = List.of(
new Product("Laptop", "Electronics", 999.99),
new Product("Headphones", "Electronics", 79.99),
new Product("Coffee Maker", "Kitchen", 49.99),
new Product("Blender", "Kitchen", 39.99),
new Product("Monitor", "Electronics", 349.99),
new Product("Toaster", "Kitchen", 29.99)
);
// Filter by category and sort by price
List electronics = products.stream()
.filter(p -> p.category().equals("Electronics"))
.sorted(Comparator.comparingDouble(Product::price))
.collect(Collectors.toList());
electronics.forEach(p -> System.out.println(p.name() + " $" + p.price()));
// Output:
// Headphones $79.99
// Monitor $349.99
// Laptop $999.99
// Find the top 2 most expensive products
List topTwo = products.stream()
.sorted(Comparator.comparingDouble(Product::price).reversed())
.limit(2)
.map(Product::name)
.collect(Collectors.toList());
System.out.println("Top 2: " + topTwo);
// Output: Top 2: [Laptop, Monitor]
// Group by category and calculate average price per category
Map avgByCategory = products.stream()
.collect(Collectors.groupingBy(
Product::category,
Collectors.averagingDouble(Product::price)
));
avgByCategory.forEach((cat, avg) ->
System.out.printf("%s avg: $%.2f%n", cat, avg));
// Output:
// Electronics avg: $476.66
// Kitchen avg: $39.99
// Create a comma-separated string of product names
String productList = products.stream()
.map(Product::name)
.collect(Collectors.joining(", "));
System.out.println("Products: " + productList);
// Output: Products: Laptop, Headphones, Coffee Maker, Blender, Monitor, Toaster
// Convert to a Map: name -> price
Map priceMap = products.stream()
.collect(Collectors.toMap(Product::name, Product::price));
System.out.println("Laptop price: $" + priceMap.get("Laptop"));
// Output: Laptop price: $999.99
}
}
import java.util.*;
import java.util.stream.Collectors;
public class FlatteningPattern {
record Student(String name, List courses) {}
public static void main(String[] args) {
List students = List.of(
new Student("Alice", List.of("Math", "Physics", "CS")),
new Student("Bob", List.of("CS", "English", "Math")),
new Student("Charlie", List.of("Biology", "Chemistry"))
);
// Get all unique courses offered
Set allCourses = students.stream()
.flatMap(s -> s.courses().stream())
.collect(Collectors.toSet());
System.out.println("All courses: " + allCourses);
// Output: All courses: [Biology, CS, Chemistry, English, Math, Physics]
// Find students taking "CS"
List csStudents = students.stream()
.filter(s -> s.courses().contains("CS"))
.map(Student::name)
.collect(Collectors.toList());
System.out.println("CS students: " + csStudents);
// Output: CS students: [Alice, Bob]
}
}
Streams are not always better than loops, and loops are not always better than streams. Understanding when to use each is a sign of a mature Java developer.
| Criteria | Stream | Traditional Loop |
|---|---|---|
| Readability | Excellent for data transformations (filter, map, collect) | Better for simple iterations with side effects |
| Debugging | Harder -- stack traces are less clear, peek() helps | Easier -- set breakpoints, inspect variables |
| Performance | Slight overhead for small datasets; parallel() helps with large | Generally faster for simple operations on small data |
| Mutability | Encourages immutability (functional style) | Naturally works with mutable state |
| Short-circuiting | Built-in (findFirst, anyMatch, limit) | Manual (break, return) |
| Parallelism | Trivial -- just call parallel() | Complex -- manual thread management |
| State management | Stateless operations preferred | Stateful iteration is natural |
import java.util.*;
import java.util.stream.Collectors;
public class StreamVsLoop {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie", "David", "Eve", "Frank");
// Task: Get uppercase names that are longer than 3 characters
// --- Loop approach ---
List resultLoop = new ArrayList<>();
for (String name : names) {
if (name.length() > 3) {
resultLoop.add(name.toUpperCase());
}
}
System.out.println("Loop: " + resultLoop);
// --- Stream approach ---
List resultStream = names.stream()
.filter(n -> n.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("Stream: " + resultStream);
// Both output: [ALICE, CHARLIE, DAVID, FRANK]
// Stream is more readable here -- the intent is clear at a glance
}
}
Rule of thumb: Use streams for data transformation pipelines (filter, map, collect, group). Use loops when you need to maintain complex local state, perform multiple related side effects, or when the logic is inherently imperative (like building a graph or managing indices).
These are mistakes that even experienced developers make when working with the Stream API. Understanding them will save you hours of debugging.
A stream can only be consumed once. Attempting to reuse it throws an IllegalStateException.
import java.util.List;
import java.util.stream.Stream;
public class ReuseStreamMistake {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie");
Stream stream = names.stream().filter(n -> n.length() > 3);
// First use -- works fine
long count = stream.count();
System.out.println("Count: " + count);
// Second use -- THROWS IllegalStateException!
// stream.forEach(System.out::println);
// java.lang.IllegalStateException: stream has already been operated upon or closed
// Fix: create a new stream each time
long count2 = names.stream().filter(n -> n.length() > 3).count();
names.stream().filter(n -> n.length() > 3).forEach(System.out::println);
}
}
Intermediate operations like map() and filter() should be stateless and free of side effects. Modifying external state from these operations leads to unpredictable behavior, especially with parallel streams.
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class SideEffectMistake {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie");
// WRONG: modifying external state inside map()
List sideEffectList = new ArrayList<>();
names.stream()
.map(n -> {
sideEffectList.add(n); // Side effect! Don't do this.
return n.toUpperCase();
})
.collect(Collectors.toList());
// RIGHT: use collect() to gather results
List upper = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
}
}
import java.util.stream.Stream;
public class InfiniteStreamMistake {
public static void main(String[] args) {
// WRONG: this runs forever and causes OutOfMemoryError
// Stream.generate(Math::random).forEach(System.out::println);
// RIGHT: always use limit() with generate() or iterate()
Stream.generate(Math::random)
.limit(5)
.forEach(n -> System.out.printf("%.2f%n", n));
// Or use the Java 9+ iterate with a predicate
Stream.iterate(1, n -> n <= 100, n -> n * 2)
.forEach(System.out::println);
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class ModifySourceMistake {
public static void main(String[] args) {
List names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
// WRONG: modifying the source while streaming -- ConcurrentModificationException!
// names.stream()
// .filter(n -> n.startsWith("A"))
// .forEach(n -> names.remove(n));
// RIGHT: collect results, then modify
List toRemove = names.stream()
.filter(n -> n.startsWith("A"))
.collect(Collectors.toList());
names.removeAll(toRemove);
System.out.println(names);
// Output: [Bob, Charlie]
// Or use removeIf() which is simpler
// names.removeIf(n -> n.startsWith("A"));
}
}
import java.util.List;
import java.util.stream.Collectors;
public class PerformanceTrapMistake {
public static void main(String[] args) {
List numbers = List.of(1, 2, 3, 4, 5);
// SLOW: unnecessary boxing -- Stream instead of IntStream
int sum1 = numbers.stream()
.map(n -> n * 2) // boxes/unboxes Integer repeatedly
.reduce(0, Integer::sum);
// FAST: use primitive stream
int sum2 = numbers.stream()
.mapToInt(n -> n * 2) // works with primitive int
.sum();
// WASTEFUL: sorting the entire stream just to find the max
// numbers.stream().sorted(Comparator.reverseOrder()).findFirst();
// EFFICIENT: use max() directly
// numbers.stream().max(Comparator.naturalOrder());
System.out.println("Sum: " + sum2);
// Output: Sum: 30
}
}
| Mistake | Symptom | Fix |
|---|---|---|
| Reusing a consumed stream | IllegalStateException |
Create a new stream each time |
| Side effects in map/filter | Unpredictable results in parallel | Use collect() for results, keep lambdas pure |
| Infinite stream without limit | Program hangs or OutOfMemoryError |
Always use limit() with generate()/iterate() |
| Modifying source during stream | ConcurrentModificationException |
Collect first, then modify; or use removeIf() |
| Unnecessary boxing | Poor performance | Use mapToInt()/mapToLong()/mapToDouble() |
| Sorting just to get min/max | O(n log n) instead of O(n) | Use min()/max() directly |
Following these best practices will help you write stream code that is clean, efficient, and maintainable.
Each lambda in a stream pipeline should do one thing. If your lambda is more than 2-3 lines, extract it into a named method.
import java.util.List;
import java.util.stream.Collectors;
public class BestPractices {
// POOR: complex inline lambda
// list.stream().filter(e -> e.getAge() > 18 && e.getSalary() > 50000
// && e.getDepartment().equals("Engineering")).collect(Collectors.toList());
// BETTER: extract to a method
static boolean isSeniorEngineer(Employee e) {
return e.age > 18
&& e.salary > 50000
&& e.department.equals("Engineering");
}
record Employee(String name, int age, double salary, String department) {}
public static void main(String[] args) {
List employees = List.of(
new Employee("Alice", 30, 85000, "Engineering"),
new Employee("Bob", 25, 45000, "Marketing")
);
List seniors = employees.stream()
.filter(BestPractices::isSeniorEngineer)
.collect(Collectors.toList());
}
}
Method references are more concise and communicate intent better than equivalent lambdas.
import java.util.List;
import java.util.stream.Collectors;
public class MethodReferencePractice {
public static void main(String[] args) {
List names = List.of("alice", "bob", "charlie");
// Lambda (works but verbose)
List upper1 = names.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
// Method reference (cleaner)
List upper2 = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// More examples:
// s -> System.out.println(s) -> System.out::println
// s -> s.length() -> String::length
// s -> Integer.parseInt(s) -> Integer::parseInt
// () -> new ArrayList<>() -> ArrayList::new
}
}
Each operation in a stream pipeline should be on its own line, with consistent indentation. This makes the pipeline easy to read and modify.
import java.util.List;
import java.util.stream.Collectors;
public class FormattingPractice {
record Employee(String name, String dept, double salary) {}
public static void main(String[] args) {
List employees = List.of(
new Employee("Alice", "Engineering", 95000),
new Employee("Bob", "Marketing", 65000),
new Employee("Charlie", "Engineering", 85000)
);
// POOR: all on one line
// List result = employees.stream().filter(e -> e.dept().equals("Engineering")).map(Employee::name).sorted().collect(Collectors.toList());
// GOOD: one operation per line, aligned at the dot
List result = employees.stream()
.filter(e -> e.dept().equals("Engineering"))
.map(Employee::name)
.sorted()
.collect(Collectors.toList());
System.out.println(result);
// Output: [Alice, Charlie]
}
}
| # | Practice | Why |
|---|---|---|
| 1 | Keep lambdas short; extract complex logic to named methods | Readability, testability, reusability |
| 2 | Use method references (String::toUpperCase) |
Cleaner, more concise |
| 3 | Avoid side effects in intermediate operations | Predictable behavior, safe parallelism |
| 4 | Use primitive streams for numbers (mapToInt) |
Avoids autoboxing overhead |
| 5 | Do not over-use streams; simple loops are fine | Not everything benefits from streams |
| 6 | Format one operation per line | Readability, easy to add/remove steps |
| 7 | Prefer collect() over forEach() + mutation |
Thread-safe, functional style |
| 8 | Use Optional results properly (orElse, ifPresent) |
Avoid NullPointerException |
| 9 | Use parallel streams only when justified | Parallelism has overhead; profile first |
| 10 | Favor toList() (Java 16+) over collect(Collectors.toList()) |
Shorter and returns unmodifiable list |
Let us tie everything together with a real-world example. We will build an Employee analytics system that uses streams to answer common business questions: filtering by department, calculating salary statistics, grouping, partitioning, finding top performers, and generating a report.
import java.util.*;
import java.util.stream.Collectors;
public class EmployeeAnalytics {
// --- Employee class ---
static class Employee {
private final String name;
private final String department;
private final double salary;
private final int yearsOfExperience;
public Employee(String name, String department, double salary, int yearsOfExperience) {
this.name = name;
this.department = department;
this.salary = salary;
this.yearsOfExperience = yearsOfExperience;
}
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
public int getYearsOfExperience() { return yearsOfExperience; }
public boolean isSenior() { return yearsOfExperience >= 5; }
@Override
public String toString() {
return String.format("%-12s | %-12s | $%,10.2f | %2d yrs",
name, department, salary, yearsOfExperience);
}
}
public static void main(String[] args) {
// --- Sample data ---
List employees = List.of(
new Employee("Alice", "Engineering", 120000, 8),
new Employee("Bob", "Engineering", 95000, 3),
new Employee("Charlie", "Engineering", 110000, 6),
new Employee("Diana", "Marketing", 85000, 10),
new Employee("Eve", "Marketing", 72000, 2),
new Employee("Frank", "Sales", 78000, 5),
new Employee("Grace", "Sales", 82000, 7),
new Employee("Henry", "HR", 68000, 4),
new Employee("Ivy", "HR", 71000, 6),
new Employee("Jack", "Engineering", 135000, 12)
);
System.out.println("=== EMPLOYEE ANALYTICS REPORT ===\n");
// -------------------------------------------------------
// 1. FILTER: Engineers earning above $100K
// -------------------------------------------------------
System.out.println("--- 1. High-Earning Engineers (>$100K) ---");
List highEarningEngineers = employees.stream()
.filter(e -> e.getDepartment().equals("Engineering"))
.filter(e -> e.getSalary() > 100000)
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.collect(Collectors.toList());
highEarningEngineers.forEach(System.out::println);
// Output:
// Jack | Engineering | $135,000.00 | 12 yrs
// Alice | Engineering | $120,000.00 | 8 yrs
// Charlie | Engineering | $110,000.00 | 6 yrs
// -------------------------------------------------------
// 2. MAP + COLLECT: Average salary per department
// -------------------------------------------------------
System.out.println("\n--- 2. Average Salary by Department ---");
Map avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
avgSalaryByDept.entrySet().stream()
.sorted(Map.Entry.comparingByValue().reversed())
.forEach(e -> System.out.printf(" %-12s $%,.2f%n", e.getKey(), e.getValue()));
// Output:
// Engineering $115,000.00
// Sales $80,000.00
// Marketing $78,500.00
// HR $69,500.00
// -------------------------------------------------------
// 3. GROUPING: Employees grouped by department
// -------------------------------------------------------
System.out.println("\n--- 3. Employees by Department ---");
Map> namesByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(Employee::getName, Collectors.toList())
));
namesByDept.forEach((dept, names) ->
System.out.printf(" %-12s %s%n", dept, names));
// Output:
// Engineering [Alice, Bob, Charlie, Jack]
// Marketing [Diana, Eve]
// Sales [Frank, Grace]
// HR [Henry, Ivy]
// -------------------------------------------------------
// 4. REDUCE: Highest-paid employee
// -------------------------------------------------------
System.out.println("\n--- 4. Highest Paid Employee ---");
employees.stream()
.max(Comparator.comparingDouble(Employee::getSalary))
.ifPresent(e -> System.out.println(" " + e));
// Output:
// Jack | Engineering | $135,000.00 | 12 yrs
// -------------------------------------------------------
// 5. PARTITIONING: Senior vs Junior (5+ years = senior)
// -------------------------------------------------------
System.out.println("\n--- 5. Senior vs Junior ---");
Map> seniorPartition = employees.stream()
.collect(Collectors.partitioningBy(Employee::isSenior));
System.out.println(" Senior (" + seniorPartition.get(true).size() + "):");
seniorPartition.get(true).forEach(e -> System.out.println(" " + e.getName()));
System.out.println(" Junior (" + seniorPartition.get(false).size() + "):");
seniorPartition.get(false).forEach(e -> System.out.println(" " + e.getName()));
// Output:
// Senior (6):
// Alice, Charlie, Diana, Frank, Grace, Ivy
// Junior (4):
// Bob, Eve, Henry, Jack... wait, Jack has 12 years!
// Actually: Bob, Eve, Henry
// -------------------------------------------------------
// 6. STATISTICS: Salary summary
// -------------------------------------------------------
System.out.println("\n--- 6. Salary Statistics ---");
DoubleSummaryStatistics salaryStats = employees.stream()
.mapToDouble(Employee::getSalary)
.summaryStatistics();
System.out.printf(" Count: %d%n", salaryStats.getCount());
System.out.printf(" Total: $%,.2f%n", salaryStats.getSum());
System.out.printf(" Min: $%,.2f%n", salaryStats.getMin());
System.out.printf(" Max: $%,.2f%n", salaryStats.getMax());
System.out.printf(" Average: $%,.2f%n", salaryStats.getAverage());
// Output:
// Count: 10
// Total: $916,000.00
// Min: $68,000.00
// Max: $135,000.00
// Average: $91,600.00
// -------------------------------------------------------
// 7. TOP N: Top 3 highest salaries
// -------------------------------------------------------
System.out.println("\n--- 7. Top 3 Highest Salaries ---");
employees.stream()
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.limit(3)
.forEach(e -> System.out.println(" " + e));
// Output:
// Jack | Engineering | $135,000.00 | 12 yrs
// Alice | Engineering | $120,000.00 | 8 yrs
// Charlie | Engineering | $110,000.00 | 6 yrs
// -------------------------------------------------------
// 8. STRING JOINING: Department roster
// -------------------------------------------------------
System.out.println("\n--- 8. Department Roster ---");
Map rosters = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(
Employee::getName,
Collectors.joining(", ")
)
));
rosters.forEach((dept, roster) ->
System.out.printf(" %-12s %s%n", dept, roster));
// Output:
// Engineering Alice, Bob, Charlie, Jack
// Marketing Diana, Eve
// Sales Frank, Grace
// HR Henry, Ivy
// -------------------------------------------------------
// 9. COMPLEX: Department with highest average salary
// -------------------------------------------------------
System.out.println("\n--- 9. Highest-Paying Department ---");
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
))
.entrySet().stream()
.max(Map.Entry.comparingByValue())
.ifPresent(e -> System.out.printf(" %s with avg $%,.2f%n", e.getKey(), e.getValue()));
// Output:
// Engineering with avg $115,000.00
// -------------------------------------------------------
// 10. BOOLEAN CHECKS: Quick analytics
// -------------------------------------------------------
System.out.println("\n--- 10. Quick Checks ---");
boolean anyOver130K = employees.stream()
.anyMatch(e -> e.getSalary() > 130000);
System.out.println(" Anyone earning >$130K? " + anyOver130K); // true
boolean allAbove50K = employees.stream()
.allMatch(e -> e.getSalary() > 50000);
System.out.println(" All earning >$50K? " + allAbove50K); // true
long totalExperience = employees.stream()
.mapToInt(Employee::getYearsOfExperience)
.sum();
System.out.println(" Total years of experience: " + totalExperience); // 63
System.out.println("\n=== END OF REPORT ===");
}
}
| # | Concept | Where Used |
|---|---|---|
| 1 | filter() |
Section 1 -- filtering by department and salary |
| 2 | sorted() with Comparator |
Sections 1, 2, 7 -- sorting by salary |
| 3 | collect(Collectors.toList()) |
Sections 1, 3 -- gathering results |
| 4 | groupingBy() |
Sections 2, 3, 8, 9 -- grouping by department |
| 5 | averagingDouble() |
Sections 2, 9 -- average salary |
| 6 | mapping() downstream |
Sections 3, 8 -- extracting names within groups |
| 7 | max() with Comparator |
Section 4 -- highest-paid employee |
| 8 | partitioningBy() |
Section 5 -- senior vs junior split |
| 9 | summaryStatistics() |
Section 6 -- comprehensive salary stats |
| 10 | limit() |
Section 7 -- top 3 |
| 11 | Collectors.joining() |
Section 8 -- comma-separated roster |
| 12 | Chained stream operations | Section 9 -- collect then stream the result |
| 13 | anyMatch(), allMatch() |
Section 10 -- boolean checks |
| 14 | mapToInt() + sum() |
Section 10 -- total experience |
| Category | Operation | Type | Returns |
|---|---|---|---|
| Create | collection.stream() |
Source | Stream<T> |
| Create | Stream.of(a, b, c) |
Source | Stream<T> |
| Create | IntStream.rangeClosed(1, 10) |
Source | IntStream |
| Transform | filter(Predicate) |
Intermediate | Stream<T> |
| Transform | map(Function) |
Intermediate | Stream<R> |
| Transform | flatMap(Function) |
Intermediate | Stream<R> |
| Transform | sorted() |
Intermediate | Stream<T> |
| Transform | distinct() |
Intermediate | Stream<T> |
| Transform | limit(n) / skip(n) |
Intermediate | Stream<T> |
| Collect | collect(Collectors.toList()) |
Terminal | List<T> |
| Collect | collect(Collectors.toSet()) |
Terminal | Set<T> |
| Collect | collect(Collectors.toMap(...)) |
Terminal | Map<K,V> |
| Collect | collect(Collectors.groupingBy(...)) |
Terminal | Map<K,List<T>> |
| Collect | collect(Collectors.joining(...)) |
Terminal | String |
| Reduce | reduce(identity, BinaryOp) |
Terminal | T |
| Reduce | count() |
Terminal | long |
| Reduce | min(Comparator) / max(Comparator) |
Terminal | Optional<T> |
| Search | findFirst() / findAny() |
Terminal | Optional<T> |
| Match | anyMatch / allMatch / noneMatch |
Terminal | boolean |
| Action | forEach(Consumer) |
Terminal | void |