Java cơ bản #9: Collections — List, Set, Map, Generics và sắp xếp với Comparator
Bộ sưu tập co giãn thay cho mảng: ArrayList, HashSet, HashMap và khi nào dùng loại nào; generics và kiểu bao (Integer), duyệt và xoá an toàn, danh sách bất biến List.of, sắp xếp bằng Comparable/Comparator, Deque làm ngăn xếp và hàng đợi, cùng độ phức tạp của từng thao tác.
13 phút đọc
Mục lục
Mảng có kích thước cố định — trong khi giỏ hàng, danh sách bạn bè, lịch sử giao dịch thì cứ lớn dần. Thư viện Java Collections Framework cung cấp các cấu trúc dữ liệu co giãn, đã tối ưu và kiểm thử kỹ, mà bạn sẽ dùng hằng ngày: List, Set, Map.
Bài này giới thiệu từng loại, cách chọn loại phù hợp, generics (<String> trong List<String> là gì), và cách sắp xếp đối tượng tuỳ ý.
Bức tranh chung
Đang vẽ sơ đồ…
flowchart TB
C[Collection] --> L["List — có thứ tự, cho trùng"]
C --> S["Set — không trùng"]
C --> Q["Queue / Deque — hàng đợi, ngăn xếp"]
L --> AL[ArrayList]
L --> LL[LinkedList]
S --> HS[HashSet]
S --> LHS[LinkedHashSet]
S --> TS["TreeSet (đã sắp xếp)"]
Q --> AD[ArrayDeque]
Q --> PQ[PriorityQueue]
M["Map — cặp khoá → giá trị (không thuộc Collection)"] --> HM[HashMap]
M --> LHM[LinkedHashMap]
M --> TM["TreeMap (khoá đã sắp xếp)"]Nguyên tắc: khai báo biến theo interface, tạo đối tượng theo lớp cụ thể — List<String> ds = new ArrayList<>();. Sau này đổi sang cài đặt khác chỉ cần sửa một chỗ.
List: danh sách có thứ tự
import java.util.ArrayList;
import java.util.List;
void main() {
List<String> monHoc = new ArrayList<>(); // <> : trình biên dịch tự suy kiểu
monHoc.add("Java");
monHoc.add("SQL");
monHoc.add("Git");
monHoc.add(1, "HTML"); // chèn vào vị trí 1
IO.println(monHoc); // [Java, HTML, SQL, Git] — toString có sẵn
IO.println(monHoc.get(0) + ", size = " + monHoc.size());
monHoc.set(3, "GitHub"); // thay phần tử
monHoc.remove("HTML"); // xoá theo giá trị (dùng equals)
IO.println(monHoc.contains("SQL") + " " + monHoc.indexOf("GitHub"));
for (String m : monHoc) IO.print(m + " | ");
IO.println("");
List<String> coDinh = List.of("Thứ 2", "Thứ 4", "Thứ 6"); // danh sách BẤT BIẾN
// coDinh.add("Thứ 7"); → UnsupportedOperationException
List<String> banSao = new ArrayList<>(coDinh); // cần sửa thì sao chép sang ArrayList
banSao.add("Thứ 7");
IO.println(banSao);
}ArrayList là lựa chọn mặc định cho danh sách: truy cập theo chỉ số O(1), thêm cuối nhanh. LinkedList hiếm khi nhanh hơn trong thực tế — gần như không cần dùng.
Generics và kiểu bao
List<String> nghĩa là "danh sách chỉ chứa String". Trình biên dịch kiểm tra giúp — thêm số vào sẽ báo lỗi ngay, và lấy ra không cần ép kiểu.
Generics chỉ nhận kiểu tham chiếu, nên với số ta dùng kiểu bao (wrapper): Integer cho int, Double cho double, Boolean, Character, Long… Java tự chuyển qua lại (autoboxing/unboxing):
import java.util.ArrayList;
import java.util.List;
void main() {
List<Integer> diem = new ArrayList<>();
diem.add(8); // int 8 tự bọc thành Integer
diem.add(6);
diem.add(9);
int tong = 0;
for (int d : diem) tong += d; // Integer tự mở thành int
IO.println("Tổng " + tong);
diem.remove(Integer.valueOf(6)); // xoá GIÁ TRỊ 6 — remove(6) sẽ hiểu là xoá CHỈ SỐ 6!
IO.println(diem);
Integer a = 1000, b = 1000;
IO.println(a == b); // false — so sánh địa chỉ hai đối tượng Integer
IO.println(a.equals(b)); // true
Integer khongCo = null;
// int x = khongCo; → NullPointerException khi tự mở hộp
}Lưu ý: đừng so sánh
Integerbằng==. Nó "tình cờ đúng" với số nhỏ (−128 đến 127, được dùng chung bộ nhớ đệm) nên lỗi chỉ lộ ra khi dữ liệu thật lớn hơn.
Tự viết lớp và phương thức generic
import java.util.List;
record Cap<A, B>(A dau, B sau) {} // record generic: hai kiểu tuỳ ý
<T extends Comparable<T>> T lonNhat(List<T> ds) { // T phải so sánh được
T max = ds.getFirst();
for (T x : ds) if (x.compareTo(max) > 0) max = x;
return max;
}
void main() {
var p = new Cap<>("Tuổi", 20);
IO.println(p.dau() + " = " + p.sau());
IO.println(lonNhat(List.of(3, 17, 8)));
IO.println(lonNhat(List.of("chuối", "táo", "xoài"))); // so sánh từ điển
}Set: không trùng lặp
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
void main() {
List<String> email = List.of("an@x.vn", "binh@x.vn", "an@x.vn", "chi@x.vn", "binh@x.vn");
Set<String> hs = new HashSet<>(email); // không thứ tự xác định
Set<String> lhs = new LinkedHashSet<>(email); // giữ thứ tự thêm vào
Set<String> ts = new TreeSet<>(email); // tự sắp xếp
IO.println(hs.size() + " email duy nhất"); // 3
IO.println(lhs);
IO.println(ts);
Set<String> lopA = new HashSet<>(Set.of("An", "Bình", "Chi"));
Set<String> lopB = Set.of("Chi", "Dũng");
IO.println("Thêm 'An' lần nữa: " + lopA.add("An")); // false — đã có
lopA.retainAll(lopB); // giao hai tập
IO.println("Học cả hai lớp: " + lopA);
}Dùng Set khi cần loại trùng hoặc kiểm tra "có chứa không" thật nhanh: HashSet.contains là O(1) trung bình, còn ArrayList.contains phải duyệt cả danh sách O(n). Nhớ bài 8: phần tử của HashSet cần equals/hashCode đúng (record có sẵn).
Map: tra cứu theo khoá
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
void main() {
Map<String, Long> gia = new HashMap<>();
gia.put("cà phê", 35_000L);
gia.put("trà sữa", 45_000L);
gia.put("cà phê", 39_000L); // khoá đã có → GHI ĐÈ giá trị
IO.println(gia.get("cà phê")); // 39000
IO.println(gia.get("nước ép")); // null — không có khoá
IO.println(gia.getOrDefault("nước ép", 0L)); // 0
IO.println(gia.containsKey("trà sữa"));
for (Map.Entry<String, Long> e : gia.entrySet()) {
IO.println(e.getKey() + ": " + e.getValue());
}
// Đếm tần suất từ — bài toán Map kinh điển
String van = "học java học sql học git java";
Map<String, Integer> dem = new TreeMap<>(); // TreeMap: khoá được sắp xếp
for (String tu : van.split(" ")) {
dem.merge(tu, 1, Integer::sum); // chưa có → 1; có rồi → cộng thêm 1
}
IO.println(dem); // {git=1, học=3, java=2, sql=1}
// Nhóm: mỗi khoá ứng với một danh sách
Map<Character, java.util.List<String>> theoChuDau = new TreeMap<>();
for (String ten : new String[]{"An", "Anh", "Bình", "Bảo", "Chi"}) {
theoChuDau.computeIfAbsent(ten.charAt(0), k -> new java.util.ArrayList<>()).add(ten);
}
IO.println(theoChuDau);
}| Map | Thứ tự khi duyệt | Dùng khi |
|---|---|---|
HashMap | Không xác định | Mặc định, nhanh nhất |
LinkedHashMap | Thứ tự thêm vào | Cần giữ thứ tự, cache LRU |
TreeMap | Khoá tăng dần | Cần khoá sắp xếp, tìm khoảng (floorKey, headMap) |
Duyệt và xoá an toàn
Xoá phần tử trong lúc duyệt bằng for-each gây ConcurrentModificationException:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
void main() {
List<Integer> so = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
// for (Integer x : so) if (x % 2 == 0) so.remove(x); → ConcurrentModificationException
so.removeIf(x -> x % 2 == 0); // cách gọn nhất (Java 8+)
IO.println(so); // [1, 3, 5]
List<String> ten = new ArrayList<>(List.of("An", "", "Bình", " "));
Iterator<String> it = ten.iterator(); // cách truyền thống
while (it.hasNext()) {
if (it.next().isBlank()) it.remove();
}
IO.println(ten);
}Sắp xếp: Comparable và Comparator
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
record SinhVien(String ten, double diem, int namSinh) implements Comparable<SinhVien> {
@Override
public int compareTo(SinhVien khac) { // "thứ tự tự nhiên": theo tên
return ten.compareTo(khac.ten);
}
}
void main() {
List<SinhVien> ds = new ArrayList<>(List.of(
new SinhVien("Chi", 8.5, 2005),
new SinhVien("An", 7.0, 2006),
new SinhVien("Bình", 8.5, 2004),
new SinhVien("Dũng", 6.5, 2005)));
ds.sort(null); // dùng compareTo (thứ tự tự nhiên)
IO.println(ds.stream().map(SinhVien::ten).toList());
// Comparator: thứ tự tuỳ ý, ghép nhiều tiêu chí
ds.sort(Comparator.comparingDouble(SinhVien::diem).reversed() // điểm giảm dần
.thenComparingInt(SinhVien::namSinh)); // bằng điểm → ai sinh trước đứng trước
for (SinhVien sv : ds) IO.println(sv);
}Comparable(phương thứccompareTo): thứ tự tự nhiên, một cái duy nhất, cài trong chính lớp.String,Integer,LocalDateđều đã có.Comparator: thứ tự bên ngoài, tạo bao nhiêu cũng được.Comparator.comparing(...),.reversed(),.thenComparing(...)ghép tiêu chí rất gọn (cú phápSinhVien::diemlà tham chiếu phương thức — bài 11).
compareTo trả về số âm / 0 / dương nếu đối tượng này nhỏ hơn / bằng / lớn hơn đối tượng kia.
Deque: ngăn xếp và hàng đợi
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.PriorityQueue;
import java.util.Queue;
boolean ngoacHopLe(String s) { // bài toán ngăn xếp kinh điển
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
switch (c) {
case '(', '[', '{' -> stack.push(c);
case ')' -> { if (stack.isEmpty() || stack.pop() != '(') return false; }
case ']' -> { if (stack.isEmpty() || stack.pop() != '[') return false; }
case '}' -> { if (stack.isEmpty() || stack.pop() != '{') return false; }
default -> { }
}
}
return stack.isEmpty();
}
void main() {
IO.println(ngoacHopLe("{a[b(c)]}") + " " + ngoacHopLe("(]"));
Queue<String> hangCho = new ArrayDeque<>(); // hàng đợi FIFO: vào trước ra trước
hangCho.offer("Khách 1");
hangCho.offer("Khách 2");
IO.println("Phục vụ: " + hangCho.poll() + ", còn chờ: " + hangCho);
Queue<Integer> uuTien = new PriorityQueue<>(); // luôn lấy ra phần tử nhỏ nhất
uuTien.addAll(java.util.List.of(5, 1, 4, 2));
IO.print("Theo ưu tiên: ");
while (!uuTien.isEmpty()) IO.print(uuTien.poll() + " ");
IO.println("");
}Dùng ArrayDeque cho cả ngăn xếp (push/pop) lẫn hàng đợi (offer/poll); lớp Stack cũ đã lỗi thời.
Chọn cấu trúc nào
| Cần | Dùng | Độ phức tạp chính |
|---|---|---|
| Danh sách có thứ tự, truy cập theo vị trí | ArrayList | get O(1), add cuối O(1), contains O(n) |
| Không trùng, kiểm tra "có chứa" nhanh | HashSet | add/contains O(1) |
| Không trùng, cần sắp xếp | TreeSet | O(log n) |
| Tra cứu theo khoá | HashMap | get/put O(1) |
| Khoá sắp xếp, tìm theo khoảng | TreeMap | O(log n) |
| Ngăn xếp / hàng đợi | ArrayDeque | O(1) hai đầu |
| Luôn lấy phần tử nhỏ/lớn nhất | PriorityQueue | O(log n) |
Bài tập
- Nhập một đoạn văn, in 5 từ xuất hiện nhiều nhất (gợi ý:
HashMapđếm, rồi sắp xếpentrySettheo giá trị giảm dần). - Viết
GioHangdùngMap<String, Integer>(mã sản phẩm → số lượng) vớithem,bot,xoa,tongSoMon. - Cho hai danh sách học viên, in người chỉ học lớp A, chỉ học lớp B, học cả hai (dùng
Set). - Sắp xếp danh sách sản phẩm theo danh mục (A→Z), cùng danh mục thì theo giá giảm dần.
- Mô phỏng hàng đợi in ấn: tài liệu có độ ưu tiên (
PriorityQueuevớiComparator), in theo ưu tiên rồi theo thứ tự gửi.
Tổng kết
- Khai báo theo interface (
List,Set,Map), tạo bằng lớp cụ thể (ArrayList,HashSet,HashMap). - Generics bảo đảm kiểu lúc biên dịch; số dùng kiểu bao (
Integer…), không so sánh kiểu bao bằng==. List.of,Set.of,Map.oftạo tập hợp bất biến; cần sửa thì sao chép sangArrayList/HashMap.- Xoá khi duyệt bằng
removeIfhoặcIterator.remove. Comparablecho thứ tự tự nhiên;Comparator.comparing().reversed().thenComparing()cho thứ tự tuỳ ý.- Chọn cấu trúc theo thao tác cần nhanh: tra cứu →
HashMap/HashSet; thứ tự →TreeMap/TreeSet; hai đầu →ArrayDeque. - Bài tiếp theo: xử lý lỗi bằng ngoại lệ và đọc ghi file.
Nguồn tham khảo: dev.java — "The Collections Framework", "Generics"; tài liệu API Java 25 — java.util; Bloch — Effective Java, mục 26–31 (generics), 64 (tham chiếu theo interface); Java Programming MOOC — University of Helsinki, phần 7–8.
Bài này thế nào?
Bài viết có hữu ích với bạn?
Đánh giá giúp mình biết nên viết thêm về chủ đề nào.