Immutable Collections — List.of, Map.copyOf và cạm bẫy unmodifiableList
Ba cách tạo immutable collection trong Java 9+, shallow vs deep immutability, defensive copy pattern, và vì sao Collections.unmodifiableList không phải immutable thực sự.
TL;DR: Java có 3 cách chính tạo collection "bất biến": List.of(...) (true immutable, Java 9, JEP 269), List.copyOf(existing) (defensive copy rồi immutable), và Collections.unmodifiableList(arr) (chỉ là view chỉ-đọc trên list gốc — list gốc thay đổi, view thấy ngay). Sự khác biệt quan trọng: hai cách đầu bảo vệ bạn hoàn toàn sau khi tạo, cách thứ ba là bẫy kinh điển trong production. Bài này đi sâu vào cơ chế, bộ nhớ, và pattern defensive copy để bảo vệ state nội bộ class.
1. Scenario — bug "bất biến" nhưng vẫn thay đổi
Một developer viết class OrderSummary trả về danh sách item đã được "bảo vệ":
public class OrderSummary {
private List<String> items;
public OrderSummary(List<String> items) {
this.items = items;
}
public List<String> getItems() {
return Collections.unmodifiableList(this.items);
}
}
Code review đồng ý: getItems() trả về unmodifiable view, caller không thể add hay remove. Test pass. Nhưng trong production:
List<String> items = new ArrayList<>(List.of("item1", "item2", "item3"));
OrderSummary summary = new OrderSummary(items);
System.out.println(summary.getItems()); // [item1, item2, item3]
items.add("item4"); // caller sua list goc SAU khi truyen vao
System.out.println(summary.getItems()); // [item1, item2, item3, item4] -- thay doi!
OrderSummary không bao giờ sửa items. Nhưng caller vẫn giữ tham chiếu đến list gốc và sửa được. unmodifiableList chỉ ngăn caller sửa qua wrapper — không ngăn ai có reference gốc sửa trực tiếp.
Đây là aliasing bug — 2 tham chiếu cùng trỏ 1 object, thay đổi qua tham chiếu nào cũng thấy qua tham chiếu kia.
2. Collections.unmodifiableList — view, không phải immutable
Collections.unmodifiableList(List<T> list) (Java 1.2) trả về một view chỉ-đọc (read-only wrapper) bao quanh list gốc. Mọi thao tác write (add, remove, set, clear) ném UnsupportedOperationException. Nhưng nó không copy dữ liệu — nó chỉ là lớp bọc mỏng.
flowchart LR
A["caller reference<br/>(ArrayList)"] -- "add/remove OK" --> B["ArrayList backing data"]
C["unmodifiable wrapper"] -- "throw UOE on write" --> C
C -- "read-through" --> B
B -- "shared state" --> BHệ quả: nếu list gốc bị thay đổi bởi bất kỳ ai còn giữ tham chiếu gốc, wrapper "bất biến" sẽ phản ánh thay đổi đó ngay lập tức. Đây KHÔNG phải immutable — chỉ là "tôi không được phép sửa, nhưng người khác vẫn sửa được".
3. List.of — true immutable (Java 9, JEP 269)
JEP 269 (Java 9, "Convenience Factory Methods for Collections") giới thiệu các factory method List.of(...), Set.of(...), Map.of(...) tạo collection thực sự bất biến:
List<String> names = List.of("An", "Binh", "Cuong");
names.add("Dung"); // UnsupportedOperationException
names.set(0, "X"); // UnsupportedOperationException
names.remove(0); // UnsupportedOperationException
names.clear(); // UnsupportedOperationException
Đặc điểm của List.of:
- Không cho phép phần tử
null— némNullPointerExceptionngay khi tạo. - Thứ tự phần tử được bảo toàn.
- Bên trong JDK dùng array nhỏ, compact representation (không phải
ArrayList). - Immutable thực sự — không có backing mutable collection nào bên dưới.
- Thread-safe cho read (nhiều thread đọc đồng thời an toàn).
// Khong cho phep null
List<String> list = List.of("a", null, "b"); // NullPointerException!
// Set va Map tuong tu
Set<Integer> nums = Set.of(1, 2, 3);
Map<String, Integer> scores = Map.of("An", 95, "Binh", 87);
List.of(1) tạo instance List12 (tối ưu cho 1 phần tử). List.of(1, 2) tạo List12 khác. List.of(1, 2, 3, ...) tạo ListN dùng array cố định. JDK tối ưu memory bằng cách không tạo ArrayList wrapper thừa. Từ Java 12+, các class này có tên như ImmutableCollections$List12, có thể xem bằng list.getClass().getName().
4. List.copyOf — defensive copy + immutable
List.copyOf(Collection<? extends E> coll) (Java 10) tạo bản copy độc lập rồi làm immutable:
List<String> mutable = new ArrayList<>(List.of("a", "b", "c"));
List<String> immutable = List.copyOf(mutable);
mutable.add("d"); // sua list goc
System.out.println(mutable); // [a, b, c, d]
System.out.println(immutable); // [a, b, c] -- KHONG thay doi
immutable là bản copy độc lập tại thời điểm gọi copyOf — không còn liên kết với mutable.
Tối ưu thông minh: nếu đầu vào đã là immutable collection (một List.of hoặc kết quả copyOf khác), List.copyOf KHÔNG copy mà trả về nguyên đối tượng đó. Điều này giúp tránh copy không cần thiết:
List<String> original = List.of("a", "b");
List<String> copy = List.copyOf(original);
System.out.println(original == copy); // true -- same object, no copy done
5. So sánh 3 cách
List.of(...) | List.copyOf(existing) | Collections.unmodifiableList(arr) | |
|---|---|---|---|
| Immutable thực sự? | Có | Có | Không (view) |
| Copy data? | Không (tạo mới từ args) | Có (defensive copy) | Không (wrap existing) |
| Ảnh hưởng từ list gốc? | N/A (không có list gốc) | Không (đã copy) | Có (thấy thay đổi) |
Cho phép null? | Không | Không | Có (nếu gốc có null) |
| Khi nào dùng | Tạo constant list/set/map trong code | Nhận input từ ngoài, giữ snapshot | Cần expose read-only view (vẫn sync với gốc) |
| Memory overhead | Thấp (compact internal) | O(n) copy | Gần như 0 (wrapper only) |
| Thread-safety | Safe (immutable) | Safe (immutable) | Không safe nếu gốc bị sửa từ thread khác |
6. Pitfall 1 — Shallow immutability: list immutable nhưng object bên trong vẫn mutable
List.of và List.copyOf ngăn thay đổi cấu trúc của list (thêm/xoá phần tử). Nhưng nếu phần tử bên trong là mutable object, nội dung của chúng vẫn thay đổi được:
List<int[]> list = List.of(new int[]{1, 2, 3}, new int[]{4, 5, 6});
list.add(new int[]{7}); // UnsupportedOperationException -- cau truc bi bao ve
list.get(0)[0] = 99; // OK -- mang int ben trong van mutable!
System.out.println(list.get(0)[0]); // 99 -- da bi thay doi
Đây là shallow immutability — chỉ immutable ở tầng cấu trúc list, không đảm bảo immutability của object bên trong.
Deep immutability yêu cầu tất cả object bên trong cũng phải immutable. Ví dụ List<String> với String là immutable → truly deep immutable. List<LocalDate> cũng vậy. Nhưng List<List<String>> inner lists vẫn mutable nếu không dùng List.of:
List<String> inner = new ArrayList<>(List.of("a", "b"));
List<List<String>> nested = List.of(inner);
nested.add(new ArrayList<>()); // UOE -- outer list bao ve
nested.get(0).add("c"); // OK -- inner list van mutable
Fix: wrap inner list cũng bằng List.of hoặc List.copyOf:
List<List<String>> nested = List.of(List.copyOf(inner));
nested.get(0).add("c"); // UOE -- inner list cung immutable
7. Pitfall 2 — Defensive copy khi nhận input
Pattern lỗi phổ biến nhất trong Java là nhận collection từ caller mà không copy:
// SAI -- caller van co the sua items sau khi truyen vao
public class ShoppingCart {
private final List<String> items;
public ShoppingCart(List<String> items) {
this.items = items; // alias bug
}
public List<String> getItems() {
return Collections.unmodifiableList(items); // khong du
}
}
Defensive copy là kỹ thuật tạo bản sao tại điểm nhận (constructor hoặc setter) để đứt quan hệ với caller:
// DUNG -- defensive copy + immutable
public class ShoppingCart {
private final List<String> items;
public ShoppingCart(List<String> items) {
this.items = List.copyOf(items); // copy + immutable ngay tai day
}
public List<String> getItems() {
return items; // da immutable, expose truc tiep la an toan
}
}
Với List.copyOf trong constructor:
- Bản copy độc lập tạo ra ngay lập tức — caller thay đổi list gốc về sau không ảnh hưởng.
- List đã immutable — không cần thêm
unmodifiableListkhi trả về. - Nếu caller truyền vào
List.of(...)(đã immutable),copyOfthông minh không copy lại.
Effective Java item 50 (Joshua Bloch, 3rd edition): "Make defensive copies when needed". Quy tắc: khi nhận mutable object từ bên ngoài vào class, tạo defensive copy ngay tại điểm nhận, trước khi validate. Validate sau copy để tránh TOCTOU race (Time-of-check Time-of-use) trong môi trường multi-thread.
8. Map.of, Map.entry, và giới hạn 10 cặp
Map.of có overload cho tối đa 10 cặp key-value (vì Java không có vararg type-safe cho 2 type khác nhau):
// Den 10 cap -- ok
Map<String, Integer> scores = Map.of(
"An", 95,
"Binh", 87,
"Cuong", 72
);
// Qua 10 cap -- dung Map.ofEntries + Map.entry
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("An", 95),
Map.entry("Binh", 87),
Map.entry("Cuong", 72),
Map.entry("Dung", 88),
Map.entry("Em", 90),
Map.entry("Phong", 76),
Map.entry("Giang", 83),
Map.entry("Hung", 91),
Map.entry("Lan", 79),
Map.entry("Minh", 85),
Map.entry("Nam", 94) // phan tu thu 11 -- ok voi ofEntries
);
Lưu ý quan trọng với Map.of:
- Không cho phép key trùng nhau — ném
IllegalArgumentExceptionngay khi tạo (không phải lúc lookup). - Thứ tự iteration của
Map.oflà không xác định (và cố tình random hóa giữa các JVM run để phát hiện code phụ thuộc thứ tự). Map.copyOf(existingMap)tương tựList.copyOf— tạo bản copy độc lập + immutable.
Map<String, Integer> mutable = new HashMap<>();
mutable.put("a", 1);
mutable.put("b", 2);
Map<String, Integer> immutable = Map.copyOf(mutable);
mutable.put("c", 3); // sua map goc
System.out.println(immutable.containsKey("c")); // false -- khong bi anh huong
9. Set.of và thứ tự không đảm bảo
Set<String> set = Set.of("banana", "apple", "cherry");
set.add("grape"); // UnsupportedOperationException
set.contains("apple"); // true
// Khong cho phep phan tu trung
Set<String> dup = Set.of("a", "b", "a"); // IllegalArgumentException!
Set.of cũng không cho phép null và ném IllegalArgumentException ngay khi tạo nếu có phần tử trùng.
10. Khi nào dùng unmodifiableList thay vì List.copyOf?
Mặc dù unmodifiableList có nguy cơ aliasing, nó vẫn có use case hợp lệ:
Dùng unmodifiableList khi:
- Bạn cần expose view read-only của một collection đang sống — caller cần thấy thay đổi mới nhất khi gốc cập nhật (ví dụ: cache nội bộ expose ra ngoài nhưng vẫn tự cập nhật theo giờ).
- Copy O(n) là quá tốn cho collection lớn và bạn chấp nhận view semantics.
Dùng List.copyOf khi:
- Bạn muốn snapshot tại thời điểm cụ thể — không bị ảnh hưởng bởi thay đổi về sau.
- Truyền collection ra ngoài class mà không muốn caller hay source thay đổi ảnh hưởng lẫn nhau.
Dùng List.of khi:
- Tạo constant/literal collection trong code — giá trị cố định không bao giờ thay đổi.
11. 📚 Deep Dive
- JEP 269 — "Convenience Factory Methods for Collections" (Java 9) — openjdk.org/jeps/269 — motivation, design decisions, và lý do giới hạn 10 cặp cho
Map.of. List.ofJavadoc SE 21 — docs.oracle.com/.../List.html#of(E...) — spec đầy đủ về null prohibition, immutability, serialization.Collections.unmodifiableListJavadoc — docs.oracle.com/.../Collections.html#unmodifiableList — quote rõ "changes to the returned list 'write through' to the specified list".- Effective Java item 17 (Joshua Bloch) — "Minimize mutability" — lý do tại sao immutable objects là mặc định tốt hơn trong OOP.
- Effective Java item 50 — "Make defensive copies when needed" — pattern, timing (copy trước validate), và TOCTOU vulnerability.
12. Self-check
Q1Vì sao `Collections.unmodifiableList` không đủ để bảo vệ state nội bộ của class?▸
Q2`List.of("a", "b", "c")` và `List.copyOf(List.of("a", "b", "c"))` — kết quả có phải cùng object không? Vì sao?▸
Q3Đoạn code sau có vấn đề gì?
List<List<String>> data = List.of(new ArrayList<>(List.of("a","b")));
data.get(0).add("c");▸
List<List<String>> data = List.of(new ArrayList<>(List.of("a","b"))); data.get(0).add("c");
Q4`Map.of("key1", 1, "key1", 2)` — compile error hay runtime exception? Vì sao?▸
Q5Bạn nhận `List<Order>` từ caller trong constructor. Khi nào nên validate input (kiểm tra null, size...) — trước hay sau khi `List.copyOf`? Vì sao?▸
Bài tiếp theo
Bài 09 sẽ đi sâu bounded type parameters và generic invariance — vì sao List<Integer> không phải subtype của List<Number> mặc dù Integer extends Number, và cách dùng wildcard để làm việc với hierarchy kiểu.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên