Definition
- List.of(), Set.of(), Map.of() – Java 9 factory methods that return truly immutable collections; all mutating operations throw
UnsupportedOperationException; null elements throwNullPointerException - Collections.unmodifiableList() – wraps a mutable list in a read-only view; the view rejects modification, but the original mutable list can still be changed and the view reflects those changes
- List.copyOf() – Java 10 method that creates an immutable defensive copy; independent of the source collection; if the source is already immutable, returns the same instance
- Compact representation – immutable collections use optimised internal classes (List12, ListN, Set12, SetN) that consume less memory than ArrayList or HashMap
At National Bank of India, List.of() is like a bank statement carved in stone. No one can add, remove, or change any transaction line. Collections.unmodifiableList() is like a glass display case around the original paper ledger. The display case prevents viewers from touching the ledger, but the accountant (who has the original) can still write in it, and the display reflects the changes. List.copyOf() is like making a photocopy and locking it in a vault. Even if the original ledger changes, the vault copy remains unchanged.
Understanding the Concepts
What are List.of, Set.of, and Map.of?
Java 9 introduced factory methods on the List, Set, and Map interfaces for creating immutable collections in a single expression. These collections reject all mutation attempts and do not allow null elements.
package com.bank.demo;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ImmutableFactoryDemo {
public static void main(String[] args) {
List<String> accounts = List.of("SAV2001", "CUR3001", "FD4001");
Set<String> activeStatuses = Set.of("ACTIVE", "VERIFIED", "APPROVED");
Map<String, String> owners = Map.of(
"SAV2001", "Rajesh",
"CUR3001", "Priya"
);
System.out.println("Accounts: " + accounts);
System.out.println("Statuses: " + activeStatuses);
System.out.println("Owners: " + owners);
// All modification attempts throw UnsupportedOperationException
try {
accounts.add("NEW-001");
} catch (UnsupportedOperationException e) {
System.out.println("add() blocked: " + e.getClass().getSimpleName());
}
try {
accounts.set(0, "CHANGED");
} catch (UnsupportedOperationException e) {
System.out.println("set() blocked: " + e.getClass().getSimpleName());
}
}
}
Accounts: [SAV2001, CUR3001, FD4001]
Statuses: [ACTIVE, VERIFIED, APPROVED]
Owners: {CUR3001=Priya, SAV2001=Rajesh}
add() blocked: UnsupportedOperationException
set() blocked: UnsupportedOperationException
Why do we need immutable collections?
Immutable collections solve three problems. First, they prevent accidental modification of shared data, which is especially important in multi-threaded banking applications. Second, they can be safely passed between methods without defensive copying. Third, they have compact internal representations that use less memory.
package com.bank.demo;
import java.util.List;
public class SafeSharingDemo {
// This method returns an immutable list: callers cannot modify internal state
public static List<String> getVipAccounts() {
return List.of("SAV2001", "CUR3001", "FD4001");
}
public static void main(String[] args) {
List<String> vips = getVipAccounts();
System.out.println("VIP accounts: " + vips);
try {
vips.add("HACK-001"); // caller cannot tamper with the list
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify: safe!");
}
}
}
VIP accounts: [SAV2001, CUR3001, FD4001]
Cannot modify: safe!
How is Collections.unmodifiableList different from List.of?
Collections.unmodifiableList creates a read-only view that wraps the original mutable list. The view rejects modification, but if someone holds a reference to the original, they can still change it and the view reflects those changes.
package com.bank.demo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class UnmodifiableVsImmutableDemo {
public static void main(String[] args) {
// unmodifiableList: VIEW, not truly immutable
List<String> original = new ArrayList<>(List.of("SAV2001", "CUR3001"));
List<String> view = Collections.unmodifiableList(original);
System.out.println("View before: " + view);
original.add("FD4001"); // modify the ORIGINAL
System.out.println("View after: " + view); // view reflects the change!
// List.of: truly immutable, no underlying mutable reference
List<String> immutable = List.of("SAV2001", "CUR3001");
System.out.println("Immutable: " + immutable);
// No way to modify this list from any reference
}
}
View before: [SAV2001, CUR3001]
View after: [SAV2001, CUR3001, FD4001]
Immutable: [SAV2001, CUR3001]
| Feature | List.of() | Collections.unmodifiableList() | List.copyOf() |
|---|---|---|---|
| Truly immutable | Yes | No (view) | Yes |
| Null elements | Throws NPE | Allows | Throws NPE |
| Backed by original | No | Yes | No |
| Memory | Compact | Wrapper + original | Compact |
| Since | Java 9 | Java 2 | Java 10 |
How does List.copyOf work?
List.copyOf (Java 10) creates an immutable defensive copy of any collection. If the source is already an immutable List.of() result, copyOf returns the same instance (zero-copy optimisation).
package com.bank.demo;
import java.util.ArrayList;
import java.util.List;
public class CopyOfDemo {
public static void main(String[] args) {
List<String> mutable = new ArrayList<>(List.of("SAV2001", "CUR3001"));
List<String> copy = List.copyOf(mutable);
mutable.add("FD4001"); // modify the original
System.out.println("Original: " + mutable);
System.out.println("Copy: " + copy); // copy is unaffected
// Optimisation: copyOf an already immutable list returns same instance
List<String> immutable = List.of("A", "B");
List<String> copy2 = List.copyOf(immutable);
System.out.println("Same instance: " + (immutable == copy2));
}
}
Original: [SAV2001, CUR3001, FD4001]
Copy: [SAV2001, CUR3001]
Same instance: true
What are the rules and constraints?
- No nulls.
List.of(null),Set.of(null),Map.of(null, val)all throw NullPointerException. - No duplicates in Set.of.
Set.of("A", "A")throws IllegalArgumentException. - No duplicate keys in Map.of.
Map.of("k", "v1", "k", "v2")throws IllegalArgumentException. - Map.of limit. Map.of() supports up to 10 key-value pairs. For more, use Map.ofEntries.
- Iteration order. Set.of() and Map.of() do not guarantee iteration order. List.of() preserves insertion order.
package com.bank.demo;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ImmutableConstraintsDemo {
public static void main(String[] args) {
// Null rejection
try { List.of((String) null); }
catch (NullPointerException e) { System.out.println("List.of(null): NPE"); }
// Duplicate rejection in Set
try { Set.of("SAV2001", "SAV2001"); }
catch (IllegalArgumentException e) { System.out.println("Set.of duplicates: IAE"); }
// Map.ofEntries for more than 10 pairs
Map<String, String> bigMap = Map.ofEntries(
Map.entry("SAV2001", "Rajesh"),
Map.entry("CUR3001", "Priya"),
Map.entry("FD4001", "Pankaj")
);
System.out.println("Map.ofEntries: " + bigMap);
}
}
List.of(null): NPE
Set.of duplicates: IAE
Map.ofEntries: {FD4001=Pankaj, SAV2001=Rajesh, CUR3001=Priya}
What are common mistakes?
Mistake: Assuming unmodifiableList is truly immutable
// WRONG: the caller can still modify through the original reference
List<String> original = new ArrayList<>(List.of("SAV2001"));
List<String> safe = Collections.unmodifiableList(original);
original.add("SNEAKY"); // modifies the "safe" list through the back door!
System.out.println(safe); // [SAV2001, SNEAKY]
// CORRECT: use List.copyOf for a true defensive copy
List<String> original = new ArrayList<>(List.of("SAV2001"));
List<String> safe = List.copyOf(original);
original.add("SNEAKY");
System.out.println(safe); // [SAV2001] - unaffected
Mistake: Passing null to List.of
// WRONG: List.of does not allow null
String name = null;
List<String> list = List.of(name); // NullPointerException!
// CORRECT: filter nulls or use ArrayList if nulls are needed
import java.util.stream.*;
List<String> list = Stream.of("SAV2001", null, "CUR3001")
.filter(java.util.Objects::nonNull)
.toList();
What about thread safety?
Immutable collections are inherently thread-safe for reads because their state never changes. No synchronisation is needed when sharing them across threads. This makes them ideal for configuration data, constant lists, and read-only reference data in concurrent banking applications.
What should a senior engineer know?
- Internal classes. List.of() with 1-2 elements returns List12 (fields-based, no array). With 3+ elements, it returns ListN (compact array). This is more memory-efficient than ArrayList with its resizable array and size field.
- Serialization. Immutable collections are serializable. They deserialize to immutable instances.
- toUnmodifiableList. Java 10 added
Collectors.toUnmodifiableList()andStream.toList()(Java 16) that produce immutable lists directly from streams. - Shallow immutability. List.of(account1, account2) is immutable at the list level, but the Account objects inside can still be mutated. For deep immutability, use records or ensure the elements are also immutable.
What are common interview follow-up questions?
- "What is the difference between List.of and Collections.unmodifiableList?" : List.of is truly immutable with no backing mutable collection; unmodifiableList is a view that reflects changes to the original.
- "Can List.of contain null?" : No, it throws NullPointerException.
- "What happens if you pass duplicates to Set.of?" : IllegalArgumentException at creation time.
- "Is List.of thread-safe?" : Yes, because immutable collections cannot change state.
Visual Overview
List.of() vs Collections.unmodifiableList() vs List.copyOf()
=============================================================
List.of("SAV2001", "CUR3001"):
┌──────────────────────┐
│ ImmutableList │
│ [SAV2001, CUR3001] │ No backing mutable reference
│ add/set/remove: UOE │ Truly immutable
└──────────────────────┘
Collections.unmodifiableList(original):
┌──────────────────────┐ ┌──────────────────────┐
│ UnmodifiableList VIEW │ ──▶ │ ArrayList (original) │
│ add/set/remove: UOE │ │ Mutable! Can change! │
└──────────────────────┘ └──────────────────────┘
Changes to original are visible through the view!
List.copyOf(original):
┌──────────────────────┐ ┌──────────────────────┐
│ ImmutableList (copy) │ │ ArrayList (original) │
│ [SAV2001, CUR3001] │ │ Can change freely │
│ Independent of source │ │ No effect on copy │
└──────────────────────┘ └──────────────────────┘
Key Takeaway
Java 9 factory methods (List.of, Set.of, Map.of) create truly immutable collections that reject all modification, prohibit null elements, and use compact internal representations. They are the preferred way to create constant lists, sets, and maps in modern Java. Collections.unmodifiableList is a weaker guarantee: it creates a read-only view, but the underlying mutable collection can still change and those changes are visible through the view.
List.copyOf (Java 10) bridges the gap: it creates an immutable defensive copy from any collection, independent of the source. For collections that are already immutable, copyOf returns the same instance as an optimisation.
In the National Bank of India's API layer, Pankaj established a rule that all service methods return List.copyOf or List.of for response data. This ensured that callers could never accidentally modify the internal state of the service, and that the returned collections were inherently thread-safe for concurrent consumers.
Remember one sentence: use List.of for immutable constants, List.copyOf for defensive copies, and avoid Collections.unmodifiableList in new code because it is a leaky view, not truly immutable.
---