Java có một khung cấu trúc dữ liệu phong phú — Collections Framework. Mỗi interface (List, Set, Map) có 2–4 implementation khác nhau, tối ưu cho use case khác nhau. Chọn đúng implementation quyết định performance — ArrayList vs LinkedList cho cùng logic có thể chênh 10x tốc độ.
Bài này vẽ bản đồ framework, phân biệt các interface, so sánh implementation cụ thể qua bảng Big-O, và đưa rule chọn theo use case thực tế.
1. Cây interface — bức tranh tổng
flowchart TD
Iterable --> Collection
Collection --> List
Collection --> Set
Collection --> Queue
Set --> SortedSet
SortedSet --> NavigableSet
Queue --> Deque
Map[Map - khong extends Collection]
Map --> SortedMap
SortedMap --> NavigableMap
List -.-> ArrayList
List -.-> LinkedList
Set -.-> HashSet
Set -.-> LinkedHashSet
NavigableSet -.-> TreeSet
Map -.-> HashMap
Map -.-> LinkedHashMap
NavigableMap -.-> TreeMap
Deque -.-> ArrayDequeĐiểm chính:
Iterable— gốc, duy nhất cóiterator(). Cho phép for-each.Collection— extendsIterable, thêmadd,remove,size,contains.List— có thứ tự + index, cho phép trùng.Set— không trùng, thường không có thứ tự.Queue— FIFO.Deque— double-ended (stack + queue).Map— không extendsCollection! Key-value.
2. List — thứ tự và index
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.get(1); // "b"
list.indexOf("c"); // 2
2.1 ArrayList vs LinkedList
| Operation | ArrayList | LinkedList |
|---|---|---|
get(i) | O(1) | O(n) |
add(e) tail | O(1) amortized | O(1) |
add(0, e) head | O(n) | O(1) |
remove(i) giữa | O(n) | O(n) tìm + O(1) remove |
| Memory overhead | Thấp — 1 mảng | Cao — mỗi node 2 pointer |
| Cache locality | Tốt — contiguous | Kém — node rải rác |
Rule thực tế: dùng ArrayList 99% trường hợp. LinkedList chỉ tốt khi:
- Add/remove ở 2 đầu liên tục — dùng
ArrayDequecòn tốt hơn. - Không bao giờ cần
get(i)ngẫu nhiên.
Josh Bloch (thiết kế Collections) nói thẳng: "I don't think anyone has ever used LinkedList correctly in 20+ years of Java".
2.2 ArrayList bên dưới
public class ArrayList<E> {
private Object[] elementData; // mang noi bo
private int size;
public void add(E e) {
if (size == elementData.length) {
grow(); // resize voi factor 1.5x
}
elementData[size++] = e;
}
}
addamortized O(1) — thỉnh thoảng resize tốn O(n), trung bình O(1).ensureCapacity(n)nếu biết trước size — tránh resize nhiều lần.
3. Set — không trùng
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("a"); // khong add, da ton tai
set.size(); // 2
set.contains("a"); // true
3.1 HashSet vs LinkedHashSet vs TreeSet
HashSet | LinkedHashSet | TreeSet | |
|---|---|---|---|
| Thứ tự iterate | Không xác định | Thứ tự add | Sorted theo compareTo |
add, contains, remove | O(1) | O(1) | O(log n) |
| Null | ✅ | ✅ | ❌ (throw NPE khi compare) |
| Bên dưới | HashMap | LinkedHashMap | Red-black tree |
Rule:
HashSet— mặc định, nhanh nhất, không cần order.LinkedHashSet— cần giữ thứ tự insert (vd hiển thị cho user).TreeSet— cần sorted, hoặc query range (subSet,headSet,tailSet).
3.2 HashSet yêu cầu equals + hashCode đúng
class Book {
String title;
// khong override equals, hashCode
}
Set<Book> set = new HashSet<>();
set.add(new Book("Java"));
set.contains(new Book("Java")); // false!
Module 5 bài 5 đã nói — Set/Map dựa vào equals/hashCode của element. Nếu không override, mặc định so địa chỉ → 2 object cùng nội dung vẫn khác nhau.
4. Map — key-value
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.get("Alice"); // 30
ages.getOrDefault("Eve", 0); // 0
ages.containsKey("Bob"); // true
4.1 HashMap vs LinkedHashMap vs TreeMap
Song song với Set:
HashMap | LinkedHashMap | TreeMap | |
|---|---|---|---|
| Thứ tự | Không xác định | Thứ tự insert (hoặc access) | Sorted theo key |
put, get, remove | O(1) | O(1) | O(log n) |
| Null key | ✅ 1 null | ✅ 1 null | ❌ |
LinkedHashMap(accessOrder=true) làm LRU cache đơn giản:
Map<String, Data> cache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Data> eldest) {
return size() > 100; // giu toi da 100 entry
}
};
4.2 HashMap bên dưới (Java 8+)
- Bảng
Node<K,V>[] table— array of bucket. - Hash key → chỉ số bucket.
- Va chạm: linked list trong bucket. Java 8+ treeify thành red-black tree nếu bucket > 8 entry → lookup worst-case O(log n) thay vì O(n).
Load factor mặc định 0.75 — khi size / capacity > 0.75, table resize double + rehash toàn bộ. Nếu biết trước size, init capacity đủ:
Map<String, Integer> m = new HashMap<>(1024); // tranh resize nhieu lan
5. Queue và Deque
5.1 Queue — FIFO
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(1); // add tail
queue.offer(2);
queue.poll(); // remove head — tra 1
queue.peek(); // xem head — 2, khong remove
5.2 Deque — 2 đầu
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(1);
deque.addLast(2);
deque.pollFirst(); // 1
deque.pollLast(); // 2
// Dung lam stack:
deque.push(1);
deque.push(2);
deque.pop(); // 2
ArrayDeque nhanh hơn LinkedList và Stack cổ điển cho mọi stack/queue use case. Rule: cần stack/queue → ArrayDeque.
5.3 PriorityQueue — heap
Queue<Integer> pq = new PriorityQueue<>();
pq.offer(3);
pq.offer(1);
pq.offer(2);
pq.poll(); // 1 — min heap mac dinh
Truy xuất min/max O(1), add/remove O(log n). Dùng cho top-K, scheduling, Dijkstra.
6. Immutable collection (Java 9+)
List<Integer> list = List.of(1, 2, 3);
Set<String> set = Set.of("a", "b", "c");
Map<String, Integer> map = Map.of("a", 1, "b", 2);
- Immutable — add/remove ném
UnsupportedOperationException. - Không cho phép
null. - Thread-safe (không đổi được).
- Thường tiết kiệm memory hơn
ArrayListtương ứng.
Dùng cho constants, config, test data. Khác Collections.unmodifiableList(mutable) — cái sau là view, mutable list gốc vẫn thay đổi được và view phản ánh.
6.1 List.copyOf, Set.copyOf, Map.copyOf — snapshot
List<Integer> mutable = new ArrayList<>(List.of(1, 2, 3));
List<Integer> snapshot = List.copyOf(mutable);
mutable.add(4);
// snapshot van la [1, 2, 3]
Pattern defensive copy — bảo vệ internal state khỏi mutation từ caller.
7. Bảng chọn — theo use case
| Use case | Chọn |
|---|---|
| Danh sách có thứ tự, random access | ArrayList |
| Stack / Queue | ArrayDeque |
| Không trùng, fast lookup | HashSet |
| Không trùng, giữ thứ tự insert | LinkedHashSet |
| Không trùng, sorted | TreeSet |
| Key-value fast lookup | HashMap |
| Key-value giữ thứ tự insert | LinkedHashMap |
| Key-value sorted theo key | TreeMap |
| Priority queue (top-K) | PriorityQueue |
| Constant / immutable | List.of, Set.of, Map.of |
| Thread-safe | ConcurrentHashMap, CopyOnWriteArrayList |
8. Pitfall tổng hợp
❌ Nhầm 1: Dùng LinkedList vì "đọc trên internet nói insert nhanh".
✅ Hầu như luôn ArrayList. Nếu cần queue/stack → ArrayDeque.
❌ Nhầm 2: Không override equals/hashCode cho class dùng làm key HashMap / element HashSet.
✅ Module 5 bài 5. Luôn override cả 2 với cùng bộ field.
❌ Nhầm 3: Modify collection đang iterate → ConcurrentModificationException.
for (String s : list) { if (bad(s)) list.remove(s); } // CME
✅ list.removeIf(...) hoặc Iterator.remove().
❌ Nhầm 4: List.of(...) rồi list.add(...).
List<Integer> list = List.of(1, 2, 3);
list.add(4); // UnsupportedOperationException
✅ new ArrayList<>(List.of(1, 2, 3)) nếu cần mutable.
❌ Nhầm 5: new HashMap() với size lớn biết trước mà không init capacity.
Map<K, V> m = new HashMap<>();
for (int i = 0; i < 1_000_000; i++) m.put(keys[i], values[i]); // resize nhieu lan
✅ new HashMap<>(1_500_000) — tránh resize.
9. 📚 Deep Dive Oracle
ℹ️ 📚 Deep Dive Oracle (optional)
Spec / reference chính thức:
- Collections Framework Overview — javadoc package.
- HashMap javadoc — giải thích treeify threshold, load factor.
- JEP 180 — Handle Frequent HashMap Collisions with Balanced Trees — treeify trong Java 8.
- Effective Java Item 50: "Make defensive copies"; Item 58: "Prefer for-each loops to traditional for loops"; Item 64: "Refer to objects by their interfaces".
- Clean Code (Joshua Bloch) video series trên YouTube về Collections — từ người thiết kế.
Ghi chú: Java 8+ HashMap dùng treeify khi collision nhiều: bucket > 8 entry thì chuyển từ linked list sang red-black tree → lookup degrade O(log n) thay vì O(n). Defense chống DoS attack qua hash collision. Detail trong JEP 180 — đáng đọc để hiểu sự phát triển của HashMap.
10. Tóm tắt
- Cây interface:
Iterable → Collection → List/Set/Queue;Maptách riêng. ArrayList— mặc định cho List.LinkedListhầu như không dùng.ArrayDeque— best cho stack/queue.HashSet/HashMap— O(1) mặc định.LinkedHashSet/Mapgiữ thứ tự insert.TreeSet/Mapsorted + O(log n).HashSet/HashMapyêu cầuequals/hashCodeđúng cho element/key.- Java 8+
HashMaptreeify bucket > 8 → chống DoS hash collision. List.of/Set.of/Map.of(Java 9+) cho immutable.List.copyOf/ ...copyOf cho defensive snapshot.ConcurrentModificationException— dùngremoveIfthay vì modify trong loop.- Rule declare bằng interface, instantiate bằng class:
List<String> x = new ArrayList<>().
11. Tự kiểm tra
Q1Chọn implementation phù hợp cho từng use case:
(a) lưu 10 triệu user ID để check "đã tồn tại?" nhanh
(b) lưu danh sách tin nhắn chat, hiển thị theo thứ tự gửi
(c) queue task theo ưu tiên (task quan trọng chạy trước)
(d) lookup config theo key, giữ thứ tự khai báo trong file▸
(a) lưu 10 triệu user ID để check "đã tồn tại?" nhanh
(b) lưu danh sách tin nhắn chat, hiển thị theo thứ tự gửi
(c) queue task theo ưu tiên (task quan trọng chạy trước)
(d) lookup config theo key, giữ thứ tự khai báo trong file
- (a)
HashSet<Long>— O(1) contains/add, không cần thứ tự, không trùng. Với 10M entry thì memory đáng kể — có thể xem xétBitSetnếu ID liên tục trong range biết trước. - (b)
ArrayList<Message>— có thứ tự, random access, append nhanh. Thông thường không cần remove giữa. Nếu cần delete messages cũ:ArrayDequevới eviction. - (c)
PriorityQueue<Task>— heap-based, offer/poll O(log n), peek O(1). Task cócompareTohoặcComparatortheo priority. - (d)
LinkedHashMap<String, String>— O(1) lookup + giữ thứ tự insert.HashMapthường order không xác định, không phù hợp nếu cần iterate theo thứ tự file.
Q2Vì sao đoạn sau chạy chậm với LinkedList?LinkedList<Integer> list = new LinkedList<>();
for (int i = 0; i < 10_000; i++) list.add(i);
for (int i = 0; i < list.size(); i++) {
sum += list.get(i); // O(n)
}
▸
LinkedList?LinkedList<Integer> list = new LinkedList<>();
for (int i = 0; i < 10_000; i++) list.add(i);
for (int i = 0; i < list.size(); i++) {
sum += list.get(i); // O(n)
}LinkedList.get(i) là O(n) — phải duyệt từ đầu (hoặc cuối, tuỳ gần hơn) đến index i. Loop chạy n lần × O(n) cho mỗi get = O(n²).
Với n = 10_000 → ~50 triệu operation. Chậm vài giây. Với n = 1_000_000 → 500 tỉ → treo.
Fix (chọn 1):
- Đổi sang
ArrayList—get(i)O(1), loop O(n). Đây là case điển hình người sai khi nghe "LinkedList nhanh". - Dùng for-each:
for (int x : list) sum += x;— dùng iterator O(1) cho mỗinext(), tổng O(n).
Bài học: biết complexity từng operation của implementation bạn dùng. ArrayList tốt hơn LinkedList trong hầu hết mọi trường hợp.
Q3Đoạn sau in gì?Map<String, Integer> m = new HashMap<>();
m.put("a", 1);
m.put("b", 2);
m.put("c", 3);
for (var e : m.entrySet()) {
System.out.println(e.getKey() + "=" + e.getValue());
}
▸
Map<String, Integer> m = new HashMap<>();
m.put("a", 1);
m.put("b", 2);
m.put("c", 3);
for (var e : m.entrySet()) {
System.out.println(e.getKey() + "=" + e.getValue());
}In 3 entry nhưng thứ tự không xác định — có thể a=1, b=2, c=3 hoặc c=3, a=1, b=2 hoặc khác, tuỳ hash của key.
HashMap không đảm bảo thứ tự iterate — phụ thuộc bucket position do hash function quyết định.
Muốn thứ tự insert: dùng LinkedHashMap<String, Integer>. Muốn sorted theo key: TreeMap<String, Integer>.
Đây là nguồn bug tinh vi: developer test local, HashMap tình cờ in đúng thứ tự mong đợi → viết assertion dựa vào thứ tự → deploy prod có JDK khác, thứ tự đổi → test fail. Rule: không bao giờ assume thứ tự của HashMap.
Q4Vì sao đoạn sau ném ConcurrentModificationException? Fix.List<String> names = new ArrayList<>(List.of("alice", "bob", "charlie"));
for (String name : names) {
if (name.startsWith("b")) names.remove(name);
}
▸
ConcurrentModificationException? Fix.List<String> names = new ArrayList<>(List.of("alice", "bob", "charlie"));
for (String name : names) {
if (name.startsWith("b")) names.remove(name);
}Iterator của ArrayList giữ expectedModCount. list.remove(name) tăng modCount trên list. Iteration tiếp tục check modCount != expectedModCount → throw ConcurrentModificationException (fail-fast).
Tên "ConcurrentModification" gây hiểu lầm — không phải chỉ xảy ra với multi-thread. Chỉ cần modify collection trong khi đang iterate nó (cùng thread) cũng bị.
Fix (chọn 1):
- `removeIf` (ngắn nhất, Java 8+):
names.removeIf(name -> name.startsWith("b")); - Iterator thủ công:
Iterator<String> it = names.iterator(); while (it.hasNext()) { if (it.next().startsWith("b")) it.remove(); } - Duyệt ngược với index:
for (int i = names.size() - 1; i >= 0; i--) { if (names.get(i).startsWith("b")) names.remove(i); }
Q5Khi nào chọn TreeMap thay HashMap?▸
TreeMap thay HashMap?TreeMap — bên dưới là red-black tree. HashMap — hash table.
Chọn TreeMap khi:
- Cần key sorted: iterate theo thứ tự (vd alphabetically). Key phải implement
Comparablehoặc passComparator. - Range query:
subMap(from, to),headMap(to),tailMap(from),ceilingKey,floorKey. Hữu ích cho time-series (tìm entry gần timestamp nhất), pricing tier (lookup bậc giá theo số tiền). - Ổn định worst-case: O(log n) đảm bảo, trong khi
HashMapworst-case O(n) nếu bad hash (hiếm với Java 8+ treeify).
Chọn HashMap (mặc định) khi:
- Chỉ cần lookup/insert/remove theo key — O(1) trung bình.
- Không cần thứ tự iterate.
- Không cần range query.
Khác biệt performance: HashMap get/put ~10-50 ns; TreeMap ~100-500 ns tuỳ size. Hầu hết app không thấy khác biệt, nhưng hot loop 1M ops thì đáng cân nhắc.
Bài tiếp theo: Mini-challenge: LRU Cache với LinkedHashMap