1. Vấn đề về thứ tự trong các collection cổ điển
Trong Java luôn có các collection bảo đảm thứ tự phần tử (ví dụ, ArrayList, LinkedList, LinkedHashSet, LinkedHashMap) và những collection không bảo đảm thứ tự (ví dụ, HashSet, HashMap). Nhưng tất cả các collection này đều có một nhược điểm chung: mặc dù một số trong chúng lưu phần tử theo một thứ tự xác định, các interface chuẩn (List, Set, Map) không cung cấp các phương thức phổ quát để truy cập phần tử đầu và cuối hoặc để đảo ngược thứ tự.
Ví dụ, nếu bạn có List<String>, bạn có thể lấy phần tử đầu bằng list.get(0), nhưng với Set hoặc Map thì mẹo đó không còn hiệu quả — bạn sẽ phải dùng iterator hoặc viết thêm mã thừa. Đồng ý là không tiện và cũng không giúp mã dễ đọc hơn.
Giống như bạn có một tủ với các ngăn kéo, nhưng để lấy ngăn đầu hoặc ngăn cuối, mỗi lần bạn phải tự đếm lại tất cả các ngăn! Sẽ thật tuyệt nếu có những tay nắm “đầu” và “cuối”, phải không?
Sự xuất hiện của SequencedCollection và các interface liên quan
Trong Java 21 đã xuất hiện các interface collection mới:
- SequencedCollection<E>
- SequencedSet<E>
- SequencedMap<K, V>
Các interface này mở rộng các collection chuẩn và đưa ra một cách tiếp cận thống nhất để làm việc với thứ tự phần tử. Giờ đây bạn có thể viết mã tổng quát cho mọi collection mà ở đó thứ tự là quan trọng, mà không cần bận tâm đến hiện thực cụ thể.
Nó là gì?
- SequencedCollection — là collection mà các phần tử có thứ tự xác định, và bạn có thể dễ dàng lấy phần tử đầu, phần tử cuối, cũng như đảo ngược thứ tự.
- SequencedSet — tương tự, nhưng cho tập hợp (các phần tử là duy nhất).
- SequencedMap — tương tự, nhưng cho ánh xạ (key-value).
Những collection nào hiện triển khai chúng?
Trong Java 21, các collection chuẩn sau triển khai các interface mới:
- ArrayList, LinkedList → SequencedCollection
- LinkedHashSet, TreeSet → SequencedSet
- LinkedHashMap, TreeMap → SequencedMap
Điều này có nghĩa là nếu bạn đã dùng các collection này, bạn tự động nhận được các khả năng mới!
2. Các phương thức chính của SequencedCollection, SequencedSet, SequencedMap
Các phương thức của SequencedCollection
E getFirst(); // Lấy phần tử đầu tiên
E getLast(); // Lấy phần tử cuối cùng
SequencedCollection<E> reversed(); // Nhận collection theo thứ tự đảo ngược
Các phương thức của SequencedSet
Giống các phương thức của SequencedCollection, cộng thêm mọi thứ mà Set làm.
Các phương thức của SequencedMap
Map.Entry<K, V> firstEntry(); // Phần tử đầu tiên (key-value)
Map.Entry<K, V> lastEntry(); // Phần tử cuối cùng (key-value)
SequencedMap<K, V> reversed(); // Map theo thứ tự đảo ngược
3. Ví dụ sử dụng các interface mới
Ví dụ 1: Lấy phần tử đầu và cuối
import java.util.*;
public class SequencedDemo {
public static void main(String[] args) {
SequencedCollection<String> sc = new ArrayList<>();
sc.add("Java");
sc.add("Python");
sc.add("Kotlin");
// Lấy phần tử đầu và cuối
String first = sc.getFirst(); // "Java"
String last = sc.getLast(); // "Kotlin"
System.out.println("Đầu tiên: " + first);
System.out.println("Cuối cùng: " + last);
}
}
Kết quả:
Đầu tiên: Java
Cuối cùng: Kotlin
Ví dụ 2: Đảo ngược collection
SequencedCollection<String> sc = new LinkedList<>();
sc.add("A");
sc.add("B");
sc.add("C");
SequencedCollection<String> reversed = sc.reversed();
System.out.println(reversed); // [C, B, A]
Lưu ý: reversed() trả về một view của collection theo thứ tự đảo ngược. Nếu bạn thay đổi collection gốc, view reversed cũng sẽ thay đổi!
Ví dụ 3: Làm việc với SequencedSet
SequencedSet<Integer> set = new LinkedHashSet<>();
set.add(100);
set.add(200);
set.add(300);
System.out.println("Phần tử đầu tiên: " + set.getFirst()); // 100
System.out.println("Phần tử cuối cùng: " + set.getLast()); // 300
SequencedSet<Integer> reversedSet = set.reversed();
System.out.println(reversedSet); // [300, 200, 100]
Ví dụ 4: Làm việc với SequencedMap
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.put("apple", 5);
map.put("banana", 3);
map.put("cherry", 7);
Map.Entry<String, Integer> first = map.firstEntry();
Map.Entry<String, Integer> last = map.lastEntry();
System.out.println("Đầu tiên: " + first.getKey() + " = " + first.getValue()); // apple = 5
System.out.println("Cuối cùng: " + last.getKey() + " = " + last.getValue()); // cherry = 7
SequencedMap<String, Integer> reversedMap = map.reversed();
System.out.println(reversedMap); // {cherry=7, banana=3, apple=5}
4. Điều này liên quan thế nào đến ứng dụng của bạn?
Giả sử trong ứng dụng học tập của bạn, bạn lưu danh sách người dùng đã đăng nhập và muốn nhanh chóng lấy người dùng đầu tiên và cuối cùng (ví dụ để hiển thị “ai đăng nhập đầu tiên” và “ai cuối cùng”). Trước đây bạn phải viết như sau:
List<String> users = new ArrayList<>();
// ... thêm người dùng
String first = users.get(0);
String last = users.get(users.size() - 1);
Nhưng nếu collection không phải là danh sách mà là, ví dụ, LinkedHashSet (nơi phần tử là duy nhất và thứ tự được giữ), mẹo đó sẽ không còn hiệu quả:
Set<String> users = new LinkedHashSet<>();
// ... thêm người dùng
// Lấy phần tử đầu tiên thế nào? Chỉ qua iterator:
String first = users.iterator().next();
// Còn phần tử cuối cùng? Chỉ có cách duyệt hết tất cả phần tử!
Giờ đây mọi thứ đơn giản và tổng quát hơn:
SequencedSet<String> users = new LinkedHashSet<>();
// ... thêm người dùng
String first = users.getFirst();
String last = users.getLast();
Điều này không chỉ giảm số lượng mã mà còn làm cho mã dễ đọc và an toàn hơn.
5. Sơ đồ các interface mới
classDiagram
Collection <|-- SequencedCollection
List <|-- SequencedCollection
Set <|-- SequencedSet
Map <|-- SequencedMap
SequencedCollection <|-- SequencedSet
SequencedSet <|-- LinkedHashSet
SequencedSet <|-- TreeSet
SequencedCollection <|-- ArrayList
SequencedCollection <|-- LinkedList
SequencedMap <|-- LinkedHashMap
SequencedMap <|-- TreeMap
6. Những lưu ý hữu ích
Lợi ích thực tiễn của SequencedCollection
- Interface thống nhất để làm việc với thứ tự: Không còn phải nhớ nơi nào có get(0), nơi nào cần iterator, và nơi nào không thể lấy phần tử đầu.
- Tiện lợi khi làm việc với hàng đợi và ngăn xếp: Dễ dàng lấy phần tử đầu (head) và cuối (tail).
- An toàn và dễ đọc: Ít lỗi hơn do sử dụng collection sai cách; mã trở nên tự mô tả hơn.
- Đảo ngược collection nhanh chóng: Phương thức reversed() cho phép lấy thứ tự ngược một cách dễ dàng mà không cần thao tác thủ công.
- Dễ bảo trì và mở rộng mã: Nếu sau này bạn muốn thay ArrayList bằng LinkedHashSet, mã sử dụng SequencedCollection sẽ không cần viết lại.
Đặc điểm triển khai
- Không phải mọi collection đều triển khai SequencedCollection: Ví dụ, HashSet và HashMap không bảo đảm thứ tự, vì vậy không triển khai các interface mới.
- Các phương thức có thể ném ngoại lệ: Nếu collection rỗng, gọi getFirst() hoặc getLast() sẽ dẫn đến NoSuchElementException. Đừng quên kiểm tra collection không rỗng!
- reversed() — là view chứ không phải bản sao: Thay đổi ở collection gốc sẽ phản ánh vào view đảo ngược và ngược lại.
- Tương thích: Các interface mới chỉ có từ Java 21. Nếu bạn dùng phiên bản Java cũ hơn, hiện chưa có các khả năng này (nhưng đây là lý do tuyệt vời để nâng cấp!).
- Generics: Tất cả các interface mới đều hỗ trợ đầy đủ generics, vì vậy có thể làm việc với mọi kiểu dữ liệu.
7. Lỗi thường gặp khi làm việc với SequencedCollection
Lỗi số 1: Kỳ vọng có thứ tự ở các collection không bảo đảm thứ tự. Nếu bạn cố gắng ép kiểu HashSet sang SequencedSet, bạn sẽ nhận lỗi biên dịch — HashSet không có thứ tự và không triển khai interface này.
Lỗi số 2: Bỏ qua collection rỗng. Gọi getFirst() hoặc getLast() trên collection rỗng sẽ ném ngoại lệ. Trước khi gọi các phương thức này, hãy kiểm tra collection không rỗng:
if (!sc.isEmpty()) {
String first = sc.getFirst();
}
Lỗi số 3: Hiểu nhầm với reversed(). Phương thức reversed() trả về một view, không phải bản sao. Nếu bạn thay đổi view reversed, collection gốc cũng sẽ thay đổi (và ngược lại). Điều này có thể dẫn đến kết quả bất ngờ nếu bạn không lường trước hành vi này.
Lỗi số 4: Dùng các interface mới trên các phiên bản Java cũ. Nếu dự án của bạn biên dịch với phiên bản thấp hơn Java 21, trình biên dịch sẽ không tìm thấy các interface này. Hãy kiểm tra phiên bản JDK trong cài đặt dự án!
GO TO FULL VERSION