1. Giới thiệu
Collection cổ điển: linh hoạt nhưng nhiều cạm bẫy
Khi bạn tạo một collection bằng new ArrayList<>(), bạn nhận được một cấu trúc có thể thay đổi tự do: thêm, xóa, chỉnh sửa phần tử. Điều đó rất tiện khi bạn xây dựng dữ liệu “tức thì”. Nhưng nếu bạn truyền collection này sang lớp hay phương thức khác, nơi nó không nên bị thay đổi thì sao? Và nếu bạn vô tình lộ collection này ra bên ngoài, ai đó sửa nó thì sao? Đó là lúc rắc rối bắt đầu.
Ví dụ về lỗi kinh điển
import java.util.*;
public class Example {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// Truyền collection "ra bên ngoài"
processNames(names);
// Kỳ vọng danh sách không bị thay đổi...
System.out.println(names);
}
public static void processNames(List<String> list) {
// Nhưng ai đó lại xóa một phần tử!
list.remove("Bob");
}
}
Kết quả:
[Alice, Charlie]
Collection của bạn đã bị thay đổi, dù bạn hoàn toàn không hề muốn. Trong các dự án lớn, những “bất ngờ” như vậy rất dễ biến thành các bug cực kỳ khó chịu và khó lần ra.
Nguy hiểm ở chỗ mã làm việc với collection có thể bất ngờ gặp phải thay đổi dữ liệu khó lường. Hơn nữa, luôn có rủi ro mất thông tin — ai đó vô tình xóa phần tử hoặc ghi đè nó. Và nếu collection bị thay đổi đồng thời từ nhiều luồng, bạn không chỉ có thể gặp ConcurrentModificationException mà còn vấn đề thâm hiểm hơn — dữ liệu không nhất quán.
Bảo vệ collection: cách làm cũ
Trước Java 9, phải dùng các phương thức wrapper như Collections.unmodifiableList(...) mà chúng ta đã nói ở cấp trước. Chúng giúp trả về một collection “đóng băng”. Nhưng cách này không phải lúc nào cũng tiện và không giải quyết được mọi vấn đề (về chi tiết — trong bài sau).
2. Giải pháp hiện đại: các phương thức factory List.of, Set.of, Map.of
Trong Java 9 xuất hiện các phương thức tĩnh mới trong các interface collection: List.of, Set.of, Map.of. Chúng cho phép tạo nhanh một collection không thể thay đổi. Tựa như bạn tạo xong collection rồi “đổ bê tông” ngay — không ai có thể thêm, xóa hay sửa phần tử.
Ví dụ tạo collection bất biến
import java.util.*;
public class ImmutableDemo {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
Set<Integer> numbers = Set.of(1, 2, 3);
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25, "Charlie", 28);
System.out.println(names);
System.out.println(numbers);
System.out.println(ages);
}
}
Kết quả:
[Alice, Bob, Charlie]
[1, 2, 3]
{Alice=30, Bob=25, Charlie=28}
Cách hoạt động?
- List.of(...) — tạo danh sách bất biến.
- Set.of(...) — tạo tập hợp (set) bất biến.
- Map.of(...) — tạo map bất biến (tối đa 10 cặp key-value; nhiều hơn hãy dùng Map.ofEntries(...)).
Lưu ý! Các collection được tạo bằng những phương thức này không cho phép thay đổi. Mọi nỗ lực thêm, xóa hoặc thay thế phần tử sẽ ném ngoại lệ.
3. Ví dụ sử dụng và cạm bẫy
Ví dụ: thử thay đổi collection
import java.util.*;
public class ImmutableFail {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob");
// names.add("Charlie"); // Lỗi lúc chạy!
try {
names.add("Charlie");
} catch (UnsupportedOperationException ex) {
System.out.println("Không thể thêm phần tử: " + ex.getClass().getSimpleName());
}
}
}
Kết quả:
Không thể thêm phần tử: UnsupportedOperationException
Ví dụ: thử thêm null
import java.util.*;
public class NullFail {
public static void main(String[] args) {
try {
List<String> badList = List.of("Alice", null, "Bob");
} catch (NullPointerException ex) {
System.out.println("Null bị cấm: " + ex.getClass().getSimpleName());
}
}
}
Kết quả:
Null bị cấm: NullPointerException
Ví dụ: trùng lặp trong Set.of
import java.util.*;
public class DuplicatesFail {
public static void main(String[] args) {
try {
Set<String> badSet = Set.of("one", "two", "one");
} catch (IllegalArgumentException ex) {
System.out.println("Không cho phép trùng lặp: " + ex.getClass().getSimpleName());
}
}
}
Kết quả:
Không cho phép trùng lặp: IllegalArgumentException
Ví dụ: Map.of với số lượng cặp lớn
import java.util.*;
public class MapOfLarge {
public static void main(String[] args) {
// Map.of hỗ trợ tối đa 10 cặp key-value
Map<String, Integer> map = Map.of(
"one", 1, "two", 2, "three", 3, "four", 4, "five", 5,
"six", 6, "seven", 7, "eight", 8, "nine", 9, "ten", 10
);
System.out.println(map);
// Đối với số lượng lớn hơn hãy dùng Map.ofEntries
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("eleven", 11),
Map.entry("twelve", 12),
Map.entry("thirteen", 13)
// ...v.v.
);
System.out.println(bigMap);
}
}
4. Đặc điểm và hạn chế của collection bất biến
Không thể thay đổi.
Mọi nỗ lực thêm, xóa hoặc chỉnh sửa phần tử sẽ ném UnsupportedOperationException. Ngay cả các phương thức thường được phép (add, remove, set) cũng không hoạt động.
Không được dùng null.
Nếu bạn cố thêm null làm phần tử của list hoặc set, hay làm key/value trong map, bạn sẽ nhận NullPointerException. Điều này nhằm tăng độ an toàn: phần tử null thường dẫn đến lỗi trong collection.
Không có bảo đảm về cài đặt cụ thể.
Bạn sẽ không biết chính xác lớp nào nằm bên dưới collection được tạo qua List.of và các phương thức khác. Đừng làm instanceof với ArrayList hay cố ép kiểu sang một loại cụ thể nào đó.
Thứ tự phần tử.
— Với List.of, thứ tự phần tử được giữ nguyên (như list thông thường).
— Với Set.of, thứ tự không được bảo đảm (trên thực tế có thể trùng với thứ tự đối số truyền vào, nhưng đừng phụ thuộc).
— Với Map.of, thứ tự các cặp không được bảo đảm.
Hiệu năng.
Các collection được tạo qua phương thức factory thường chạy nhanh hơn so với wrapper quanh collection có thể thay đổi, vì chúng không tốn bộ nhớ cho những khả năng dư thừa.
5. Khi nào và vì sao nên dùng collection bất biến
Tập dữ liệu hằng
Nếu bạn có list, set hoặc map không nên thay đổi trong quá trình chạy chương trình, hãy dùng List.of, Set.of, Map.of. Ví dụ:
private static final List<String> ROLES = List.of("USER", "ADMIN", "MODERATOR");
Giờ thì không ai có thể thêm một vai trò thừa vào danh sách này.
Trả về collection từ phương thức
Nếu bạn trả về một collection từ phương thức và không muốn ai đó ở bên ngoài sửa nó:
public List<String> getDefaultNames() {
return List.of("Alice", "Bob", "Charlie");
}
Người nhận sẽ không thể làm hỏng dữ liệu của bạn.
Truyền giữa các lớp của ứng dụng
Khi bạn truyền collection giữa các phần khác nhau của chương trình (ví dụ giữa các layer Controller và Service trong ứng dụng web), tốt hơn hãy dùng collection bất biến để không ai có thể “lén lút” sửa chúng.
An toàn và an toàn luồng
Collection bất biến theo định nghĩa là an toàn luồng khi đọc: nếu không ai có thể sửa chúng, thì có thể thoải mái dùng từ nhiều luồng mà không cần đồng bộ hóa.
6. Ví dụ thực tiễn cho một ứng dụng phổ thông
Giả sử trong ứng dụng học tập của chúng ta có danh sách các lệnh hỗ trợ:
public class Commands {
public static final List<String> SUPPORTED_COMMANDS = List.of(
"help", "exit", "list", "add", "remove"
);
}
Nếu bạn thử làm thế này:
Commands.SUPPORTED_COMMANDS.add("hack_the_system");
Bạn sẽ nhận ngoại lệ và không thể gây hại cho ứng dụng.
Hoặc, ví dụ, nếu bạn có một map mã lỗi:
public class ErrorCodes {
public static final Map<Integer, String> CODES = Map.of(
404, "Not Found",
500, "Internal Server Error",
403, "Forbidden"
);
}
Bất kỳ cố gắng thêm mã mới nào sẽ ném ngoại lệ.
7. So sánh các cách tạo collection
| Cách tạo | Có thể thay đổi? | Cho phép null? | Trùng lặp? | An toàn luồng | Ví dụ |
|---|---|---|---|---|---|
|
Có | Có | Có | Không | |
|
Không | Không | Có | Có* | |
|
Không | Không | Không | Có* | |
|
Không | Không | Không | Có* | |
|
Không | Phụ thuộc vào bản gốc | Có | Không | |
* — an toàn luồng chỉ ở khía cạnh bất biến: nếu không ai sửa collection, có thể đọc an toàn từ nhiều luồng.
8. Các lỗi thường gặp khi làm việc với List.of, Set.of, Map.of
Lỗi №1: thử thay đổi collection.
Một lỗi rất thường gặp — cố gắng thêm hoặc xóa phần tử khỏi collection được tạo qua List.of, Set.of hoặc Map.of. Ví dụ, names.add("Dmitry") hoặc ages.remove("Bob"). Điều này luôn dẫn đến UnsupportedOperationException lúc chạy.
Lỗi №2: thử thêm null.
Nếu bạn vô tình truyền null vào bất kỳ phương thức nào (ví dụ, List.of("Alice", null)), bạn sẽ nhận NullPointerException. Collection bất biến trong Java 9+ “không ưa” null — và điều đó thực ra là tốt.
Lỗi №3: phần tử trùng lặp trong Set.of hoặc Map.of.
Set.of("a", "b", "a") hoặc Map.of("x", 1, "x", 2) sẽ dẫn đến IllegalArgumentException. Set và Map theo định nghĩa không thể chứa trùng lặp.
Lỗi №4: kỳ vọng một cài đặt cụ thể.
Đừng làm như sau:
List<String> list = List.of("a", "b");
if (list instanceof ArrayList) {
// ...
} // Luôn luôn là false!
Cài đặt bên trong bị ẩn — đừng phụ thuộc vào chi tiết cài đặt.
Lỗi №5: thử dùng các phương thức thay đổi collection.
Ngay cả các phương thức như clear(), set(index, value) (đối với list) cũng sẽ ném ngoại lệ. Hãy nhớ: các collection được tạo qua phương thức factory là bất biến.
GO TO FULL VERSION