Immutable Collections — List.of và unmodifiableList
So sánh List.of, List.copyOf và unmodifiableList, cơ chế shallow immutability và defensive copy pattern trong hệ thống quản lý ấn phẩm thư viện.
TL;DR: Java cung cấp 3 cơ chế tạo tập hợp "không thể sửa đổi" với bản chất hoàn toàn khác nhau: Collections.unmodifiableList() chỉ là một lớp bọc view chỉ-đọc (nếu danh sách gốc bị thay đổi bởi ai đó còn giữ tham chiếu, view sẽ lập tức phản ánh sự thay đổi đó); trong khi List.of() (Java 9, JEP 269) và List.copyOf() (Java 10) là các tập hợp bất biến thực sự (True Immutable Collections) độc lập trong bộ nhớ. Để bảo vệ trạng thái của đối tượng nghiệp vụ, kỹ thuật sao chép phòng thủ (Defensive Copying) kết hợp với đối tượng bất biến sâu là bắt buộc.
1. Vì sao Collections.unmodifiableList() không phải là bất biến thực sự?
Trong hệ thống quản lý thư viện, ta xây dựng lớp CatalogArchive chứa danh mục ấn phẩm không cho phép người ngoài chỉnh sửa:
public class CatalogArchive {
private final List<String> bookIsbns;
public CatalogArchive(List<String> isbns) {
this.bookIsbns = isbns; // Directly holding client reference
}
public List<String> getBookIsbns() {
return Collections.unmodifiableList(this.bookIsbns);
}
}
Ở góc độ người gọi, phương thức getBookIsbns() trả về một unmodifiableList. Nếu gọi add() hay remove(), JVM sẽ lập tức ném ra lỗi UnsupportedOperationException. Tuy nhiên, lỗ hổng Aliasing Bug vẫn tồn tại:
List<String> rawList = new ArrayList<>(List.of("ISBN-01", "ISBN-02"));
CatalogArchive archive = new CatalogArchive(rawList);
System.out.println(archive.getBookIsbns()); // [ISBN-01, ISBN-02]
// Caller modifies the original list via external reference:
rawList.add("ISBN-03");
System.out.println(archive.getBookIsbns()); // [ISBN-01, ISBN-02, ISBN-03] -- Changed!
Dù CatalogArchive không bao giờ chủ động thay đổi dữ liệu, danh mục bên trong vẫn bị biến đổi vì người gọi vẫn nắm giữ tham chiếu mảng gốc.

