Java OO & Functional
24/38
Bài 24 / 38~12 phútGenerics & CollectionsLộ trình · chặng 05/21Miễn phí… lượt xem

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:

Java
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:

Java
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.

Năm bước cho thấy unmodifiableList chỉ bọc chứ không chép mảng của caller, nên caller còn giữ tham chiếu gốc vẫn thêm được phần tử, trong khi List.copyOf chép sang vùng riêng

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):

Java
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()

  1. 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.
  2. Cấm phần tử null: Ném NullPointerException ngay tại thời điểm tạo nếu gặp null, bảo đảm thiết kế an toàn fail-fast.
  3. Cấu trúc bộ nhớ siêu tinh gọn (Compact Representation): Thay vì bọc một ArrayList cồ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ủa List12 (chỉ chứa đúng 1 trường tham chiếu, không tốn mảng).
    • List.of(e1, e2) -> Thể hiện của List12 (chứa 2 trường).
    • List.of(e1, ..., eN) -> Thể hiện của ListN (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:

Java
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 đó:

Java
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.unmodifiableListList.of(...)List.copyOf(...)
Thời điểm xuất hiệnJava 1.2Java 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ệuKhông (Trỏ thẳng vào mảng cũ)Không có mảng ngoàiCó sao chép (nếu nguồn mutable)
Bị ảnh hưởng khi mảng gốc đổiCÓ (Nguy cơ Aliasing Bug)Không có mảng gốcKHÔNG (Độc lập hoàn toàn)
Chấp nhận giá trị nullTùy thuộc vào list gốcCẤM (Ném NPE)CẤM (Ném NPE)
An toàn đa luồngPhụ thuộc vào luồng ghi mảng gốcAn toàn tuyệt đốiAn 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):

Java
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ề):

Java
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:

  1. Cho dù caller truyền vào một ArrayList có thể thay đổi và sau đó cố tình gọi add(), CatalogEntry vẫn hoàn toàn miễn nhiễm.
  2. Phương thức getter có thể trả về trực tiếp tags mà 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

Tài liệu tham khảo chuyên sâu

8. Tóm tắt các điểm mấu chốt

  • Collections.unmodifiableList chỉ 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.of và List.copyOf cung 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ử null và ném UnsupportedOperationException cho mọi thao tác ghi.
  • Java Collections chỉ bảo đảm Shallow Immutability; cần kết hợp với record hoặc immutable class để đạt tính bất biến sâu.
  • Áp dụng List.copyOf trong 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

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao gọi Collections.unmodifiableList() không bảo đảm danh sách sẽ không bao giờ bị thay đổi?
  2. Q2
    Điều gì xảy ra khi truyền null vào phương thức List.of() hoặc List.copyOf()?
  3. Q3
    Phươ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()?
  4. Q4
    Khái niệm Shallow Immutability trong List.of() có ý nghĩa gì đối với việc bảo vệ dữ liệu?
  5. Q5
    Mẫ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.

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

Bài tiếp theo

Bounded Types và Generic Invariance trong Java