1. Vấn đề tính có thể thay đổi của bộ sưu tập
Trong Java, bộ sưu tập giống như một kho hàng: ai cũng có thể đến và thêm, bớt, thay đổi thứ gì đó. Đôi khi điều này tiện lợi, nhưng trong các chương trình lớn, nó trở thành cơn đau đầu. Hãy tưởng tượng bạn chuyển danh sách hàng hóa từ lớp của mình ra bên ngoài, và ai đó xóa một nửa mục. Hoặc — thú vị hơn — trong một chương trình đa luồng, một luồng thêm phần tử còn luồng khác đọc: kết quả có thể bất ngờ, và lỗi thì khó lần ra (ví dụ, ConcurrentModificationException).
Đây là ví dụ vì sao tính có thể thay đổi của bộ sưu tập là nguồn gốc lỗi:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Trà");
products.add("Cà phê");
}
public List<String> getProducts() {
// NGUY HIỂM! Trả về tham chiếu tới danh sách nội bộ
return products;
}
}
public class Main {
public static void main(String[] args) {
Inventory inv = new Inventory();
List<String> external = inv.getProducts();
external.remove("Trà"); // Ôi! Bây giờ trong kho không còn trà
System.out.println(inv.getProducts()); // [Cà phê]
}
}
Bạn thấy vấn đề chứ? Một phương thức trả về bộ sưu tập nội bộ, phương thức khác lại thay đổi nó. Như vậy có thể vô tình phá hủy dữ liệu lẽ ra phải được bảo vệ.
2. Tạo bộ sưu tập bất biến: Collections.unmodifiable*
Để tránh những tình huống như vậy, Java cung cấp cơ chế bảo vệ hiệu quả: có thể biến bộ sưu tập thành “bất biến” bằng các lớp bọc đặc biệt từ lớp Collections:
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
Cách hoạt động thế nào? Trước hết, bạn tạo một bộ sưu tập thông thường, sau đó bọc nó trong một “lớp vỏ” bất biến:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Trà");
drinks.add("Cà phê");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
System.out.println(immutableDrinks); // [Trà, Cà phê]
// Thử thêm phần tử
immutableDrinks.add("Ca cao"); // Bùm! UnsupportedOperationException
}
}
Cố gắng sửa đổi bộ sưu tập như vậy sẽ ném ra ngoại lệ UnsupportedOperationException. Tưởng tượng bạn dán lên hộp một nhãn khổng lồ "KHÔNG ĐƯỢC ĐỤNG!" — ai cố thêm hoặc xóa thứ gì sẽ “ăn đòn” (hoặc “ăn” stack trace).
Ví dụ: bảo vệ trạng thái nội bộ
Hãy sửa lại lớp Inventory ở ví dụ trước:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Trà");
products.add("Cà phê");
}
public List<String> getProducts() {
// Giờ trả về lớp bọc
return Collections.unmodifiableList(products);
}
}
Bây giờ, nếu ai đó cố thay đổi danh sách nhận được, họ sẽ nhận ngoại lệ.
3. Hành vi của bộ sưu tập bất biến: bảo vệ ở mức bề mặt
Quan trọng: unmodifiableList và “anh em” của nó chỉ tạo lớp bọc quanh bộ sưu tập gốc. Chúng không tạo bản sao — mọi thay đổi với bộ sưu tập gốc (bên “trong”) sẽ hiển thị trong lớp bọc!
Minh họa
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Trà");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
drinks.add("Cà phê"); // Thay đổi bộ sưu tập gốc
System.out.println(immutableDrinks); // [Trà, Cà phê] — phần tử đã xuất hiện!
}
}
Kết luận: lớp bọc chỉ bảo vệ khỏi việc sửa đổi thông qua chính lớp bọc. Nếu ai đó giữ tham chiếu tới bộ sưu tập gốc, họ vẫn có thể thay đổi nó.
4. Tính bất biến sâu: huyền thoại và thực tế
Các lớp bọc unmodifiable* chỉ làm bộ sưu tập bất biến từ bên ngoài. Nhưng nếu bộ sưu tập chứa các đối tượng có thể thay đổi, bạn vẫn có thể thay đổi chúng!
Ví dụ
import java.util.*;
class Product {
String name;
Product(String name) {
this.name = name;
}
public String toString() {
return name;
}
}
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Trà"));
List<Product> immutableProducts = Collections.unmodifiableList(products);
// Thay đổi đối tượng bên trong bộ sưu tập
immutableProducts.get(0).name = "Cà phê";
System.out.println(immutableProducts); // [Cà phê]
}
}
Kết luận:
- Bộ sưu tập là “bất biến”, nhưng các đối tượng bên trong thì không.
- Để đạt bất biến hoàn toàn (sâu), hãy dùng các đối tượng bất biến (ví dụ, String, Integer, record-classes hoặc tự thiết kế lớp của bạn là immutable).
5. Khi nào nên dùng bộ sưu tập bất biến
Để bảo vệ trạng thái nội bộ
Nếu bạn viết một lớp lưu trữ bộ sưu tập và trả nó ra bên ngoài, hãy luôn trả về lớp bọc để không ai có thể vô tình (hoặc cố ý) sửa đổi dữ liệu của bạn:
public List<String> getProducts() {
return Collections.unmodifiableList(products);
}
Trong các chương trình đa luồng
Trong ứng dụng đa luồng, các bộ sưu tập có thể thay đổi là nguồn rắc rối (race condition, ConcurrentModificationException và những “niềm vui” khác). Nếu bộ sưu tập không cần thay đổi sau khi tạo — hãy biến nó thành bất biến.
Để truyền dữ liệu giữa các tầng
Nếu bạn truyền bộ sưu tập từ tầng này sang tầng khác (ví dụ, từ DAO tới service), hãy truyền bản sao bất biến hoặc lớp bọc — điều này giúp tránh các thay đổi vô tình.
6. Ví dụ thực tế
Ví dụ 1: bảo vệ danh sách sinh viên
import java.util.*;
public class Group {
private final List<String> students = new ArrayList<>();
public void addStudent(String name) {
students.add(name);
}
public List<String> getStudents() {
return Collections.unmodifiableList(students);
}
}
Giờ không ai có thể thêm hoặc xóa sinh viên trực tiếp thông qua getStudents().
Ví dụ 2: Map bất biến
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> grades = new HashMap<>();
grades.put("Vasya", 5);
grades.put("Masha", 4);
Map<String, Integer> immutableGrades = Collections.unmodifiableMap(grades);
// immutableGrades.put("Petya", 3); // UnsupportedOperationException
}
}
7. Những điểm tinh tế hữu ích
Lựa chọn hiện đại: List.of, Set.of, Map.of
Từ Java 9, có những cách tiện lợi hơn để tạo bộ sưu tập bất biến:
List<String> drinks = List.of("Trà", "Cà phê");
Set<String> fruits = Set.of("Táo", "Chuối");
Map<String, Integer> ages = Map.of("Vasya", 20, "Masha", 21);
- Những bộ sưu tập này là bất biến (mọi nỗ lực sửa đổi — sẽ ném ngoại lệ).
- Chúng không có “bộ sưu tập gốc” có thể thay đổi (khác với Collections.unmodifiable*).
- Không cho phép giá trị null.
Ngoài ra từ Java 10 có các phương thức sao chép: List.copyOf, Set.copyOf, Map.copyOf — tạo bản sao bất biến của bộ sưu tập được truyền vào.
So sánh cách tạo bộ sưu tập bất biến
| Cách | Bất biến sâu | Có thể thay đổi bộ sưu tập gốc không? | Cho phép null? | Phiên bản Java |
|---|---|---|---|---|
|
Không | Có | Có | 1.2 |
|
Không | Không (không có bộ sưu tập gốc) | Không | 9+ |
8. Các lỗi điển hình khi làm việc với bộ sưu tập bất biến
Lỗi số 1: Thay đổi bộ sưu tập gốc sau khi tạo lớp bọc. Bạn đã tạo unmodifiableList, rồi ai đó thay đổi danh sách gốc. Lớp bọc sẽ không bảo vệ khỏi điều này — thay đổi sẽ hiển thị ở mọi nơi dùng lớp bọc.
Lỗi số 2: Kỳ vọng tính bất biến sâu. Nhiều người nghĩ nếu bộ sưu tập bất biến thì các đối tượng bên trong cũng không thể thay đổi. Thực tế chỉ cấu trúc được bảo vệ (thêm/xóa/sửa thông qua bộ sưu tập), còn nội dung đối tượng thì không.
Lỗi số 3: Dùng các bộ sưu tập bất biến với giá trị null trong các factory hiện đại. Các bộ sưu tập tạo qua List.of, Set.of, Map.of không cho phép null. Cố gắng thêm hoặc nhận null sẽ dẫn tới ngoại lệ.
Lỗi số 4: Trả tham chiếu tới các bộ sưu tập có thể thay đổi ra bên ngoài. Nếu bạn trả ra ngoài tham chiếu tới bộ sưu tập nội bộ (không có lớp bọc), bạn mất quyền kiểm soát dữ liệu của mình — con đường thẳng tới lỗi và rò rỉ các bất biến.
Lỗi số 5: Dùng bộ sưu tập bất biến trong mã vốn kỳ vọng có thể thay đổi. Nếu mã bên ngoài cố gắng sửa đổi bộ sưu tập (ví dụ, thêm phần tử), nó sẽ nhận UnsupportedOperationException. Hãy đảm bảo người dùng dữ liệu biết rằng dữ liệu là bất biến.
GO TO FULL VERSION