🗃️ Collections Framework · Intermediate

Immutable collections in Java

List.of, Set.of, Map.of, copyOf; no nulls; unmodifiable view vs immutable copy.

🧩 The mysterySet.of("a", "b", "a") doesn't quietly drop the duplicate. It throws. Java's immutable collections are strict on purpose. Here's why that's a good thing.

Sealed at birth

**List.of, Set.of and Map.of (Java 9+) create collections that can never change**. add, remove, set and clear all throw **UnsupportedOperationException**.

List<String> days = List.of("Mon", "Tue");
Map<String, Integer> m = Map.of("a", 1, "b", 2);
days.add("Wed"); // throws

Strict about input

These factories treat suspicious input as a bug: **null elements, keys or values → NullPointerException. Duplicate Set.of elements or Map.of keys → IllegalArgumentException**. Compare new HashSet<>(...), which silently drops duplicates.

List.of("a", null);     // NPE
Set.of("a", "a");       // IAE
Map.of("k", 1, "k", 2); // IAE
// NPE = NullPointerException
// IAE = IllegalArgumentException
🔮 Predict it

Your turn

What happens?

Set<String> s = Set.of("x", "y", "x");
System.out.println(s.size());
  1. Prints 2
  2. Prints 3
  3. Throws IllegalArgumentException
Show the answer

Set.of sees the duplicate "x" at creation time and throws **IllegalArgumentException**. A duplicate in a literal set is almost always a typo.

Snapshot vs window

**List.copyOf(src) (Java 10+) takes an independent immutable snapshot. Collections.unmodifiableList(src) is only a read-only view**: later changes to src show through it.

var copy = List.copyOf(src);   // frozen
var view = Collections.unmodifiableList(src);
// live window onto src
🔮 Predict it

Snapshot or window?

What does this print?

var src = new ArrayList<>(List.of("a"));
var copy = List.copyOf(src);
var view = Collections.unmodifiableList(src);
src.add("b");
int c = copy.size(), v = view.size();
System.out.println(c + " " + v);
  1. 2 2
  2. 1 2
  3. 1 1
Show the answer

copy froze one element. view still looks at src, so it sees the new element: 1 2.

⚠️ The trap

Shallowly immutable

Immutable collections freeze which objects they hold, not what's inside those objects. A List.of holding StringBuilders can't swap elements, but each StringBuilder can still be changed.

var list = List.of(new StringBuilder("hi"));
list.get(0).append("!");  // allowed
System.out.println(list); // [hi!]
💼 In the real world

Why teams love them

Immutable collections can be shared between threads with no locks and returned from methods with no defensive copying. A common pattern: records copy incoming lists with List.copyOf in their constructor, so no caller can change them later.

Key takeaways

  1. Any modification throws UnsupportedOperationException
  2. null elements, keys or values → NullPointerException
  3. Duplicate Set.of elements or Map.of keys → IllegalArgumentException
  4. copyOf = immutable snapshot; unmodifiableX = live read-only view

💡 List.of is a laminated menu; an unmodifiable view is a window onto the kitchen whiteboard — you can't write on it, but the chef can.

🤯 Did you know?

List.of has fixed-argument overloads for 0 to 10 elements plus a varargs version. The overloads avoid allocating a varargs array for the common small cases.

Practice questions

What happens when this runs?

List<String> list = List.of("a", null);
System.out.println(list.size());
  1. 2
  2. 1
  3. Throws NullPointerException
  4. Compile error
Check your answer

Throws NullPointerException. The List.of family rejects null elements at creation time with NullPointerException.

What happens when this runs?

Set<String> s = Set.of("a", "b", "a");
System.out.println(s.size());
  1. 2
  2. 3
  3. Throws IllegalArgumentException
  4. Throws UnsupportedOperationException
Check your answer

Throws IllegalArgumentException. Unlike new HashSet<>(...), which silently drops duplicates, Set.of treats a duplicate element as a programming mistake and throws IllegalArgumentException.

Next: Arrays.asList looks like it makes a normal list. Then add explodes.