Collecting to immutable collections
Stream.toList, unmodifiable collectors, List.copyOf, Guava immutable collectors, null handling, duplicate keys and shallow immutability.
On this page
🧊 What does collecting to an immutable collection mean?
Collecting to an immutable collection means the stream result cannot have elements added, removed or replaced through that collection API. It acts like printing a report at the end of a pipeline: the working notes may be messy, but the handed-off copy should not change. For example, orders.stream().map(Order::id).toList() gives an unmodifiable list in Java 16+.
For this lesson, we'll focus on the stream-side choices: Stream.toList(), Collectors.toUnmodifiableList/Set/Map, Java 8 collectingAndThen, List.copyOf, Guava immutable collectors, null handling and duplicate policies. The broader collection taxonomy is covered in Immutable collections, so we won't re-teach factory methods except where they affect stream results.
Below the line (out of scope)
- Immutable collection factories and defensive copies in constructors: Immutable collections
- General collector mechanics: Collectors deep dive
- Map duplicate-key handling: Collectors.toMap and working with maps
- Fully immutable domain objects and records: covered in Core Java lessons
The 30-second answer
In Java 16+, stream.toList() is the shortest way to get an unmodifiable list, and it allows null elements. Since Java 10, Collectors.toUnmodifiableList, toUnmodifiableSet and toUnmodifiableMap return unmodifiable results and reject nulls; toUnmodifiableMap also throws on duplicate keys unless you provide a merge function. In Java 8, collectingAndThen(toList(), Collections::unmodifiableList) wraps the mutable list produced by the downstream collector, so callers cannot mutate through the returned reference, but it is still a shallow result. List.copyOf(stream.collect(toList())) snapshots into a JDK unmodifiable copy and rejects nulls. Guava's ImmutableList.toImmutableList() is still common in projects that already use Guava. None of these make mutable element objects immutable.
What you’ll learn
- Collect a Java Stream to an immutable collection using Java 16, Java 10 and Java 8 options
- Compare
Stream.toList(),Collectors.toUnmodifiableList()andCollectors.toList()contracts - Use
collectingAndThen,List.copyOfand GuavaImmutableList.toImmutableList()in the right context - Explain unmodifiable versus immutable and why element objects may still mutate
- Predict null handling for
toList,toUnmodifiableList,toUnmodifiableSet,toUnmodifiableMapandcopyOf - Handle duplicate elements and duplicate map keys in unmodifiable collectors
01Picture it first: freeze at the boundary
Picture a stream pipeline as a prep table. While you're cooking, mutable bowls are fine: add ingredients, combine batches, then plate the result. The collecting step is the handoff to the waiter, and that is where you usually want the plate covered.
A stream pipeline accumulates into a mutable working container, then finishes as Stream.toList, toUnmodifiableList, collectingAndThen or a Guava ImmutableList
The important distinction is where the freeze happens. Stream.toList() and the Java 10 unmodifiable collectors return an unmodifiable result directly. collectingAndThen runs a finisher after another collector, often wrapping or copying whatever the downstream collector built.
02Immutable collection collecting under the hood
Try it yourself first
Before reading the code, predict which result accepts add, and which one accepts a null element.
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ImmutableStreamResultsDemo {
public static void main(String[] args) {
List<String> java16 = Stream.of("card", null).toList();
System.out.println(java16.contains(null)); // true
List<String> java10 = Stream.of("card", "cash")
.collect(Collectors.toUnmodifiableList());
List<String> java8 = Stream.of("card", "cash")
.collect(Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
));
try {
java16.add("refund");
} catch (UnsupportedOperationException expected) {
System.out.println("Stream.toList result is unmodifiable");
}
}
}
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Advanced: nulls, duplicates and maps
- Java 8, JDK 21 and Guava choices
- Unmodifiable versus immutable, in one sentence
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal