1. Unmodifiable wrappers: lớp bao bọc cho các bộ sưu tập
Đôi khi trong mã đã có một bộ sưu tập mà ai đó có thể vô tình (hoặc không hẳn vô tình) sửa đổi. Ví dụ, bạn có một danh sách người dùng muốn trả ra bên ngoài, nhưng không muốn ai đó chỉnh sửa nó:
List<String> users = new ArrayList<>();
users.add("Alice");
users.add("Bob");
Bạn trả danh sách này từ một phương thức, và ai đó gọi users.add("Hacker"); — thế là hệ thống của bạn đã có một người dùng không được phép mới! Bảo vệ thế nào?
Lớp bao bọc từ Collections
Trong Java đã có từ lâu các phương thức bao bọc đặc biệt trong lớp Collections:
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
Các phương thức này trả về lớp bao bọc quanh bộ sưu tập của bạn, không cho phép sửa đổi thông qua chính lớp bao bọc. Cố gắng thêm, xóa hoặc thay đổi phần tử thông qua lớp bao bọc sẽ ném UnsupportedOperationException.
Ví dụ:
import java.util.*;
public class Demo {
public static void main(String[] args) {
List<String> modifiable = new ArrayList<>();
modifiable.add("Alice");
modifiable.add("Bob");
// Tạo lớp bao bọc không thể sửa đổi
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
System.out.println(unmodifiable); // [Alice, Bob]
// Hãy thử thêm phần tử thông qua lớp bao bọc
try {
unmodifiable.add("Charlie"); // Bùm! UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("Không thể thay đổi bộ sưu tập: " + e);
}
}
}
Quan trọng!
- Lớp bao bọc KHÔNG biến bộ sưu tập gốc thành bất biến. Nếu ai đó vẫn giữ tham chiếu tới bộ sưu tập gốc, họ vẫn có thể sửa nó.
- Mọi thay đổi của bộ sưu tập gốc đều có thể nhìn thấy qua lớp bao bọc.
modifiable.add("Charlie");
System.out.println(unmodifiable); // [Alice, Bob, Charlie]
Nghĩa là, nếu ai đó đâu đó trong mã thêm/xóa phần tử ở danh sách gốc, lớp bao bọc sẽ thấy điều đó. Đây không phải là “đóng băng”, mà chỉ là cấm sửa đổi thông qua chính lớp bao bọc.
Lớp bao bọc cho các bộ sưu tập khác
Tương tự, bạn có thể tạo lớp bao bọc cho Set, Map, và thậm chí cho các cấu trúc “dị” hơn:
Set<Integer> numbers = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> unmodSet = Collections.unmodifiableSet(numbers);
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
Map<String, Integer> unmodMap = Collections.unmodifiableMap(ages);
So sánh với các phương thức tạo (List.of và khác)
- List.of(...) tạo ra một bộ sưu tập bất biến mới, ngay từ đầu đã không thể thêm.
- Collections.unmodifiableList(list) — là lớp bao bọc quanh một bộ sưu tập hiện có. Nếu danh sách gốc thay đổi, lớp bao bọc cũng thay đổi theo.
Bảng: so sánh các cách tiếp cận
|
|
|
|---|---|---|
| Có thể thêm không? | Không | Không (qua lớp bao bọc) |
| Có thể thêm vào bản gốc không? | Không áp dụng | Có |
| Có thấy thay đổi không? | Không | Có |
| Có thể đặt null không? | Không (NPE) | Có (nếu bộ sưu tập gốc cho phép) |
| Triển khai | Riêng của nó | Lớp bao bọc quanh bộ sưu tập của bạn |
2. Bộ sưu tập CopyOnWrite
Trong các chương trình đa luồng thường xuất hiện bài toán: một (hoặc vài) luồng đọc bộ sưu tập, còn luồng khác đôi khi thay đổi nó. Các bộ sưu tập thông thường không phù hợp: có thể xảy ra điều kiện tranh chấp (race), lỗi, ConcurrentModificationException và những “niềm vui” khác của thế giới đa luồng.
Cho những trường hợp như vậy, người ta đã tạo ra các bộ sưu tập CopyOnWrite — chúng được thiết kế riêng cho kịch bản đọc nhiều, đổi ít.
Cách hoạt động?
- Mỗi lần thay đổi (thêm, xóa, thay thế), bộ sưu tập tạo một bản sao mới của mảng nội bộ.
- Mọi luồng đọc đều nhận “phiên bản” mảng riêng của mình, không thay đổi trong lúc chúng đang đọc.
- Điều này khiến việc đọc hoàn toàn an toàn và không cần đồng bộ hóa.
Các lớp chính
- CopyOnWriteArrayList<E>
- CopyOnWriteArraySet<E>
Chúng nằm trong gói java.util.concurrent.
Ví dụ sử dụng
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteDemo {
public static void main(String[] args) {
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("Alpha");
cowList.add("Beta");
// Có thể duyệt an toàn, ngay cả khi có người đang song song thêm phần tử
for (String s : cowList) {
System.out.println(s);
cowList.add("Gamma"); // Sẽ không gây ra ConcurrentModificationException!
}
System.out.println(cowList); // [Alpha, Beta, Gamma, Gamma]
}
}
Đặc điểm:
- Iterator của các bộ sưu tập CopyOnWrite luôn “nhìn thấy” ảnh chụp của bộ sưu tập tại thời điểm tạo iterator.
- Nếu sau khi tạo iterator có ai đó thêm phần tử, iterator sẽ không thấy chúng.
- Có thể an toàn thêm/xóa phần tử trong khi duyệt — không có ConcurrentModificationException!
Khi nào dùng các bộ sưu tập CopyOnWrite?
Chúng phù hợp cho tình huống có nhiều luồng chủ yếu đọc dữ liệu từ bộ sưu tập, còn các thao tác thay đổi rất hiếm. Ví dụ kinh điển — danh sách người nghe sự kiện (event listeners): người nghe mới được thêm/bớt không thường xuyên, nhưng việc thông báo cho họ diễn ra liên tục.
Ví dụ — người đăng ký sự kiện
import java.util.concurrent.CopyOnWriteArrayList;
public class EventBus {
private final CopyOnWriteArrayList<Runnable> listeners = new CopyOnWriteArrayList<>();
public void subscribe(Runnable listener) {
listeners.add(listener);
}
public void publishEvent() {
for (Runnable listener : listeners) {
listener.run(); // an toàn, ngay cả khi ai đó đăng ký/hủy đăng ký ngay lúc này!
}
}
}
Nhược điểm của các bộ sưu tập CopyOnWrite
- Chậm khi thay đổi thường xuyên: mỗi thay đổi là tạo bản sao mới của mảng, tốn bộ nhớ và thời gian.
- Kém hiệu quả với bộ sưu tập lớn: sao chép mảng là thao tác tốn kém.
3. So sánh: khi nào dùng cái gì?
Unmodifiable wrappers (Collections.unmodifiable...)
Khi nào dùng: Khi bạn đã có một bộ sưu tập muốn bảo vệ khỏi việc sửa đổi từ mã bên ngoài, nhưng những thay đổi từ bên trong (chủ sở hữu bộ sưu tập) là chấp nhận được.
An toàn luồng: Không được đảm bảo! Nếu bộ sưu tập gốc bị thay đổi từ luồng khác, có thể xảy ra tranh chấp và lỗi.
Các phương thức tạo (List.of, Set.of, Map.of)
Khi nào dùng: Khi bạn muốn tạo một bộ sưu tập bất biến hằng ngay từ đầu, không thể thay đổi từ bất cứ đâu.
An toàn luồng: Được đảm bảo (bộ sưu tập hoàn toàn không thay đổi).
Bộ sưu tập CopyOnWrite
Khi nào dùng: Trong các kịch bản đa luồng, đọc nhiều và ghi ít. Ví dụ, cho danh sách người đăng ký.
An toàn luồng: Có, hoàn toàn an toàn luồng.
Bất biến: Không; có thể thay đổi bộ sưu tập, nhưng mỗi lần sẽ tạo bản sao mới để người đọc không bị ảnh hưởng.
4. Các lỗi điển hình và đặc điểm triển khai
Lỗi số 1: Mong đợi “đóng băng” bộ sưu tập gốc bằng lớp bao bọc. Nhiều người nghĩ rằng Collections.unmodifiableList(list) khiến bộ sưu tập hoàn toàn bất biến. Thực tế, nếu ai đó vẫn giữ tham chiếu tới danh sách gốc, họ có thể sửa nó, và các thay đổi này sẽ nhìn thấy qua lớp bao bọc. Giải pháp: Nếu cần tính bất biến thực sự — hãy dùng List.copyOf(list) (Java 10+) hoặc List.of(...).
Lỗi số 2: Dùng CopyOnWrite cho bộ sưu tập thay đổi thường xuyên. Nếu trong CopyOnWriteArrayList liên tục thêm hoặc xóa phần tử, điều đó sẽ dẫn tới vấn đề về hiệu năng và bộ nhớ. CopyOnWrite chỉ phù hợp cho kịch bản “đọc nhiều, ghi ít”.
Lỗi số 3: Kỳ vọng rằng các lớp bao bọc là an toàn luồng. Collections.unmodifiableList không khiến bộ sưu tập an toàn luồng! Nếu danh sách gốc thay đổi từ các luồng khác nhau, có thể xảy ra lỗi.
Lỗi số 4: Dùng các bộ sưu tập từ List.of hoặc Set.of với null. Khác với các bộ sưu tập thông thường, các phương thức tạo không chấp nhận null — cố gắng thêm hoặc thậm chí tạo bộ sưu tập với null sẽ dẫn tới NullPointerException.
GO TO FULL VERSION