Modern JavaCollections
Immutable & Sequenced Collections
List.of vs Collections.unmodifiableList vs Arrays.asList, defensive copies, and Java 21 sequenced collections.
Java
Three kinds of “read-only”
Java has several ways to get a collection you can’t modify — with very different guarantees:
| Factory | Mutate the result? | Backing collection changes visible? | Nulls? |
|---|---|---|---|
List.of(...), Set.of, Map.of (Java 9) |
✗ throws | n/a — truly immutable | ✗ throws NPE |
List.copyOf(c) (Java 10) |
✗ throws | No — independent copy | ✗ |
Collections.unmodifiableList(list) |
✗ throws | Yes — it’s a read-only view | ✓ |
Arrays.asList(array) |
set ✓, add/remove ✗ |
Yes — writes through to the array | ✓ |
stream.toList() (Java 16) |
✗ throws | n/a | ✓ |
Collectors.toList() |
✓ (currently an ArrayList) |
n/a | ✓ |
import java.util.*;
import java.util.stream.*;
class Main {
static void attempt(String label, Runnable r) {
try { r.run(); System.out.println(label + " -> ok"); }
catch (RuntimeException e) { System.out.println(label + " -> " + e.getClass().getSimpleName()); }
}
public static void main(String[] args) {
List<String> source = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(source);
List<String> copy = List.copyOf(source);
source.add("c"); // mutate the original
System.out.println("view sees change: " + view);
System.out.println("copy is frozen: " + copy);
attempt("view.add", () -> view.add("x"));
attempt("List.of.add", () -> List.of(1, 2).add(3));
attempt("List.of(null)", () -> List.of("a", null));
attempt("Map.of dup keys", () -> Map.of("k", 1, "k", 2));
String[] array = {"x", "y", "z"};
List<String> asList = Arrays.asList(array);
asList.set(0, "X"); // writes through to the array
System.out.println("array[0] = " + array[0]);
attempt("asList.add", () -> asList.add("w"));
List<Integer> streamed = Stream.of(1, 2, 3).toList();
attempt("stream.toList().add", () -> streamed.add(4));
List<Integer> collected = Stream.of(1, 2, 3).collect(Collectors.toList());
attempt("collect(toList()).add", () -> collected.add(4));
}
}Output
Defensive copies
When a class stores a collection passed in by a caller, or returns one of its own, copy it — otherwise the caller can mutate your internal state:
public final class Order {
private final List<LineItem> items;
public Order(List<LineItem> items) {
this.items = List.copyOf(items); // caller's list can't affect us
}
public List<LineItem> items() {
return items; // already unmodifiable — safe to share
}
}
List.copyOf is smart: if the argument is already an immutable List.of list, it returns it without copying.
Sequenced collections (Java 21)
Before Java 21, getting the last element was inconsistent: list.get(list.size() - 1), deque.getLast(), sortedSet.last(), and for LinkedHashSet — iterate the whole thing. Java 21 adds three interfaces that unify ordered collections:
classDiagram
direction BT
class SequencedCollection {
<<interface>>
+addFirst(E)
+addLast(E)
+getFirst() E
+getLast() E
+removeFirst() E
+removeLast() E
+reversed() SequencedCollection
}
class SequencedSet { <<interface>> }
class SequencedMap {
<<interface>>
+firstEntry()
+lastEntry()
+putFirst(K, V)
+putLast(K, V)
+reversed() SequencedMap
}
List --|> SequencedCollection
Deque --|> SequencedCollection
SequencedSet --|> SequencedCollection
LinkedHashSet ..|> SequencedSet
SortedSet --|> SequencedSet
LinkedHashMap ..|> SequencedMap
SortedMap --|> SequencedMap
import java.util.*;
class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("b", "c"));
list.addFirst("a");
list.addLast("d");
System.out.println(list.getFirst() + " … " + list.getLast() + " reversed view: " + list.reversed());
LinkedHashSet<Integer> set = new LinkedHashSet<>(List.of(3, 1, 4, 1, 5));
System.out.println("first=" + set.getFirst() + " last=" + set.getLast());
LinkedHashMap<String, Integer> scores = new LinkedHashMap<>();
scores.put("ada", 90); scores.put("bob", 72); scores.put("cy", 85);
scores.putFirst("zed", 60); // insert at the front
System.out.println(scores + " last entry: " + scores.lastEntry());
System.out.println("reversed: " + scores.reversed());
// reversed() is a live view, not a copy
List<String> rev = list.reversed();
list.add("e");
System.out.println("view updated: " + rev);
}
}Output
Finished this lesson?
Mark it complete to track your progress through Modern Java.