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 11, 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 (được tạo bởi các factory method như List.of(), List.copyOf(), v.v.), List.copyOf KHÔNG copy mà trả về chính xác tham chiếu của đối tượng đó.
Cơ chế hoạt động bên dưới của JDK:
- JDK thực hiện dynamic type checking (kiểm tra kiểu động) ở thời điểm runtime.
- Nó kiểm tra xem collection truyền vào có phải instance của một abstract class nội bộ của JDK hay không (cụ thể là
java.util.ImmutableCollections.AbstractImmutableList— kế thừaAbstractImmutableCollection; mọi list immutable tạo từ factory đều extends class này). - Nếu đối tượng truyền vào thỏa mãn điều kiện (tức là đã bất biến sẵn), JDK sẽ cast kiểu trực tiếp và trả về chính tham chiếu đó (
return (List<E>) coll;) với độ phức tạp thời gian là O(1) mà không cần tốn bất kỳ chi phí phân bổ bộ nhớ (memory allocation) hay sao chép mảng (array copying) nào. - Nếu không thỏa mãn, JDK mới thực hiện defensive copy thông thường bằng cách copy các phần tử sang một mảng mới.
List<String> original = List.of("a", "b");
List<String> copy = List.copyOf(original);
System.out.println(original == copy); // true -- same object, no copy done (O(1) complexity!)
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".
Trong môi trường đa luồng (multi-threaded), nếu bạn thực hiện kiểm tra tính hợp lệ (validate) trước rồi mới tiến hành copy (hoặc chỉ gán reference dùng Collections.unmodifiableList), bạn sẽ mở ra một lỗ hổng bảo mật nghiêm trọng gọi là TOCTOU (Time-of-check Time-of-use).
Sơ đồ chuỗi hành động TOCTOU (T1 -> T2 -> T3)
Dưới đây là kịch bản lỗi đồng thời xảy ra khi Thread 1 (Constructor) và Thread 2 (Caller hoặc thread khác) cùng truy cập vào danh sách gốc:
sequenceDiagram
autonumber
actor T1 as Thread 1 (Constructor / Owner)
actor T2 as Thread 2 (Caller / Concurrent Thread)
Note over T1: Nhận Mutable List gốc từ caller
T1->>T1: [T1 - Time of Check]: Validate list<br/>(Kiểm tra không chứa phần tử null - OK!)
Note over T2: Đang nắm giữ reference tới List gốc
T2->>T2: [T2 - Mutate State]: Sửa đổi dữ liệu list gốc<br/>(Chèn thêm null hoặc dữ liệu độc hại)
T1->>T1: [T3 - Time of Use]: Lưu/Gán reference<br/>(this.items = list hoặc wrapper unmodifiableList)
Note over T1: HỆ QUẢ:<br/>Object bên trong Constructor chứa phần tử bẩn (null)<br/>dù đã kiểm tra hợp lệ thành công trước đó!Tại sao giải pháp phải là "Copy trước, Validate sau"?
Để ngăn chặn hoàn toàn lỗ hổng TOCTOU, quy trình bắt buộc phải đảo ngược:
- Copy trước: Ngay khi nhận được tham chiếu từ bên ngoài, lập tức thực hiện copy dữ liệu (
List.copyOfhoặc tạo bản copy mới) sang một vùng nhớ độc lập mà chỉ đối tượng của bạn nắm giữ. - Validate sau: Thực hiện mọi kiểm tra tính hợp lệ trên bản sao mới này chứ không phải trên collection gốc do caller truyền vào.
Vì bản sao này đã được cô lập hoàn toàn, không có thread bên ngoài nào có thể thay đổi dữ liệu của nó trong khoảng thời gian giữa "check" và "use", đảm bảo tuyệt đối an toàn và tính toàn vẹn dữ liệu.
8. Map.of, Map.entry, và giới hạn 10 cặp
Map.of có các overload cụ thể cho tối đa 10 cặp key-value. Tại sao JDK lại thiết kế nạp chồng 10 method cụ thể thay vì chỉ dùng một method varargs?
Lý do thiết kế (Design Decisions):
- Tránh Array Allocation Overhead (Quá tải phân bổ mảng): Trong Java, cơ chế varargs (
Type... args) bản chất hoạt động bằng cách yêu cầu compiler tự động khởi tạo một mảng mới (new Type[]) để bọc các đối số truyền vào tại thời điểm gọi phương thức. Đối với các Map nhỏ (từ 1 đến 10 phần tử - chiếm phần lớn trường hợp tạo map trực tiếp trong code), việc gọi liên tục varargs sẽ liên tục phân bổ các mảng tạm thời trên vùng nhớ Heap, làm tăng áp lực dọn rác đáng kể cho Garbage Collector (GC). Overload cụ thể từ 1 đến 10 tham số giúp tránh hoàn toàn việc phân bổ mảng vô tội vạ này. - Tránh Lỗi/Cảnh báo Varargs kiểu Generic (Heap Pollution): Java generics bị xóa kiểu (type erasure) khi runtime. Nếu dùng varargs với kiểu generic (ví dụ:
Map.Entry<K, V>...), compiler sẽ không thể đảm bảo an toàn kiểu dữ liệu tuyệt đối ở mức runtime khi khởi tạo mảng generic, dẫn đến hiện tượng Heap Pollution (ô nhiễm vùng nhớ heap). Điều này ép lập trình viên phải sử dụng annotation@SafeVarargshoặc chịu đựng các cảnh báouncheckedphiền phức. Việc nạp chồng các tham số cụ thể loại bỏ hoàn toàn rủi ro này.
// Den 10 cap -- dung Map.of (cuc ky type-safe va toi uu memory)
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