Collections trong Java là bộ khung giúp bạn lưu và xử lý một nhóm đối tượng. Bạn không phải tự viết cấu trúc dữ liệu từ đầu. Chúng tôi dùng nó gần như mỗi ngày ở backend, từ danh sách đơn hàng đến bảng tra cứu cấu hình. Bài này đi qua các interface chính, vài lớp cài đặt quen thuộc và những thuật toán có sẵn.
Collections Framework trong Java là gì

Collections Framework nằm trong gói java.util. Nó gồm ba phần: interface mô tả hành vi, lớp cài đặt cụ thể và thuật toán tiện ích. Nhờ tách interface khỏi lớp cài đặt, bạn có thể đổi từ ArrayList sang LinkedList mà không phải sửa nhiều.
Trước khi có framework này, mỗi người lại tự viết một kiểu danh sách riêng. Code khó đọc và khó tái sử dụng. Bây giờ cả đội cùng dùng một bộ khái niệm chung, nên đọc code của người khác dễ hơn nhiều. Bạn có thể xem danh sách đầy đủ trong tài liệu Java SE của Oracle.
Các interface chính: List, Set, Queue, Map

Collection là interface gốc cho List, Set và Queue. Map đứng riêng vì nó lưu cặp khóa và giá trị chứ không lưu từng phần tử đơn lẻ.
List: có thứ tự, cho phép phần tử trùng, truy cập theo chỉ số.Set: không cho phép trùng, thường dùng để loại bỏ phần tử lặp.Queue: hàng đợi, lấy phần tử theo thứ tự vào trước ra trước hoặc theo độ ưu tiên.Map: ánh xạ khóa sang giá trị, mỗi khóa xuất hiện đúng một lần.
Một lỗi hay gặp ở người mới là nghĩ Map cũng là Collection. Thực ra Map không kế thừa Collection. Bạn không thể truyền Map vào chỗ cần Collection nếu chưa lấy values() hay keySet() trước.
Lớp cài đặt hay dùng: ArrayList, LinkedList, HashSet, HashMap

ArrayList lưu phần tử trong một mảng động. Truy cập theo chỉ số rất nhanh, nhưng chèn hoặc xóa ở giữa phải dịch các phần tử phía sau. LinkedList thì ngược lại. Chèn đầu và cuối rẻ, còn tìm phần tử thứ n phải đi từ đầu danh sách.
import java.util.*;
public class Demo {
public static void main(String[] args) {
List<String> ngonNgu = new ArrayList<>();
ngonNgu.add("Java");
ngonNgu.add("C");
ngonNgu.add("Java"); // List cho phép trùng
Set<String> duyNhat = new HashSet<>(ngonNgu);
System.out.println(duyNhat.size()); // 2
Map<String, Integer> diem = new HashMap<>();
diem.put("An", 8);
diem.put("Binh", 7);
System.out.println(diem.get("An")); // 8
}
}
HashSet và HashMap dựa trên bảng băm, nên thêm, tìm và xóa trung bình rất nhanh. Đổi lại, chúng không đảm bảo thứ tự duyệt. Cần giữ thứ tự thêm vào thì dùng LinkedHashSet hoặc LinkedHashMap. Cần sắp xếp tự động thì dùng TreeSet và TreeMap.
Với phần tử là đối tượng tự định nghĩa, hãy nhớ ghi đè cả equals lẫn hashCode. Nếu chỉ ghi đè một trong hai, HashSet coi hai đối tượng giống nhau là hai phần tử khác nhau. Đây là lỗi mà chúng tôi từng mất cả buổi chiều mới tìm ra. Khi tạo đối tượng, bạn cũng cần hiểu cách viết constructor trong Java để khởi tạo đúng ngay từ đầu.
Thuật toán có sẵn trong lớp Collections: sort, shuffle, binarySearch

