Think of a conveyor belt in a sushi restaurant. Plates of sushi travel along the belt, and you — the customer — pick only the ones you want, maybe add some soy sauce, and eat them. You never rearrange the kitchen. You never touch the plates you skip. You simply describe what you want and let the system deliver it.
The Java Stream API, introduced in Java 8 as part of the java.util.stream package, brings this declarative processing model to collections and other data sources. Instead of writing explicit loops that tell the computer how to iterate, you write a pipeline that describes what transformations and filters to apply.
Key characteristics of Streams:
filter, map, and reduce to express complex data processing in a readable, composable way.parallelStream()), leveraging multiple CPU cores automatically.Every Stream pipeline has exactly three parts:
| Part | Description | Examples |
|---|---|---|
| Source | Where the data originates | list.stream(), Arrays.stream(arr), Stream.of() |
| Intermediate Operations | Transform the stream (lazy, return a new Stream) | filter(), map(), sorted(), distinct() |
| Terminal Operation | Produces a result or side-effect (triggers execution) | collect(), forEach(), count(), reduce() |
Here is a visual representation of how a Stream pipeline works:
Source Intermediate Ops Terminal Op
[data] ----> filter() ----> map() ----> collect()
| | |
(lazy) (lazy) (triggers all)
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
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
List result = 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
.sorted() // Intermediate: sort alphabetically
.collect(Collectors.toList()); // Terminal: collect into a new list
System.out.println("Result: " + result);
// Output: Result: [ALICE, CHARLIE, DAVID]
// The original list is unchanged
System.out.println("Original: " + names);
// Output: Original: [Alice, Bob, Charlie, David, Eve]
// Streams are single-use -- this will throw IllegalStateException
var stream = names.stream();
stream.count(); // terminal operation consumes the stream
// stream.forEach(System.out::println); // ERROR: stream has already been operated upon
}
}
Before you can process data with the Stream API, you need to create a Stream. Java provides multiple ways depending on the data source.
The most common way. Every class implementing Collection (List, Set, Queue) inherits a stream() method.
import java.util.*;
import java.util.stream.Collectors;
public class StreamFromCollections {
public static void main(String[] args) {
// From a List
List languages = List.of("Java", "Python", "Go", "Rust");
List filtered = languages.stream()
.filter(lang -> lang.length() > 2)
.collect(Collectors.toList());
System.out.println("Filtered: " + filtered);
// Output: Filtered: [Java, Python, Rust]
// From a Set
Set numbers = new LinkedHashSet<>(Set.of(5, 3, 1, 4, 2));
int sum = numbers.stream()
.filter(n -> n % 2 != 0)
.mapToInt(Integer::intValue)
.sum();
System.out.println("Sum of odds: " + sum);
// From a Map (via entrySet, keySet, or values)
Map scores = Map.of("Alice", 92, "Bob", 78, "Charlie", 95);
scores.entrySet().stream()
.filter(e -> e.getValue() >= 90)
.forEach(e -> System.out.println(e.getKey() + " scored " + e.getValue()));
// Output: Alice scored 92
// Charlie scored 95
}
}
Use Arrays.stream() for arrays. For primitive arrays (int[], long[], double[]), it returns the corresponding primitive stream (IntStream, LongStream, DoubleStream).
import java.util.Arrays;
import java.util.stream.Stream;
public class StreamFromArrays {
public static void main(String[] args) {
String[] colors = {"Red", "Green", "Blue", "Yellow"};
// Full array stream
Arrays.stream(colors).forEach(System.out::println);
// Partial array: from index 1 (inclusive) to 3 (exclusive)
String[] partial = Arrays.stream(colors, 1, 3)
.toArray(String[]::new);
System.out.println(Arrays.toString(partial));
// Output: [Green, Blue]
// Stream.of() works with varargs
Stream.of("One", "Two", "Three").forEach(System.out::println);
// Primitive array -- returns IntStream, not Stream
int[] numbers = {10, 20, 30, 40};
int sum = Arrays.stream(numbers).sum();
System.out.println("Sum: " + sum); // Output: Sum: 100
double avg = Arrays.stream(numbers).average().orElse(0.0);
System.out.println("Average: " + avg); // Output: Average: 25.0
}
}
Java provides static factory methods on the Stream interface to create streams without needing an existing collection or array.
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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: 0
// Stream.of() -- create from individual elements
Stream.of("Java", "Python", "Go")
.map(String::toUpperCase)
.forEach(System.out::println);
// Stream.generate() -- infinite stream from a Supplier
// ALWAYS use limit() or the stream runs forever
List uuids = Stream.generate(UUID::randomUUID)
.limit(3)
.map(UUID::toString)
.collect(Collectors.toList());
System.out.println("UUIDs: " + uuids);
// Stream.iterate() -- Java 8 style (seed + UnaryOperator, must limit)
List powersOf2 = Stream.iterate(1, n -> n * 2)
.limit(8)
.collect(Collectors.toList());
System.out.println("Powers: " + powersOf2);
// Output: Powers: [1, 2, 4, 8, 16, 32, 64, 128]
// Stream.iterate() -- Java 9+ style (seed + predicate + UnaryOperator)
Stream.iterate(1, n -> n <= 100, n -> n * 2)
.forEach(n -> System.out.print(n + " "));
// Output: 1 2 4 8 16 32 64
System.out.println();
// Stream.concat() -- combine two streams into one
Stream s1 = Stream.of("A", "B");
Stream s2 = Stream.of("C", "D");
Stream.concat(s1, s2).forEach(System.out::println);
// Output: A, B, C, D
}
}
For generating sequences of integers, IntStream provides range() (exclusive end) and rangeClosed() (inclusive end). These are more efficient than Stream.iterate() for simple numeric ranges.
import java.util.stream.IntStream;
import java.util.stream.LongStream;
public class StreamRanges {
public static void main(String[] args) {
// range(start, endExclusive) -- does NOT include the end value
IntStream.range(1, 5).forEach(n -> System.out.print(n + " "));
// Output: 1 2 3 4
System.out.println();
// rangeClosed(start, endInclusive) -- INCLUDES the end value
IntStream.rangeClosed(1, 5).forEach(n -> System.out.print(n + " "));
// Output: 1 2 3 4 5
System.out.println();
// Sum of 1 to 100
int sum = IntStream.rangeClosed(1, 100).sum();
System.out.println("Sum 1-100: " + sum); // Output: Sum 1-100: 5050
// LongStream for larger ranges
long bigSum = LongStream.rangeClosed(1, 1_000_000).sum();
System.out.println("Sum 1-1M: " + bigSum);
// Output: Sum 1-1M: 500000500000
// Using range as a loop replacement
IntStream.range(0, 5)
.mapToObj(i -> "Item " + i)
.forEach(System.out::println);
}
}
| Method | Returns | Use Case |
|---|---|---|
collection.stream() |
Stream<T> |
Most common — stream from any Collection |
Arrays.stream(array) |
Stream<T> or primitive stream |
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 iteration |
Stream.concat(s1, s2) |
Stream<T> |
Merge two streams |
IntStream.range(a, b) |
IntStream |
Range of ints [a, b) |
IntStream.rangeClosed(a, b) |
IntStream |
Range of ints [a, b] |
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. Each one returns a new Stream, leaving the original Stream unchanged.
filter() keeps only the elements that match the given condition. Elements that pass the test continue down the pipeline; those that fail 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 AND)
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]
// Filtering with complex predicates
List names = List.of("Alice", "Bob", "Charlie", "Anna", "Andrew");
List startsWithA = names.stream()
.filter(name -> name.startsWith("A"))
.filter(name -> name.length() > 4)
.collect(Collectors.toList());
System.out.println("Starts with A, length > 4: " + startsWithA);
// Output: Starts with A, length > 4: [Alice, Andrew]
}
}
map() transforms each element by applying a function. The input type and output type can be different. This is one of the most commonly used operations.
import java.util.List;
import java.util.stream.Collectors;
public class MapExample {
public static void main(String[] args) {
// Transform strings to uppercase
List names = List.of("alice", "bob", "charlie");
List upper = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("Upper: " + upper);
// Output: Upper: [ALICE, BOB, CHARLIE]
// Transform to different type: String -> Integer (length)
List lengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println("Lengths: " + lengths);
// Output: Lengths: [5, 3, 7]
// Chain map operations
List formatted = names.stream()
.map(String::trim)
.map(s -> s.substring(0, 1).toUpperCase() + s.substring(1))
.map(s -> "Hello, " + s + "!")
.collect(Collectors.toList());
System.out.println("Formatted: " + formatted);
// Output: Formatted: [Hello, Alice!, Hello, Bob!, Hello, Charlie!]
// Extract a field from objects
List emails = List.of("alice@mail.com", "bob@mail.com", "charlie@mail.com");
List usernames = emails.stream()
.map(email -> email.split("@")[0])
.collect(Collectors.toList());
System.out.println("Usernames: " + usernames);
// Output: Usernames: [alice, bob, charlie]
}
}
flatMap() is used when each element maps to multiple elements (a stream of streams). It flattens the nested streams into a single stream. This is essential when dealing with lists of lists, optional values, or one-to-many transformations.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class FlatMapExample {
public static void main(String[] args) {
// Problem: List of lists -- map gives Stream>
List> nested = List.of(
List.of("Java", "Python"),
List.of("Go", "Rust"),
List.of("JavaScript", "TypeScript")
);
// map() would give us Stream> -- not what we want
// flatMap() flattens it into Stream
List allLanguages = nested.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
System.out.println("All: " + allLanguages);
// Output: All: [Java, Python, Go, Rust, JavaScript, TypeScript]
// Split sentences into words
List sentences = List.of(
"Java is great",
"Streams are powerful",
"FlatMap flattens"
);
List words = sentences.stream()
.flatMap(sentence -> Arrays.stream(sentence.split(" ")))
.map(String::toLowerCase)
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println("Words: " + words);
// Output: Words: [are, flatmap, flattens, great, is, java, powerful, streams]
// flatMap with arrays
String[][] data = {{"a", "b"}, {"c", "d"}, {"e", "f"}};
List flat = Arrays.stream(data)
.flatMap(Arrays::stream)
.collect(Collectors.toList());
System.out.println("Flat: " + flat);
// Output: Flat: [a, b, c, d, e, f]
}
}
sorted() sorts the elements. Without arguments, it uses natural ordering (elements must implement Comparable). With a Comparator argument, you control the sort order.
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class SortedExample {
public static void main(String[] args) {
List names = List.of("Charlie", "Alice", "Eve", "Bob", "David");
// Natural ordering (alphabetical for Strings)
List sorted = names.stream()
.sorted()
.collect(Collectors.toList());
System.out.println("Sorted: " + sorted);
// Output: Sorted: [Alice, Bob, Charlie, David, Eve]
// Reverse order
List reversed = names.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
System.out.println("Reversed: " + reversed);
// Output: Reversed: [Eve, David, Charlie, Bob, Alice]
// Sort by length
List byLength = names.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList());
System.out.println("By length: " + byLength);
// Output: By length: [Bob, Eve, Alice, David, Charlie]
// Sort by length descending, then alphabetically
List complex = names.stream()
.sorted(Comparator.comparingInt(String::length)
.reversed()
.thenComparing(Comparator.naturalOrder()))
.collect(Collectors.toList());
System.out.println("Complex sort: " + complex);
// Output: Complex sort: [Charlie, Alice, David, Bob, Eve]
}
}
These operations help you control which elements pass through the pipeline and inspect elements during processing.
| Operation | Description |
|---|---|
distinct() |
Removes duplicates (uses equals()) |
peek(Consumer) |
Performs an action on each element without consuming it — useful for debugging |
limit(n) |
Truncates the stream to at most n elements |
skip(n) |
Discards the first n elements |
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class MoreIntermediateOps {
public static void main(String[] args) {
// distinct() -- removes duplicates
List numbers = List.of(1, 2, 3, 2, 1, 4, 3, 5);
List unique = numbers.stream()
.distinct()
.collect(Collectors.toList());
System.out.println("Unique: " + unique);
// Output: Unique: [1, 2, 3, 4, 5]
// peek() -- inspect elements without consuming (great for debugging)
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) -- take only the first n elements
List firstThree = List.of(10, 20, 30, 40, 50).stream()
.limit(3)
.collect(Collectors.toList());
System.out.println("First 3: " + firstThree);
// Output: First 3: [10, 20, 30]
// skip(n) -- skip the first n elements
List skipTwo = List.of(10, 20, 30, 40, 50).stream()
.skip(2)
.collect(Collectors.toList());
System.out.println("Skip 2: " + skipTwo);
// Output: Skip 2: [30, 40, 50]
// Pagination: skip + limit
List allItems = List.of("A", "B", "C", "D", "E", "F", "G", "H");
int pageSize = 3;
int pageNumber = 2; // 0-based
List page = allItems.stream()
.skip((long) pageNumber * pageSize)
.limit(pageSize)
.collect(Collectors.toList());
System.out.println("Page 2: " + page);
// Output: Page 2: [G, H]
}
}
| Operation | Signature | Description |
|---|---|---|
filter |
filter(Predicate<T>) |
Keep elements matching the predicate |
map |
map(Function<T, R>) |
Transform each element |
flatMap |
flatMap(Function<T, Stream<R>>) |
Transform and flatten nested streams |
sorted |
sorted() or sorted(Comparator) |
Sort elements |
distinct |
distinct() |
Remove duplicates |
peek |
peek(Consumer<T>) |
Perform action without consuming |
limit |
limit(long n) |
Truncate to first n elements |
skip |
skip(long n) |
Discard first n elements |
Terminal operations trigger the execution of the entire pipeline 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() performs an action on each element. For parallel streams, use forEachOrdered() if order matters.
import java.util.List;
public class ForEachExample {
public static void main(String[] args) {
List fruits = List.of("Apple", "Banana", "Cherry");
// forEach -- order guaranteed for sequential streams
fruits.stream().forEach(f -> System.out.println("Fruit: " + f));
// Method reference shorthand
fruits.stream().forEach(System.out::println);
// forEachOrdered -- guarantees encounter order even in parallel
fruits.parallelStream().forEachOrdered(System.out::println);
// Output (always in order): Apple, Banana, Cherry
}
}
collect() is the most versatile terminal operation. It uses a Collector to accumulate elements into a result container (List, Set, Map, String, etc.). The Collectors utility class provides dozens of pre-built 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);
// Collect to Map
Map nameLengths = names.stream()
.distinct()
.collect(Collectors.toMap(
name -> name, // key
String::length // value
));
System.out.println("Map: " + nameLengths);
// Output: Map: {Alice=5, Bob=3, Charlie=7, David=5}
// Join strings
String joined = names.stream()
.distinct()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println("Joined: " + joined);
// Output: Joined: [Alice, Bob, Charlie, David]
}
}
reduce() combines all elements into a single result by repeatedly applying a binary operator. It has three forms: with identity, without identity, and with identity + combiner (for parallel streams).
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);
// reduce with identity -- always returns a value
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println("Sum: " + sum); // Output: Sum: 15
int product = numbers.stream()
.reduce(1, (a, b) -> a * b);
System.out.println("Product: " + product); // Output: Product: 120
// reduce without identity -- returns Optional (stream could be empty)
Optional max = numbers.stream()
.reduce(Integer::max);
max.ifPresent(m -> System.out.println("Max: " + m)); // Output: Max: 5
// Concatenating strings with reduce
List words = List.of("Java", "Streams", "Are", "Powerful");
String sentence = words.stream()
.reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b);
System.out.println("Sentence: " + sentence);
// Output: Sentence: Java Streams Are Powerful
// reduce on empty stream
Optional emptyResult = List.of().stream()
.reduce(Integer::sum);
System.out.println("Empty: " + emptyResult.isPresent()); // Output: Empty: false
}
}
These terminal operations return aggregate values from the stream.
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
public class CountMinMaxExample {
public static void main(String[] args) {
List numbers = List.of(5, 3, 8, 1, 9, 2, 7);
// count()
long count = numbers.stream()
.filter(n -> n > 5)
.count();
System.out.println("Count > 5: " + count); // Output: Count > 5: 3
// min() with Comparator
Optional min = numbers.stream()
.min(Comparator.naturalOrder());
System.out.println("Min: " + min.orElse(-1)); // Output: Min: 1
// max() with Comparator
Optional max = numbers.stream()
.max(Comparator.naturalOrder());
System.out.println("Max: " + max.orElse(-1)); // Output: Max: 9
// Finding the longest string
List words = List.of("cat", "elephant", "dog", "hippopotamus");
Optional longest = words.stream()
.max(Comparator.comparingInt(String::length));
System.out.println("Longest: " + longest.orElse("none"));
// Output: Longest: hippopotamus
}
}
findFirst() returns the first element. findAny() returns any element (useful with parallel streams for better performance). Both return Optional.
import java.util.List;
import java.util.Optional;
public class FindExample {
public static void main(String[] args) {
List names = List.of("Charlie", "Alice", "Bob", "Andrew", "Anna");
// findFirst() -- guaranteed to return the first matching element
Optional first = names.stream()
.filter(n -> n.startsWith("A"))
.findFirst();
System.out.println("First with A: " + first.orElse("none"));
// Output: First with A: Alice
// findAny() -- may return any matching element (non-deterministic in parallel)
Optional any = names.parallelStream()
.filter(n -> n.startsWith("A"))
.findAny();
System.out.println("Any with A: " + any.orElse("none"));
// Output: Any with A: Alice (or Andrew or Anna -- non-deterministic)
// findFirst on empty result
Optional missing = names.stream()
.filter(n -> n.startsWith("Z"))
.findFirst();
System.out.println("Found Z? " + missing.isPresent());
// Output: Found Z? false
}
}
These short-circuiting operations test whether elements match a condition. They return boolean and 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 -- does at least one element match?
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);
System.out.println("Has even? " + hasEven); // Output: Has even? true
// allMatch -- do ALL elements match?
boolean allEven = numbers.stream().allMatch(n -> n % 2 == 0);
System.out.println("All even? " + allEven); // Output: All even? true
// noneMatch -- does NO element match?
boolean noneNegative = numbers.stream().noneMatch(n -> n < 0);
System.out.println("None negative? " + noneNegative); // Output: None negative? true
// Short-circuiting behavior
List mixed = List.of(1, 2, 3, 4, 5);
boolean anyOver3 = mixed.stream()
.peek(n -> System.out.print("Checking " + n + "... "))
.anyMatch(n -> n > 3);
System.out.println("\nAny over 3? " + anyOver3);
// Output: Checking 1... Checking 2... Checking 3... Checking 4...
// Any over 3? true
// Note: 5 was never checked -- short-circuited after finding 4
}
}
toArray() collects the stream elements into an array. Without arguments it returns Object[]; with a generator function it returns the specific type.
import java.util.Arrays;
import java.util.List;
public class ToArrayExample {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie");
// toArray() without argument -- returns Object[]
Object[] objArray = names.stream().toArray();
System.out.println(Arrays.toString(objArray));
// toArray with generator -- returns String[]
String[] strArray = names.stream()
.map(String::toUpperCase)
.toArray(String[]::new);
System.out.println(Arrays.toString(strArray));
// Output: [ALICE, BOB, CHARLIE]
// Convert filtered stream to int array
int[] evenNumbers = List.of(1, 2, 3, 4, 5, 6).stream()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.toArray();
System.out.println(Arrays.toString(evenNumbers));
// Output: [2, 4, 6]
}
}
| Operation | Return Type | Description |
|---|---|---|
forEach(Consumer) |
void |
Perform action on each element |
collect(Collector) |
R |
Accumulate into a container (List, Set, Map, etc.) |
reduce(identity, op) |
T |
Combine all elements into one value |
count() |
long |
Count elements |
min(Comparator) |
Optional<T> |
Find minimum element |
max(Comparator) |
Optional<T> |
Find maximum element |
findFirst() |
Optional<T> |
First element (deterministic) |
findAny() |
Optional<T> |
Any element (non-deterministic in parallel) |
anyMatch(Predicate) |
boolean |
True if any element matches |
allMatch(Predicate) |
boolean |
True if all elements match |
noneMatch(Predicate) |
boolean |
True if no element matches |
toArray() |
Object[] or T[] |
Collect to an array |
The Collectors class in java.util.stream provides a rich set of pre-built collectors for the collect() terminal operation. This section covers the most commonly used ones. For a deep dive into all Collectors, see the dedicated Java 8 Collectors Class tutorial.
import java.util.*;
import java.util.stream.Collectors;
public class BasicCollectors {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie", "Alice", "David");
// toList() -- ordered, allows duplicates
List list = names.stream()
.filter(n -> n.length() > 3)
.collect(Collectors.toList());
System.out.println("List: " + list);
// Output: List: [Alice, Charlie, Alice, David]
// toSet() -- unordered, no duplicates
Set set = names.stream()
.collect(Collectors.toSet());
System.out.println("Set: " + set);
// toMap() -- key-value pairs
Map nameLengths = names.stream()
.distinct()
.collect(Collectors.toMap(
n -> n, // key mapper
String::length // value mapper
));
System.out.println("Map: " + nameLengths);
// toMap() with merge function for duplicate keys
Map nameCount = names.stream()
.collect(Collectors.toMap(
n -> n, // key
n -> 1, // value
Integer::sum // merge: add counts
));
System.out.println("Counts: " + nameCount);
// Output: Counts: {Alice=2, Bob=1, Charlie=1, David=1}
}
}
Collectors.joining() concatenates stream elements into a single String. You can specify a delimiter, prefix, and suffix.
import java.util.List;
import java.util.stream.Collectors;
public class JoiningCollector {
public static void main(String[] args) {
List languages = List.of("Java", "Python", "Go", "Rust");
// Simple concatenation
String simple = languages.stream().collect(Collectors.joining());
System.out.println(simple); // Output: JavaPythonGoRust
// With delimiter
String delimited = languages.stream().collect(Collectors.joining(", "));
System.out.println(delimited); // Output: Java, Python, Go, Rust
// With delimiter, prefix, and suffix
String formatted = languages.stream()
.collect(Collectors.joining(" | ", "[ ", " ]"));
System.out.println(formatted); // Output: [ Java | Python | Go | Rust ]
// Practical: building a SQL IN clause
List ids = List.of(101, 205, 310, 422);
String inClause = ids.stream()
.map(String::valueOf)
.collect(Collectors.joining(", ", "WHERE id IN (", ")"));
System.out.println(inClause);
// Output: WHERE id IN (101, 205, 310, 422)
}
}
groupingBy() groups elements by a classifier function into a Map<K, List<T>>. partitioningBy() is a special case that splits elements into two groups (true/false) based on a predicate.
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GroupingExample {
public static void main(String[] args) {
List names = List.of("Alice", "Anna", "Bob", "Brian", "Charlie", "Chris");
// Group by first letter
Map> byLetter = names.stream()
.collect(Collectors.groupingBy(n -> n.charAt(0)));
System.out.println("By letter: " + byLetter);
// Output: By letter: {A=[Alice, Anna], B=[Bob, Brian], C=[Charlie, Chris]}
// Group by length with counting
Map byLength = names.stream()
.collect(Collectors.groupingBy(String::length, Collectors.counting()));
System.out.println("By length: " + byLength);
// Output: By length: {3=1, 4=1, 5=3, 7=1}
// partitioningBy -- split into two groups
Map> partitioned = names.stream()
.collect(Collectors.partitioningBy(n -> n.length() > 4));
System.out.println("Long names: " + partitioned.get(true));
System.out.println("Short names: " + partitioned.get(false));
// Output: Long names: [Alice, Brian, Charlie, Chris]
// Short names: [Anna, Bob]
}
}
Parallel streams divide the workload across multiple threads using the common ForkJoinPool. They can significantly speed up processing of large data sets, but they are not always faster and come with important caveats.
| Use Parallel When… | Avoid Parallel When… |
|---|---|
| Large data sets (100,000+ elements) | Small data sets (overhead exceeds benefit) |
| CPU-bound operations (computation) | I/O-bound operations (network, file reads) |
| No shared mutable state | Operations modify shared variables |
| Operations are independent and stateless | Order-dependent processing |
| Source supports efficient splitting (ArrayList, arrays) | Poor-splitting sources (LinkedList, Stream.iterate) |
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class ParallelStreamExample {
public static void main(String[] args) {
// Creating a parallel stream
List numbers = IntStream.rangeClosed(1, 1_000_000)
.boxed()
.collect(Collectors.toList());
// Sequential sum
long startSeq = System.nanoTime();
long sumSeq = numbers.stream()
.mapToLong(Integer::longValue)
.sum();
long timeSeq = System.nanoTime() - startSeq;
// Parallel sum
long startPar = System.nanoTime();
long sumPar = numbers.parallelStream()
.mapToLong(Integer::longValue)
.sum();
long timePar = System.nanoTime() - startPar;
System.out.println("Sequential sum: " + sumSeq + " in " + timeSeq / 1_000_000 + "ms");
System.out.println("Parallel sum: " + sumPar + " in " + timePar / 1_000_000 + "ms");
// Converting between sequential and parallel
List names = List.of("Alice", "Bob", "Charlie");
names.stream()
.parallel() // switch to parallel
.sequential() // switch back to sequential
.forEach(System.out::println);
}
}
Parallel streams use multiple threads. If your operations modify shared mutable state, you will get race conditions and incorrect results. Here is an example of what NOT to do:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class ParallelSafetyExample {
public static void main(String[] args) {
// WRONG: Modifying a shared list from parallel stream
List unsafeList = new ArrayList<>();
IntStream.rangeClosed(1, 10000)
.parallel()
.forEach(unsafeList::add); // Race condition!
System.out.println("Unsafe size: " + unsafeList.size());
// Output: unpredictable -- could be less than 10000, could throw exception
// CORRECT: Use collect() instead of modifying shared state
List safeList = IntStream.rangeClosed(1, 10000)
.parallel()
.boxed()
.collect(Collectors.toList());
System.out.println("Safe size: " + safeList.size());
// Output: Safe size: 10000
// CORRECT: Use a synchronized collection if you must
List syncList = Collections.synchronizedList(new ArrayList<>());
IntStream.rangeClosed(1, 10000)
.parallel()
.forEach(syncList::add);
System.out.println("Sync size: " + syncList.size());
// Output: Sync size: 10000
}
}
Java generics do not work with primitives, so Stream<int> is not valid. To avoid the performance cost of boxing/unboxing (wrapping primitives in Integer, Long, Double), Java provides three specialized stream types:
| Stream Type | Element Type | Key Methods |
|---|---|---|
IntStream |
int |
sum(), average(), min(), max(), range() |
LongStream |
long |
sum(), average(), min(), max(), range() |
DoubleStream |
double |
sum(), average(), min(), max() |
| From | To | Method |
|---|---|---|
Stream<T> |
IntStream |
mapToInt(ToIntFunction) |
Stream<T> |
LongStream |
mapToLong(ToLongFunction) |
Stream<T> |
DoubleStream |
mapToDouble(ToDoubleFunction) |
IntStream |
Stream<Integer> |
boxed() |
IntStream |
LongStream |
asLongStream() |
IntStream |
DoubleStream |
asDoubleStream() |
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.stream.IntStream;
public class PrimitiveStreamExample {
public static void main(String[] args) {
// IntStream basics
int sum = IntStream.of(10, 20, 30, 40, 50).sum();
System.out.println("Sum: " + sum); // Output: Sum: 150
OptionalDouble avg = IntStream.of(10, 20, 30, 40, 50).average();
System.out.println("Average: " + avg.orElse(0.0)); // Output: Average: 30.0
OptionalInt min = IntStream.of(10, 20, 30, 40, 50).min();
System.out.println("Min: " + min.orElse(-1)); // Output: Min: 10
OptionalInt max = IntStream.of(10, 20, 30, 40, 50).max();
System.out.println("Max: " + max.orElse(-1)); // Output: Max: 50
// summaryStatistics -- get all stats in one pass
IntSummaryStatistics stats = IntStream.of(10, 20, 30, 40, 50)
.summaryStatistics();
System.out.println("Count: " + stats.getCount()); // 5
System.out.println("Sum: " + stats.getSum()); // 150
System.out.println("Min: " + stats.getMin()); // 10
System.out.println("Max: " + stats.getMax()); // 50
System.out.println("Avg: " + stats.getAverage()); // 30.0
// mapToInt -- convert Stream to IntStream
List words = List.of("Java", "is", "powerful");
int totalChars = words.stream()
.mapToInt(String::length)
.sum();
System.out.println("Total chars: " + totalChars); // Output: Total chars: 15
// boxed() -- convert IntStream back to Stream
List boxed = IntStream.rangeClosed(1, 5)
.boxed()
.toList();
System.out.println("Boxed: " + boxed); // Output: Boxed: [1, 2, 3, 4, 5]
}
}
Streams are a powerful tool, but they can be misused. Following these best practices will keep your stream pipelines readable, correct, and performant.
Each operation in the pipeline should do one thing. If a lambda is more than 2-3 lines, extract it into a method and use a method reference.
import java.util.List;
import java.util.stream.Collectors;
public class BestPracticeSimple {
public static void main(String[] args) {
List emails = List.of(
"alice@GMAIL.com", " bob@yahoo.com ", "CHARLIE@gmail.com", "invalid-email"
);
// BAD: Complex logic crammed into one lambda
List bad = emails.stream()
.filter(e -> {
String trimmed = e.trim().toLowerCase();
return trimmed.contains("@") && trimmed.endsWith(".com");
})
.map(e -> e.trim().toLowerCase())
.collect(Collectors.toList());
// GOOD: Extract complex logic into named methods
List good = emails.stream()
.map(String::trim)
.map(String::toLowerCase)
.filter(BestPracticeSimple::isValidEmail)
.collect(Collectors.toList());
System.out.println("Valid emails: " + good);
// Output: Valid emails: [alice@gmail.com, bob@yahoo.com, charlie@gmail.com]
}
private static boolean isValidEmail(String email) {
return email.contains("@") && email.endsWith(".com");
}
}
Stream operations should be pure functions — they should not modify external state. The exception is forEach(), which exists specifically for side effects.
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class BestPracticeNoSideEffects {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie");
// BAD: Side effect in map() -- modifying an external list
List external = new ArrayList<>();
names.stream()
.map(n -> {
external.add(n); // Side effect -- do not do this!
return n.toUpperCase();
})
.collect(Collectors.toList());
// GOOD: Use collect() to build the result
List result = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("Result: " + result);
// Output: Result: [ALICE, BOB, CHARLIE]
}
}
A Stream can only be consumed once. Attempting to reuse it throws IllegalStateException. If you need to process the same data multiple times, create a new Stream each time or store the source data.
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class BestPracticeNoReuse {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie");
// BAD: Reusing a stream
Stream stream = names.stream().filter(n -> n.length() > 3);
stream.forEach(System.out::println); // works
// stream.count(); // IllegalStateException: stream has already been operated upon
// GOOD: Create a new stream each time
long count = names.stream().filter(n -> n.length() > 3).count();
List filtered = names.stream()
.filter(n -> n.length() > 3)
.collect(Collectors.toList());
// ALTERNATIVE: Use a Supplier for repeated use
Supplier> streamSupplier = () ->
names.stream().filter(n -> n.length() > 3);
long count2 = streamSupplier.get().count();
List list2 = streamSupplier.get().collect(Collectors.toList());
System.out.println("Count: " + count2 + ", List: " + list2);
}
}
The order of intermediate operations can significantly affect performance. Filter early to reduce the number of elements processed by expensive operations like sorted() or map().
import java.util.List;
import java.util.stream.Collectors;
public class BestPracticeOrder {
public static void main(String[] args) {
List names = List.of("Charlie", "Alice", "Eve", "Bob", "David",
"Frank", "Grace", "Hannah", "Ivy", "Jack");
// BAD: Sort everything, then filter
List bad = names.stream()
.sorted() // sorts all 10 elements
.filter(n -> n.length() > 3) // then filters
.map(String::toUpperCase)
.collect(Collectors.toList());
// GOOD: Filter first, then sort the smaller set
List good = names.stream()
.filter(n -> n.length() > 3) // filters to 7 elements
.sorted() // sorts only 7 elements
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("Result: " + good);
// Output: Result: [ALICE, CHARLIE, DAVID, FRANK, GRACE, HANNAH]
}
}
Streams and loops are both tools for processing collections. Neither is universally better — each has strengths. Knowing when to use which is a mark of a senior developer.
| Scenario | Why Streams Win |
|---|---|
| Filter + transform + collect | Readable, composable pipeline |
| Grouping and aggregation | groupingBy, counting, summarizing are expressive |
| Parallel processing | One method call to parallelize |
| Chaining multiple operations | Flat pipeline vs nested loops |
| Declarative data queries | Reads like SQL: filter-where, map-select, collect-into |
| Scenario | Why Loops Win |
|---|---|
| Need to break/continue mid-iteration | Streams have no break/continue |
| Modifying the source collection | Streams do not modify the source |
| Index-based access needed | Streams do not expose indices directly |
| Multiple return values from one pass | Loops can update multiple variables |
| Checked exceptions in the logic | Lambdas do not support checked exceptions neatly |
| Simple iteration with nothing to transform | A for-each loop is simpler |
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class StreamVsLoop {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie", "Anna", "David", "Andrew");
// --- STREAM WINS: Filter + Transform + Collect ---
// Stream approach -- clean, declarative
List streamResult = names.stream()
.filter(n -> n.startsWith("A"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
// Loop approach -- more verbose
List loopResult = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
loopResult.add(name.toUpperCase());
}
}
java.util.Collections.sort(loopResult);
System.out.println("Stream: " + streamResult);
System.out.println("Loop: " + loopResult);
// --- STREAM WINS: Grouping ---
Map> grouped = names.stream()
.collect(Collectors.groupingBy(n -> n.charAt(0)));
System.out.println("Grouped: " + grouped);
// This would take 10+ lines with loops
// --- LOOP WINS: Early exit with break ---
String found = null;
for (String name : names) {
if (name.length() > 5) {
found = name;
break; // Stream alternative: findFirst(), but less flexible
}
}
System.out.println("Found: " + found);
// --- LOOP WINS: Index-based access ---
for (int i = 0; i < names.size(); i++) {
System.out.println(i + ": " + names.get(i));
}
}
}
Let us bring everything together with a realistic example. We have a list of employees and need to perform various analytics: filtering, grouping, aggregating, and reporting.
import java.util.*;
import java.util.stream.Collectors;
public class EmployeeAnalytics {
enum Department { ENGINEERING, MARKETING, SALES, HR, FINANCE }
record Employee(String name, Department department, double salary, int yearsOfExperience) {}
public static void main(String[] args) {
List employees = List.of(
new Employee("Alice", Department.ENGINEERING, 95000, 5),
new Employee("Bob", Department.ENGINEERING, 110000, 8),
new Employee("Charlie", Department.MARKETING, 72000, 3),
new Employee("Diana", Department.SALES, 68000, 2),
new Employee("Eve", Department.ENGINEERING, 125000, 12),
new Employee("Frank", Department.HR, 65000, 4),
new Employee("Grace", Department.FINANCE, 88000, 6),
new Employee("Hannah", Department.MARKETING, 78000, 5),
new Employee("Ivan", Department.SALES, 71000, 3),
new Employee("Julia", Department.FINANCE, 92000, 7),
new Employee("Kevin", Department.HR, 60000, 1),
new Employee("Laura", Department.ENGINEERING, 105000, 9),
new Employee("Mike", Department.SALES, 75000, 4),
new Employee("Nina", Department.MARKETING, 82000, 6),
new Employee("Oscar", Department.FINANCE, 97000, 8)
);
// 1. Total salary expense
double totalSalary = employees.stream()
.mapToDouble(Employee::salary)
.sum();
System.out.println("Total salary expense: $" + String.format("%,.2f", totalSalary));
// 2. Average salary
double avgSalary = employees.stream()
.mapToDouble(Employee::salary)
.average()
.orElse(0.0);
System.out.println("Average salary: $" + String.format("%,.2f", avgSalary));
// 3. Highest paid employee
employees.stream()
.max(Comparator.comparingDouble(Employee::salary))
.ifPresent(e -> System.out.println("Highest paid: " + e.name() + " ($" +
String.format("%,.2f", e.salary()) + ")"));
// 4. Employees earning above average
List aboveAverage = employees.stream()
.filter(e -> e.salary() > avgSalary)
.sorted(Comparator.comparingDouble(Employee::salary).reversed())
.map(e -> e.name() + " ($" + String.format("%,.2f", e.salary()) + ")")
.collect(Collectors.toList());
System.out.println("Above average: " + aboveAverage);
// 5. Group by department
Map> byDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.mapping(Employee::name, Collectors.toList())
));
System.out.println("\nEmployees by department:");
byDept.forEach((dept, names) ->
System.out.println(" " + dept + ": " + names));
// 6. Average salary by department
Map avgByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.averagingDouble(Employee::salary)
));
System.out.println("\nAverage salary by department:");
avgByDept.entrySet().stream()
.sorted(Map.Entry.comparingByValue().reversed())
.forEach(e -> System.out.println(" " + e.getKey() + ": $" +
String.format("%,.2f", e.getValue())));
// 7. Department with highest average salary
avgByDept.entrySet().stream()
.max(Map.Entry.comparingByValue())
.ifPresent(e -> System.out.println("\nHighest avg dept: " + e.getKey() +
" ($" + String.format("%,.2f", e.getValue()) + ")"));
// 8. Count employees per department
Map countByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::department, Collectors.counting()));
System.out.println("\nHeadcount by department: " + countByDept);
// 9. Partition into senior (5+ years) and junior
Map> seniorPartition = employees.stream()
.collect(Collectors.partitioningBy(e -> e.yearsOfExperience() >= 5));
System.out.println("\nSenior employees (5+ years):");
seniorPartition.get(true).forEach(e ->
System.out.println(" " + e.name() + " - " + e.yearsOfExperience() + " years"));
System.out.println("Junior employees (< 5 years):");
seniorPartition.get(false).forEach(e ->
System.out.println(" " + e.name() + " - " + e.yearsOfExperience() + " years"));
// 10. Salary statistics by department
System.out.println("\nSalary statistics by department:");
employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.summarizingDouble(Employee::salary)
))
.forEach((dept, stats) -> System.out.printf(
" %s: count=%d, min=$%,.0f, max=$%,.0f, avg=$%,.0f, total=$%,.0f%n",
dept, stats.getCount(), stats.getMin(), stats.getMax(),
stats.getAverage(), stats.getSum()));
// 11. Top 3 earners across the company
System.out.println("\nTop 3 earners:");
employees.stream()
.sorted(Comparator.comparingDouble(Employee::salary).reversed())
.limit(3)
.forEach(e -> System.out.println(" " + e.name() + " - $" +
String.format("%,.2f", e.salary())));
// 12. Build a salary report string
String report = employees.stream()
.sorted(Comparator.comparing(Employee::name))
.map(e -> String.format("%-10s | %-12s | $%,10.2f | %2d yrs",
e.name(), e.department(), e.salary(), e.yearsOfExperience()))
.collect(Collectors.joining("\n"));
System.out.println("\n--- Employee Report ---");
System.out.println(String.format("%-10s | %-12s | %11s | %s",
"Name", "Department", "Salary", "Experience"));
System.out.println("-".repeat(55));
System.out.println(report);
}
}
Total salary expense: $1,283,000.00 Average salary: $85,533.33 Highest paid: Eve ($125,000.00) Above average: [Eve ($125,000.00), Bob ($110,000.00), Laura ($105,000.00), Oscar ($97,000.00), Alice ($95,000.00), Julia ($92,000.00), Grace ($88,000.00)] Employees by department: ENGINEERING: [Alice, Bob, Eve, Laura] MARKETING: [Charlie, Hannah, Nina] SALES: [Diana, Ivan, Mike] HR: [Frank, Kevin] FINANCE: [Grace, Julia, Oscar] Average salary by department: ENGINEERING: $108,750.00 FINANCE: $92,333.33 MARKETING: $77,333.33 SALES: $71,333.33 HR: $62,500.00 Top 3 earners: Eve - $125,000.00 Bob - $110,000.00 Laura - $105,000.00