1. Giới thiệu
Nói một cách đơn giản, mutable-collection là thứ bạn có thể thay đổi sau khi tạo: thêm, xóa và sửa phần tử. Bộ sưu tập immutable (bất biến) là bộ sưu tập không thể thay đổi sau khi đã tạo. Giống như bê tông sau khi đông cứng: có thể nhìn, có thể chạm, nhưng không thể nặn thêm hình mới nữa.
Bộ sưu tập mutable giống như một cuốn sổ với bút chì: bạn viết, tẩy, thêm ghi chú mới. Bộ sưu tập immutable giống như một trang đã được ép plastic: giờ không ai có thể thêm hay xóa gì nữa.
Ví dụ về các bộ sưu tập có thể thay đổi (mutable)
Trong Java, hầu hết các collection chuẩn theo mặc định đều có thể thay đổi. Ví dụ như các lớp:
- ArrayList
- LinkedList
- HashSet
- TreeSet
- HashMap
- LinkedHashMap
- và nhiều lớp khác
Ví dụ: ArrayList
import java.util.*;
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.set(1, "Charlie"); // thay Bob bằng Charlie
names.remove("Alice"); // xóa Alice
System.out.println(names); // [Charlie]
Ở đây ta có thể làm mọi thứ với collection: thêm, xóa, hoán đổi phần tử. Điều này tiện khi collection được xây dựng động, ví dụ trong quá trình đọc dữ liệu từ file hoặc nhập từ người dùng.
2. Ví dụ về các bộ sưu tập bất biến (immutable)
Những “anh bạn” này xuất hiện từ Java 9, có lẽ bạn đã gặp, nhưng vẫn cần làm quen thêm:
- List.of(...)
- Set.of(...)
- Map.of(...)
- List.copyOf(collection)
- Set.copyOf(collection)
- Map.copyOf(map)
Ví dụ: List.of
List<String> planets = List.of("Mercury", "Venus", "Earth", "Mars");
System.out.println(planets); // [Mercury, Venus, Earth, Mars]
planets.add("Jupiter"); // Ném UnsupportedOperationException!
Thử thay đổi collection sẽ dẫn đến ngoại lệ lúc chạy.
Ví dụ: Collections.unmodifiableList
List<String> modifiable = new ArrayList<>(List.of("a", "b"));
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
unmodifiable.add("c"); // UnsupportedOperationException!
Nhưng có một điểm lắt léo: nếu bạn thay đổi collection gốc, lớp bọc cũng sẽ thay đổi!
modifiable.add("c");
System.out.println(unmodifiable); // [a, b, c] — phần tử đã xuất hiện!
3. Khác biệt chính giữa các collection mutable và immutable
| Thuộc tính | Mutable (có thể thay đổi) | Immutable (bất biến) |
|---|---|---|
| Có thể thêm phần tử không? | Có | Không |
| Có thể xóa phần tử không? | Có | Không |
| Có thể sửa phần tử không? | Có (ví dụ, set) | Không |
| An toàn luồng | Không (theo mặc định) | Có (không có trạng thái — không có gì để thay đổi) |
| Có thể thêm null? | Có (thường là vậy) | Không (trong các phương thức factory của Java 9+) |
| Triển khai | ArrayList, HashSet, v.v. | List.of, Set.of, Map.of, copyOf |
4. Tại sao lại cần các collection bất biến?
Câu hỏi đặt ra: nếu collection có thể thay đổi linh hoạt như vậy, tại sao ta cần collection bất biến? Có nhiều lý do, tất cả đều liên quan đến độ an toàn, tính dễ đọc và tính dự đoán của mã.
An toàn và chống lỗi
Khi bạn trả một collection ra bên ngoài (ví dụ từ một phương thức hay lớp), bạn muốn chắc chắn rằng không ai vô tình thay đổi nội dung của nó. Điều này đặc biệt quan trọng nếu collection chứa “quan trọng” dữ liệu, vốn không được phép thay đổi sau khi khởi tạo.
Ví dụ:
public class Team {
private final List<String> players;
public Team(List<String> players) {
// Tạo một bản sao bất biến để không ai có thể thay đổi đội hình
this.players = List.copyOf(players);
}
public List<String> getPlayers() {
return players;
}
}
Giờ thì bất kỳ mã nào nhận danh sách người chơi cũng không thể thêm “bạn” của mình vào đó.
An toàn luồng
Các collection có thể thay đổi không an toàn khi truy cập đồng thời từ nhiều luồng. Ngược lại, collection bất biến có thể truyền giữa các luồng một cách tự do — không ai có thể làm hỏng chúng.
Đơn giản hóa gỡ lỗi
Nếu collection không thay đổi, bạn luôn biết bên trong có gì. Không phải lo sợ ai đó đã “lặng lẽ” sửa nó ở nơi khác trong mã.
Sử dụng làm khóa hoặc giá trị trong các collection khác
Các đối tượng bất biến là lựa chọn lý tưởng để làm khóa trong Map hoặc phần tử trong Set. Nếu đối tượng có thể thay đổi sau khi thêm, bạn có nguy cơ mất khả năng truy cập tới nó (xem hashCode và equals).
5. Khi nào nên dùng collection có thể thay đổi?
Collection có thể thay đổi phù hợp khi:
- Collection được xây dựng theo từng bước, trong vòng lặp hoặc từ nhiều nguồn.
- Cần thay đổi thường xuyên: thêm, xóa, sắp xếp.
- Collection chỉ dùng nội bộ và không ai “bên ngoài” có thể làm hỏng nó.
Ví dụ: Xây dựng danh sách
List<String> shoppingList = new ArrayList<>();
shoppingList.add("Sữa");
shoppingList.add("Bánh mì");
shoppingList.add("Táo");
// Sau khi xây dựng — có thể tạo phiên bản bất biến
List<String> finalList = List.copyOf(shoppingList);
6. Khi nào nên dùng collection bất biến?
- Để lưu trữ dữ liệu hằng (ví dụ, danh sách các ngày trong tuần).
- Để truyền collection giữa các tầng của ứng dụng (ví dụ, từ DAO sang service).
- Để trả về collection từ phương thức nhằm bảo vệ khỏi bị chỉnh sửa.
- Cho các kịch bản đa luồng, nơi an toàn là quan trọng.
Ví dụ: Dữ liệu hằng
public static final List<String> WEEKDAYS = List.of(
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
);
Ví dụ: Trả ra bên ngoài
public List<String> getReadOnlyNames() {
return List.copyOf(names); // không ai có thể thay đổi danh sách
}
7. Đặc điểm và cạm bẫy
Bất biến ≠ An toàn luồng
Collection bất biến được bảo vệ khỏi việc bị thay đổi, nhưng không có nghĩa là nó được bảo vệ khỏi các vấn đề khác trong môi trường đa luồng (ví dụ, nếu các phần tử trong collection là đối tượng có thể thay đổi).
List<List<String>> listOfLists = List.of(new ArrayList<>());
listOfLists.get(0).add("Oops!"); // Có thể thay đổi danh sách bên trong!
Wrapper so với bản sao
Như đã bàn ở trên, Collections.unmodifiableList là một lớp bọc; nếu collection gốc thay đổi, lớp bọc cũng thay đổi. Còn List.copyOf tạo ra một bản sao độc lập thực sự.
List<String> base = new ArrayList<>(List.of("a", "b"));
List<String> wrap = Collections.unmodifiableList(base);
List<String> copy = List.copyOf(base);
base.add("c");
System.out.println(wrap); // [a, b, c] — đã thay đổi!
System.out.println(copy); // [a, b] — vẫn như cũ!
NullPointerException
Các phương thức factory (List.of, Set.of, Map.of) không cho phép thêm null:
List<String> bad = List.of("a", null); // Ném NullPointerException ngay khi tạo!
8. So sánh cách tiếp cận: ưu và nhược điểm
Collection có thể thay đổi
Ưu điểm lớn nhất của collection có thể thay đổi là tính linh hoạt. Bạn có thể thêm, xóa phần tử ngay trong lúc chạy, tái cấu trúc collection khi logic chương trình tiến triển. Cách tiếp cận này đặc biệt tiện khi cần nhanh chóng dựng cấu trúc tạm thời hoặc thay đổi động.
Nhưng sự tiện lợi phải trả giá. Collection có thể thay đổi luôn tiềm ẩn rủi ro bị can thiệp ngẫu nhiên: ai đó trong mã có thể vô tình sửa dữ liệu, dẫn đến lỗi khó lần ra. Trong chương trình đa luồng, tình hình còn tệ hơn — các thay đổi song song dễ dẫn tới race condition và sự cố bất ngờ. Việc kiểm soát vòng đời của dữ liệu kiểu này cũng khó hơn: phải luôn nhớ ai và khi nào có thể thay đổi collection.
Collection bất biến
Làm việc với collection bất biến sẽ yên tâm hơn. Bạn biết chắc: không ai “lén” sửa chúng. Điều này làm mã an toàn hơn, dễ gỡ lỗi và kiểm thử hơn, và bản thân các collection có thể truyền giữa các luồng mà không cần khóa bổ sung.
Nhược điểm là đôi khi phải hy sinh hiệu năng. Nếu cần thêm phần tử mới, bạn phải tạo một bản sao collection mới, tốn thêm bộ nhớ và thời gian. Ngoài ra, việc dựng ngay các cấu trúc lớn và phức tạp ở dạng bất biến thường bất tiện. Thông thường, người ta xây dựng trong một collection tạm thời có thể thay đổi, và khi xong thì “đóng băng”.
9. Các lỗi thường gặp
Lỗi 1: Trả ra ngoài một collection có thể thay đổi. Nếu bạn trả về từ phương thức một ArrayList thông thường, bất kỳ mã bên ngoài nào cũng có thể thêm hoặc xóa phần tử. Điều này có thể dẫn tới lỗi rất khó lần theo.
Lỗi 2: Dùng lớp bọc thay vì bản sao. Nếu bạn sử dụng Collections.unmodifiableList, nhưng collection gốc vẫn thay đổi ở nơi khác, “tính bất biến” — chỉ là ảo tưởng.
Lỗi 3: Đối tượng có thể thay đổi bên trong immutable-collection. Ngay cả khi bản thân collection là bất biến, các phần tử của nó vẫn có thể là đối tượng có thể thay đổi. Điều này có thể dẫn đến những thay đổi trạng thái ngoài ý muốn.
Lỗi 4: Cố gắng thêm null vào collection được tạo qua các phương thức factory. Không giống các collection kiểu cũ, các phương thức factory mới không cho phép thêm null — bạn sẽ nhận NullPointerException.
Lỗi 5: Kỳ vọng một hiện thực cụ thể. Các collection được tạo qua List.of hoặc Set.of không đảm bảo kiểu hiện thực (không nhất thiết là ArrayList hay HashSet). Đừng dựa dẫm vào điều đó.
GO TO FULL VERSION