Đừng nhầm Collection (interface) với Collections (lớp tiện ích). Lớp thứ hai chứa các phương thức tĩnh làm việc với collection, bạn khỏi phải tự viết.
List<Integer> so = new ArrayList<>(List.of(5, 3, 9, 1));
Collections.sort(so); // [1, 3, 5, 9]
int viTri = Collections.binarySearch(so, 5); // 2
Collections.shuffle(so); // xáo trộn ngẫu nhiên
Collections.reverse(so); // đảo ngược
System.out.println(Collections.max(so)); // 9
binarySearch chỉ cho kết quả đúng khi danh sách đã được sắp xếp. Nếu gọi trên danh sách lộn xộn, bạn nhận một số vô nghĩa mà không có thông báo lỗi nào. Ngoài ra, List.of tạo danh sách không sửa được, nên ta bọc nó trong new ArrayList<>() trước khi sắp xếp.
Khi duyệt collection và có thể gặp phần tử lỗi, bạn nên bọc xử lý trong khối try-catch. Bài về xử lý ngoại lệ trong Java giải thích rõ cách làm này.
Chọn collection nào cho bài toán của bạn
Không có collection tốt nhất cho mọi trường hợp, chỉ có collection hợp với cách bạn dùng dữ liệu. Bảng dưới đây là cách chúng tôi hay cân nhắc.
| Nhu cầu | Nên chọn |
|---|---|
| Đọc theo chỉ số nhiều, ít chèn giữa | ArrayList |
| Thêm bớt ở hai đầu liên tục | ArrayDeque hoặc LinkedList |
| Loại trùng, không cần thứ tự | HashSet |
| Tra cứu nhanh theo khóa, giữ thứ tự thêm vào nếu cần | HashMap hoặc LinkedHashMap |
| Luôn giữ dữ liệu đã sắp xếp | TreeMap, TreeSet |
Nếu chưa chắc, hãy bắt đầu với ArrayList và HashMap. Chỉ đổi khi bạn đo được rằng chúng chậm. Nhiều người tối ưu sớm bằng LinkedList rồi nhận ra danh sách của mình chỉ có vài chục phần tử. Khi đó chẳng có gì khác biệt.
Một tình huống thực tế: đếm từ trong văn bản
Hãy lấy bài toán đếm số lần xuất hiện của từng từ để thấy các collection phối hợp ra sao. Chúng tôi dùng HashMap để lưu từ và số lần, rồi chuyển sang List để sắp xếp theo tần suất.
String van = "java c java aws java c";
Map<String, Integer> dem = new HashMap<>();
for (String tu : van.split(" ")) {
dem.merge(tu, 1, Integer::sum);
}
List<Map.Entry<String, Integer>> ds = new ArrayList<>(dem.entrySet());
ds.sort((a, b) -> b.getValue() - a.getValue());
System.out.println(ds); // [java=3, c=2, aws=1]
Phương thức merge gọn hơn nhiều so với kiểu kiểm tra khóa đã tồn tại rồi mới cộng. Bạn để ý rằng Map không sắp xếp được trực tiếp. Vì vậy ta mới sao chép các entry sang một danh sách. Đây là mẫu code xuất hiện rất thường xuyên khi xử lý dữ liệu báo cáo.
Các lỗi thường gặp khi dùng collection
Lỗi đầu tiên là dùng kiểu thô, tức không khai báo kiểu trong dấu ngoặc nhọn. Code vẫn chạy nhưng bạn mất khả năng kiểm tra kiểu lúc biên dịch. Lỗi ép kiểu chỉ lộ ra khi chạy thật.
Lỗi thứ hai là sửa danh sách trong lúc đang duyệt bằng for-each, dẫn tới ConcurrentModificationException. Lỗi thứ ba là tin rằng List.of hay Arrays.asList cho bạn danh sách sửa được hoàn toàn. Danh sách đầu không cho thêm hay xóa, danh sách sau không cho đổi kích thước. Gặp UnsupportedOperationException thì hãy nhớ lại điều này.
Cuối cùng, nhớ rằng collection chỉ chứa đối tượng, không chứa kiểu nguyên thủy. Khi bạn viết List<Integer>, Java tự đóng gói số int thành đối tượng Integer. Với dữ liệu hàng triệu phần tử, chi phí này đáng kể. Đôi khi mảng int[] là lựa chọn nhanh hơn.
Khi đã quen với collection, bạn có thể đọc tiếp vòng lặp for trong Java để duyệt chúng bằng for-each một cách gọn gàng. Hãy gõ lại các ví dụ ở trên, đổi dữ liệu và quan sát kết quả; đó là cách nhanh nhất để nhớ lâu.