Collections.unmodifiableList chỉ là một chiếc kính nhìn một chiều (Read-Only View) bọc quanh mảng gốc. Nó bảo đảm "bạn không thể sửa mảng qua chiếc kính này", chứ hoàn toàn không bảo đảm "không ai khác đang sửa mảng ở phía bên kia bức tường".
2. List.of() (Java 9 JEP 269) tạo collection bất biến thực sự ra sao?
Ra đời từ Java 9, factory methods List.of(), Set.of(), Map.of() tạo ra các tập hợp bất biến thực sự (True Immutable Collections):
List<String> fixedGenres = List.of("Computer Science", "Mathematics", "Physics");
fixedGenres.add("Biology"); // Throws UnsupportedOperationException!
fixedGenres.set(0, "AI"); // Throws UnsupportedOperationException!
Các đặc tính cốt lõi của List.of()
- Bất biến thực sự: Không hề có một mảng mutable ngầm nào của caller nằm phía dưới.
- Cấm phần tử
null: NémNullPointerExceptionngay tại thời điểm tạo nếu gặpnull, bảo đảm thiết kế an toàn fail-fast. - Cấu trúc bộ nhớ siêu tinh gọn (Compact Representation): Thay vì bọc một
ArrayListcồng kềnh, OpenJDK triển khai các lớp nội bộ chuyên biệt:List.of(e1)-> Thể hiện củaList12(chỉ chứa đúng 1 trường tham chiếu, không tốn mảng).List.of(e1, e2)-> Thể hiện củaList12(chứa 2 trường).List.of(e1, ..., eN)-> Thể hiện củaListN(sử dụng mảng cố định không bao giờ resize).
3. List.copyOf() (Java 10) tối ưu hóa Defensive Copying thế nào?
Phương thức List.copyOf(collection) (bổ sung từ Java 10) nhận vào một tập hợp bất kỳ, sao chép độc lập dữ liệu và trả về một tập hợp bất biến:
List<String> mutable = new ArrayList<>(List.of("B1", "B2"));
List<String> safeSnapshot = List.copyOf(mutable);
mutable.add("B3"); // Does not affect safeSnapshot
System.out.println(safeSnapshot); // [B1, B2]
Cơ chế nhận diện thông minh (Smart Re-use)
Nếu đối số truyền vào List.copyOf() vốn dĩ đã là một tập hợp bất biến do List.of() hoặc List.copyOf() tạo ra, JVM sẽ không sao chép lại mảng mà trả về chính tham chiếu đó:
List<String> immutable1 = List.of("A", "B");
List<String> immutable2 = List.copyOf(immutable1);
System.out.println(immutable1 == immutable2); // true! Same instance in memory
Tính năng này giúp bạn thoải mái áp dụng phòng thủ ở mọi tầng kiến trúc mà không lo ngại chi phí lãng phí bộ nhớ hay suy giảm hiệu năng.
4. Bảng so sánh 3 cơ chế bất biến trong Java có gì khác biệt?
| Tiêu chí | Collections.unmodifiableList | List.of(...) | List.copyOf(...) |
|---|---|---|---|
| Thời điểm xuất hiện | Java 1.2 | Java 9 (JEP 269) | Java 10 |
| Bản chất lưu trữ | Lớp bọc mỏng (Wrapper View) | Instance bất biến nội bộ (ListN) | Instance bất biến nội bộ (ListN) |
| Sao chép dữ liệu | Không (Trỏ thẳng vào mảng cũ) | Không có mảng ngoài | Có sao chép (nếu nguồn mutable) |
| Bị ảnh hưởng khi mảng gốc đổi | CÓ (Nguy cơ Aliasing Bug) | Không có mảng gốc | KHÔNG (Độc lập hoàn toàn) |
Chấp nhận giá trị null | Tùy thuộc vào list gốc | CẤM (Ném NPE) | CẤM (Ném NPE) |
| An toàn đa luồng | Phụ thuộc vào luồng ghi mảng gốc | An toàn tuyệt đối | An toàn tuyệt đối |
5. Shallow vs Deep Immutability: Đối tượng bên trong có an toàn không?
Một quan niệm sai lầm rất phổ biến là: "Khi bọc danh sách vào List.of(), toàn bộ dữ liệu bên trong sẽ trở nên bất biến". Trên thực tế, Java Collections chỉ bảo đảm Shallow Immutability (Bất biến nông):
public class Author {
private String name;
public Author(String name) { this.name = name; }
public void setName(String name) { this.name = name; }
public String getName() { return name; }
}
Author author = new Author("Uncle Bob");
List<Author> authors = List.of(author);
// List structure is protected:
// authors.add(new Author("Martin Fowler")); // UnsupportedOperationException
// But internal state of element object can still mutate:
authors.get(0).setName("Robert C. Martin"); // Mutates internal state!
List.of chỉ bảo đảm số lượng phần tử và các địa chỉ tham chiếu không đổi. Nếu bản thân các đối tượng phần tử là Mutable Object, bất kỳ ai có tham chiếu đến đối tượng đó vẫn có thể làm biến đổi dữ liệu. Để đạt được Deep Immutability, các đối tượng phần tử phải là record hoặc lớp bất biến với các trường final.
6. Mẫu thiết kế Defensive Copying chuẩn mực bảo vệ dữ liệu ra sao?
Để bảo vệ toàn diện trạng thái của một lớp nghiệp vụ (như CatalogEntry), ta cần thực hiện sao chép phòng vệ ở cả hai đầu: Constructor (chặn caller sửa từ bên ngoài) và Getter (chặn caller sửa kết quả trả về):
public final class CatalogEntry {
private final String barcode;
private final List<String> tags;
public CatalogEntry(String barcode, List<String> tags) {
this.barcode = barcode;
// Defensive copy on input:
this.tags = List.copyOf(tags);
}
public String getBarcode() {
return barcode;
}
public List<String> getTags() {
// Safe: tags is already a true immutable list
return tags;
}
}
Bằng cách áp dụng List.copyOf() ngay trong constructor:
- Cho dù caller truyền vào một
ArrayListcó thể thay đổi và sau đó cố tình gọiadd(),CatalogEntryvẫn hoàn toàn miễn nhiễm. - Phương thức getter có thể trả về trực tiếp
tagsmà không cần bọc lại hay sao chép thêm lần nào nữa.
7. Đọc thêm từ tài liệu Oracle và các liên kết liên quan
- JEP 269: Convenience Factory Methods for Collections: Động lực ra đời của
List.of,Set.of,Map.of. - Oracle JavaDoc: List.copyOf: Chi tiết thuật toán tái sử dụng instance.
- Effective Java (Joshua Bloch), Item 50: Make defensive copies when needed.
- Bài 07 — Comparator và Comparable: Kỹ thuật so sánh không làm biến đổi dữ liệu.
8. Tóm tắt các điểm mấu chốt
Collections.unmodifiableListchỉ là một góc nhìn chỉ-đọc, dữ liệu bên dưới vẫn có thể bị biến đổi thông qua tham chiếu gốc.List.ofvàList.copyOfcung cấp các tập hợp bất biến thực sự, tối ưu hóa bộ nhớ và an toàn tuyệt đối trước sửa đổi bên ngoài.- Các tập hợp bất biến của Java cấm phần tử
nullvà némUnsupportedOperationExceptioncho mọi thao tác ghi. - Java Collections chỉ bảo đảm Shallow Immutability; cần kết hợp với
recordhoặc immutable class để đạt tính bất biến sâu. - Áp dụng
List.copyOftrong constructor là phương thức chuẩn mực để thực hiện Defensive Copying trong Java hiện đại.
9. Câu hỏi tự kiểm tra
- Q1Vì sao gọi Collections.unmodifiableList() không bảo đảm danh sách sẽ không bao giờ bị thay đổi?
- Q2Điều gì xảy ra khi truyền null vào phương thức List.of() hoặc List.copyOf()?
- Q3Phương thức List.copyOf() xử lý như thế nào khi đối số truyền vào đã là một danh sách bất biến được tạo bởi List.of()?
- Q4Khái niệm Shallow Immutability trong List.of() có ý nghĩa gì đối với việc bảo vệ dữ liệu?
- Q5Mẫu thiết kế Defensive Copying bảo vệ tính đóng gói của đối tượng bằng cách nào?
Bài tiếp theo: Bounded Types và Invariance — biên trên, biên dưới và generic methods
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Comparator và Comparable — thứ tự sắp xếp và hợp đồng so sánh
Bài tiếpBounded Types và Generic Invariance trong Java
Lộ trình Junior Java Backend Developer
Chặng 5/21 · Generics & Collections — bài 8/10 của chặng
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
Thảo luận & góp ý
Hỏi đáp về bài này