Definition
- Immutability contract – all mutating operations (add, set, remove, sort, clear, replaceAll) throw
UnsupportedOperationException - Design intent – immutable collections are thread-safe, can be shared freely, and protect API boundaries from accidental modification
- List.copyOf(collection) – creates an independent immutable copy; if the source is already immutable, returns the same instance
- To mutable – wrap in
new ArrayList<>(immutableList)to get a mutable copy - Stream integration –
Stream.toList()(Java 16) andCollectors.toUnmodifiableList()(Java 10) return immutable lists directly from streams
At National Bank of India, the annual audit report is printed and sealed in a tamper-proof cover (List.of). No one can add pages, rip out pages, or rewrite entries. If someone needs to make changes, they must photocopy the report into a new binder (new ArrayList<>) and modify the copy. List.copyOf is like the vault department making a sealed duplicate of the ledger: even if the original ledger is later amended, the vault copy remains untouched.
Understanding the Concepts
Why does List.of() throw UnsupportedOperationException?
List.of() returns an immutable list. Every method that would change the list's state throws UnsupportedOperationException. This is an intentional design decision in the Java Collections Framework: immutable collections guarantee that their state never changes after creation.
package com.bank.demo;
import java.util.List;
public class UnsupportedOperationDemo {
public static void main(String[] args) {
List<String> accounts = List.of("SAV2001", "CUR3001", "FD4001");
String[] operations = {"add", "set", "remove", "sort", "clear"};
for (String op : operations) {
try {
switch (op) {
case "add" -> accounts.add("NEW-001");
case "set" -> accounts.set(0, "CHANGED");
case "remove" -> accounts.remove(0);
case "sort" -> accounts.sort(null);
case "clear" -> accounts.clear();
}
} catch (UnsupportedOperationException e) {
System.out.println(op + "() throws UnsupportedOperationException");
}
}
// Read operations work perfectly
System.out.println("get(0): " + accounts.get(0));
System.out.println("size: " + accounts.size());
System.out.println("contains SAV2001: " + accounts.contains("SAV2001"));
}
}
add() throws UnsupportedOperationException
set() throws UnsupportedOperationException
remove() throws UnsupportedOperationException
sort() throws UnsupportedOperationException
clear() throws UnsupportedOperationException
get(0): SAV2001
size: 3
contains SAV2001: true
How does List.copyOf create a safe snapshot?
List.copyOf takes any Collection and returns an immutable copy. The copy is completely independent of the source: changes to the source do not affect the copy, and the copy cannot be modified.
package com.bank.demo;
import java.util.ArrayList;
import java.util.List;
public class CopyOfSnapshotDemo {
public static void main(String[] args) {
List<String> mutable = new ArrayList<>();
mutable.add("SAV2001");
mutable.add("CUR3001");
// Create immutable snapshot
List<String> snapshot = List.copyOf(mutable);
System.out.println("Snapshot: " + snapshot);
// Modify original: snapshot is unaffected
mutable.add("FD4001");
mutable.remove("SAV2001");
System.out.println("Original after changes: " + mutable);
System.out.println("Snapshot still: " + snapshot);
// Optimisation: copyOf on already-immutable returns same instance
List<String> immutable = List.of("A", "B");
List<String> copy = List.copyOf(immutable);
System.out.println("Same instance? " + (immutable == copy));
}
}
Snapshot: [SAV2001, CUR3001]
Original after changes: [CUR3001, FD4001]
Snapshot still: [SAV2001, CUR3001]
Same instance? true
How do you convert between mutable and immutable lists?
Converting from immutable to mutable requires wrapping in a new ArrayList. Converting from mutable to immutable uses List.copyOf or List.of.
package com.bank.demo;
import java.util.ArrayList;
import java.util.List;
public class MutableImmutableConversionDemo {
public static void main(String[] args) {
// Immutable to mutable
List<String> immutable = List.of("SAV2001", "CUR3001");
List<String> mutable = new ArrayList<>(immutable);
mutable.add("FD4001");
System.out.println("Mutable copy: " + mutable);
// Mutable to immutable
List<String> snapshot = List.copyOf(mutable);
System.out.println("Immutable copy: " + snapshot);
// From stream (Java 16)
List<String> filtered = mutable.stream()
.filter(acc -> acc.startsWith("SAV"))
.toList(); // returns immutable list
System.out.println("Filtered: " + filtered);
}
}
Mutable copy: [SAV2001, CUR3001, FD4001]
Immutable copy: [SAV2001, CUR3001, FD4001]
Filtered: [SAV2001]
What is the difference between List.of and Arrays.asList for immutability?
| Feature | List.of() | Arrays.asList() | new ArrayList<>() |
|---|---|---|---|
| add/remove | UOE | UOE | Allowed |
| set() | UOE | Allowed (writes through) | Allowed |
| Backed by array | No | Yes | Independent |
| Null elements | No (NPE) | Yes | Yes |
| Truly immutable | Yes | No (set works) | No |
package com.bank.demo;
import java.util.Arrays;
import java.util.List;
public class ListOfVsAsListDemo {
public static void main(String[] args) {
List<String> listOf = List.of("SAV2001", "CUR3001");
List<String> asList = Arrays.asList("SAV2001", "CUR3001");
// set() works on asList but not on List.of
asList.set(0, "CHANGED");
System.out.println("asList after set: " + asList);
try {
listOf.set(0, "CHANGED");
} catch (UnsupportedOperationException e) {
System.out.println("List.of set: UOE");
}
}
}
asList after set: [CHANGED, CUR3001]
List.of set: UOE
What are common mistakes?
Mistake: Returning a mutable internal list from a service method
// WRONG: caller can modify the service's internal state
public class AccountService {
private final List<String> vipAccounts = new ArrayList<>(List.of("SAV2001", "CUR3001"));
public List<String> getVipAccounts() {
return vipAccounts; // caller can add/remove from internal list!
}
}
// CORRECT: return an immutable copy
public class AccountService {
private final List<String> vipAccounts = new ArrayList<>(List.of("SAV2001", "CUR3001"));
public List<String> getVipAccounts() {
return List.copyOf(vipAccounts); // caller gets immutable snapshot
}
}
Mistake: Expecting List.of to work with nulls from database results
// WRONG: database query may return null values
String name = null; // from ResultSet
List<String> names = List.of("Rajesh", name, "Pankaj"); // NPE!
// CORRECT: filter nulls first, or use ArrayList
import java.util.stream.Stream;
import java.util.Objects;
List<String> names = Stream.of("Rajesh", null, "Pankaj")
.filter(Objects::nonNull)
.toList();
What about shallow vs deep immutability?
List.of provides shallow immutability: the list itself cannot be modified, but the objects inside it can still be mutated if they are mutable.
package com.bank.demo;
import java.util.List;
public class ShallowImmutabilityDemo {
static class Account {
String id;
String status;
Account(String id, String status) { this.id = id; this.status = status; }
@Override public String toString() { return id + ":" + status; }
}
public static void main(String[] args) {
Account acc = new Account("SAV2001", "ACTIVE");
List<Account> list = List.of(acc);
// Cannot add/remove from the list
// list.add(new Account("X", "X")); // UOE
// BUT: can mutate the object inside
acc.status = "CLOSED";
System.out.println("After mutation: " + list.get(0));
}
}
After mutation: SAV2001:CLOSED
For deep immutability, use Java records or ensure all elements are themselves immutable.
What should a senior engineer know?
- Internal classes. List.of() with 0 elements returns a singleton empty list. With 1-2 elements, it returns List12 with direct fields (no array). With 3+ elements, it returns ListN with a compact array.
- Stream.toList() vs Collectors.toUnmodifiableList(). Both produce immutable lists. Stream.toList() (Java 16) is shorter. Collectors.toUnmodifiableList() (Java 10) is available for older codebases.
- Performance. Immutable lists have less overhead than ArrayList: no unused capacity, no size tracking, no structural modification counter.
- Set.copyOf and Map.copyOf. The copyOf pattern applies to all three collection types.
What are common interview follow-up questions?
- "Which operations throw UnsupportedOperationException on List.of?" : All mutating operations: add, set, remove, sort, clear, replaceAll, addAll, removeAll, retainAll.
- "Is the UnsupportedOperationException checked or unchecked?" : Unchecked (runtime exception).
- "How do you make a mutable copy of an immutable list?" : new ArrayList<>(immutableList).
- "What does List.copyOf do if the source is already immutable?" : Returns the same instance (no copy).
Visual Overview
Immutability Spectrum ===================== new ArrayList<>(): ┌────────────────────────────┐ │ add: OK │ │ set: OK │ │ remove: OK │ │ Fully mutable │ └────────────────────────────┘ Arrays.asList(arr): ┌────────────────────────────┐ │ add: UOE (fixed size) │ │ set: OK (writes to array) │ │ remove: UOE (fixed size) │ │ Semi-mutable │ └────────────────────────────┘ Collections.unmodifiableList(list): ┌────────────────────────────┐ │ add: UOE │ │ set: UOE │ │ remove: UOE │ │ BUT: original can change! │ │ Unmodifiable VIEW │ └────────────────────────────┘ List.of() / List.copyOf(): ┌────────────────────────────┐ │ add: UOE │ │ set: UOE │ │ remove: UOE │ │ No backing mutable ref │ │ TRULY IMMUTABLE │ └────────────────────────────┘
Key Takeaway
List.of() creates truly immutable lists where every mutating operation throws UnsupportedOperationException. This is not a limitation but a design feature: immutable collections are thread-safe, can be shared freely across method boundaries, and protect internal service state from accidental modification.
List.copyOf creates an immutable snapshot from any collection, making it the standard tool for defensive copies. If the source is already an immutable list, copyOf returns the same instance, avoiding unnecessary allocation. Converting back to a mutable list is straightforward: wrap in new ArrayList<>().
At National Bank of India, Pankaj mandated that every public service method return List.copyOf for list responses. This prevented a class of bugs where a downstream consumer accidentally added or removed elements from a list that was still being used by the service internally. In a banking system, such accidental mutation could mean a missing account in an audit trail.
Remember one sentence: List.of() is truly immutable and throws UnsupportedOperationException on all mutations, while List.copyOf() creates an independent immutable snapshot, making both essential for safe API design.
---