1. What is the Stream API?
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:
- Not a data structure — A Stream does not store elements. It pulls elements from a source (like a List or array) and pushes them through a pipeline of operations.
- Pipeline of operations — You chain multiple operations together. Each operation takes input, transforms it, and passes the result to the next operation.
- Lazy evaluation — Intermediate operations are not executed until a terminal operation is invoked. This allows the Stream to optimize the processing (e.g., short-circuiting).
- Does not modify the source — Streaming over a List does not add, remove, or change elements in that List. The original data remains untouched.
- Can only be consumed once — Once a terminal operation is called, the Stream is spent. To process the data again, you must create a new Stream.
- Supports parallelism — You can switch to parallel execution with a single method call, leveraging multi-core processors.
Stream Pipeline Structure
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<String> 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]
}
}2. Creating Streams
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.
2.1 From Collections
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<String> list = List.of("Java", "Python", "Go");
list.stream().forEach(System.out::println);
// From a Set
Set<Integer> 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<String, Integer> 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
}
}2.2 From Arrays
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<Integer>
int[] numbers = {10, 20, 30};
int sum = Arrays.stream(numbers).sum();
System.out.println("Sum: " + sum); // Output: Sum: 60
}
}2.3 Stream Factory Methods
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<String> empty = Stream.empty();
System.out.println("Empty count: " + empty.count()); // Output: Empty count: 0
// Stream.of() -- create from individual elements
Stream<String> 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
}
}2.4 Streams from Files
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<String> -- one element per line
// Use try-with-resources because the stream must be closed
try (Stream<String> 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());
}
}
}Stream Creation Summary
| 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 |
3. Intermediate Operations
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.
3.1 filter()
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<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Keep only even numbers
List<Integer> 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<Integer> 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<String> names = List.of("Alice", "Bob", "Charlie", "Ana", "Albert");
List<String> aNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println("A-names: " + aNames);
// Output: A-names: [Alice, Ana, Albert]
}
}3.2 map()
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<String> names = List.of("alice", "bob", "charlie");
// Transform: String -> String (uppercase)
List<String> upper = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upper);
// Output: [ALICE, BOB, CHARLIE]
// Transform: String -> Integer (get length)
List<Integer> lengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(lengths);
// Output: [5, 3, 7]
// Transform: Integer -> String
List<Integer> numbers = List.of(1, 2, 3);
List<String> labels = numbers.stream()
.map(n -> "Item #" + n)
.collect(Collectors.toList());
System.out.println(labels);
// Output: [Item #1, Item #2, Item #3]
}
}3.3 flatMap()
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<List<String>> nested = List.of(
List.of("Java", "Kotlin"),
List.of("Python", "Ruby"),
List.of("Go", "Rust")
);
// Using map() -- gives Stream<List<String>>, NOT what we want
// Using flatMap() -- gives Stream<String>, flattened!
List<String> 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<String> sentences = List.of("Hello World", "Java Streams are powerful");
List<String> 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())
}
}3.4 sorted()
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<Integer> numbers = List.of(5, 3, 8, 1, 9, 2);
List<Integer> sorted = numbers.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sorted);
// Output: [1, 2, 3, 5, 8, 9]
// Reverse order
List<Integer> 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<String> names = List.of("Charlie", "Bob", "Alice", "Eve");
List<String> 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<String> byLengthThenAlpha = names.stream()
.sorted(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()))
.collect(Collectors.toList());
System.out.println(byLengthThenAlpha);
// Output: [Bob, Eve, Alice, Charlie]
}
}3.5 distinct()
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<Integer> numbers = List.of(1, 2, 3, 2, 4, 3, 5, 1);
List<Integer> unique = numbers.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(unique);
// Output: [1, 2, 3, 4, 5]
// With strings (equals/hashCode already implemented)
List<String> words = List.of("hello", "world", "hello", "java", "world");
List<String> uniqueWords = words.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueWords);
// Output: [hello, world, java]
}
}3.6 peek()
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<String> 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]
}
}3.7 limit() and skip()
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<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// First 3 elements
List<Integer> 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<Integer> 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<Integer> 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]
}
}3.8 mapToInt(), mapToLong(), mapToDouble()
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<String> 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<Integer> 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
}
}Intermediate Operations Summary
| 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 |
4. Terminal Operations
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.
4.1 forEach()
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<String> 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)
}
}4.2 collect()
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<String> names = List.of("Alice", "Bob", "Charlie", "Alice", "David");
// Collect to List
List<String> 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<String> set = names.stream()
.collect(Collectors.toSet());
System.out.println("Set: " + set);
// Output: Set: [Bob, Alice, Charlie, David]
// Collect to unmodifiable List (Java 10+)
List<String> immutable = names.stream()
.collect(Collectors.toUnmodifiableList());
// Collect to Map (name -> length)
Map<String, Integer> 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]
}
}4.3 reduce()
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<Integer> 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<Integer> product = numbers.stream()
.reduce((a, b) -> a * b);
product.ifPresent(p -> System.out.println("Product: " + p));
// Output: Product: 120
// Finding the maximum
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
System.out.println("Max: " + max.orElse(0));
// Output: Max: 5
// String concatenation with reduce
List<String> 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
}
}4.4 count(), findFirst(), findAny()
import java.util.List;
import java.util.Optional;
public class CountFindExample {
public static void main(String[] args) {
List<String> 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<String> 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<String> 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)
}
}4.5 anyMatch(), allMatch(), noneMatch()
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<Integer> 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
}
}4.6 min(), max(), and toArray()
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
public class MinMaxToArrayExample {
public static void main(String[] args) {
List<String> names = List.of("Charlie", "Bob", "Alice", "David");
// min -- requires a Comparator
Optional<String> shortest = names.stream()
.min(Comparator.comparingInt(String::length));
System.out.println("Shortest: " + shortest.orElse("none"));
// Output: Shortest: Bob
// max
Optional<String> 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
}
}Terminal Operations Summary
| 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 |
5. Collectors In-Depth
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.
5.1 groupingBy()
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<String> names = List.of("Alice", "Bob", "Charlie", "Anna", "Ben", "Chris");
// Group by first letter
Map<Character, List<String>> 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<Integer, List<String>> 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<Character, Long> 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<Character, String> 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<Integer, Map<Character, List<String>>> 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]}}
}
}5.2 partitioningBy()
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<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Partition into even and odd
Map<Boolean, List<Integer>> 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<Boolean, Long> 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
}
}5.3 toMap() with Merge Function
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<String> words = List.of("hello", "world", "hello", "java", "world");
// Problem: duplicate keys without merge function throws exception
// Solution: provide a merge function
Map<String, Integer> 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<String, Integer> 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}
}
}5.4 summarizingInt() and Other Summarizers
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<String> 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
}
}6. Parallel Streams
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.
Creating Parallel Streams
import java.util.List;
import java.util.stream.IntStream;
public class ParallelStreamExample {
public static void main(String[] args) {
List<Integer> 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-*)
}
}When to Use (and Not Use) Parallel Streams
| 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<Integer> 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<Integer> safeList = IntStream.range(0, 1000)
.parallel()
.boxed()
.collect(Collectors.toList());
System.out.println("Safe size: " + safeList.size());
// Output: Safe size: 1000
}
}7. Optional with Streams
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<String> names = List.of("Alice", "Bob", "Charlie");
// findFirst returns Optional
Optional<String> 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<Integer> 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<Optional<String>> optionals = List.of(
Optional.of("Hello"),
Optional.empty(),
Optional.of("World")
);
List<String> values = optionals.stream()
.flatMap(Optional::stream)
.toList();
System.out.println(values);
// Output: [Hello, World]
}
}8. Primitive Streams
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<Integer>
List<Integer> 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<String> 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<T> and primitive streams
List<String> names = List.of("Alice", "Bob", "Charlie");
IntStream lengths = names.stream().mapToInt(String::length);
System.out.println("Total chars: " + lengths.sum());
// Output: Total chars: 15
}
}9. Common Patterns
This section demonstrates practical, real-world patterns you will use repeatedly in production code. These patterns solve common data-processing problems elegantly with streams.
9.1 Filtering and Collecting
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<Product> 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<Product> 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<String> 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<String, Double> 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<String, Double> priceMap = products.stream()
.collect(Collectors.toMap(Product::name, Product::price));
System.out.println("Laptop price: $" + priceMap.get("Laptop"));
// Output: Laptop price: $999.99
}
}9.2 Flattening Nested Collections
import java.util.*;
import java.util.stream.Collectors;
public class FlatteningPattern {
record Student(String name, List<String> courses) {}
public static void main(String[] args) {
List<Student> 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<String> 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<String> 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]
}
}10. Stream vs Loop
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 |
The Same Problem: Both Ways
import java.util.*;
import java.util.stream.Collectors;
public class StreamVsLoop {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie", "David", "Eve", "Frank");
// Task: Get uppercase names that are longer than 3 characters
// --- Loop approach ---
List<String> resultLoop = new ArrayList<>();
for (String name : names) {
if (name.length() > 3) {
resultLoop.add(name.toUpperCase());
}
}
System.out.println("Loop: " + resultLoop);
// --- Stream approach ---
List<String> 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).
11. Common Mistakes
These are mistakes that even experienced developers make when working with the Stream API. Understanding them will save you hours of debugging.
Mistake 1: Reusing a Stream
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<String> names = List.of("Alice", "Bob", "Charlie");
Stream<String> 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);
}
}Mistake 2: Side Effects in Intermediate Operations
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<String> names = List.of("Alice", "Bob", "Charlie");
// WRONG: modifying external state inside map()
List<String> 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<String> upper = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
}
}Mistake 3: Infinite Streams Without limit()
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);
}
}Mistake 4: Modifying the Source During Streaming
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class ModifySourceMistake {
public static void main(String[] args) {
List<String> 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<String> 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"));
}
}Mistake 5: Performance Traps
import java.util.List;
import java.util.stream.Collectors;
public class PerformanceTrapMistake {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// SLOW: unnecessary boxing -- Stream<Integer> 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
}
}Common Mistakes Summary
| 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 |
12. Best Practices
Following these best practices will help you write stream code that is clean, efficient, and maintainable.
1. Keep Operations Simple
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<Employee> employees = List.of(
new Employee("Alice", 30, 85000, "Engineering"),
new Employee("Bob", 25, 45000, "Marketing")
);
List<Employee> seniors = employees.stream()
.filter(BestPractices::isSeniorEngineer)
.collect(Collectors.toList());
}
}2. Use Method References When Possible
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<String> names = List.of("alice", "bob", "charlie");
// Lambda (works but verbose)
List<String> upper1 = names.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
// Method reference (cleaner)
List<String> 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
}
}3. Format Multi-Line Streams for Readability
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<Employee> employees = List.of(
new Employee("Alice", "Engineering", 95000),
new Employee("Bob", "Marketing", 65000),
new Employee("Charlie", "Engineering", 85000)
);
// POOR: all on one line
// List<String> 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<String> result = employees.stream()
.filter(e -> e.dept().equals("Engineering"))
.map(Employee::name)
.sorted()
.collect(Collectors.toList());
System.out.println(result);
// Output: [Alice, Charlie]
}
}Best Practices Summary
| # | 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 |
13. Complete Practical Example: Employee Analytics System
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<Employee> 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<Employee> 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<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
avgSalaryByDept.entrySet().stream()
.sorted(Map.Entry.<String, Double>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<String, List<String>> 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<Boolean, List<Employee>> 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<String, String> 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 ===");
}
}Concepts Demonstrated in the Practical Example
| # | 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 |
Quick Reference
| 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 |