Tìm Hiểu Sâu: Java Collections Framework

Java Collections Framework là một trong những phần cơ bản và mạnh mẽ nhất của Java Standard Library. Hiểu cách chọn và sử dụng collection phù hợp có thể ảnh hưởng đáng kể đến hiệu suất ứng dụng, việc sử dụng bộ nhớ và khả năng bảo trì code của bạn.
Hướng dẫn này cung cấp cái nhìn sâu toàn diện về Collections Framework, với phân tích hiệu suất, ví dụ thực tế và best practices cho các developer có kinh nghiệm.
Tổng Quan về Collections Framework
Collections Framework bao gồm các interface, implementation và thuật toán để biểu diễn và thao tác với các tập hợp đối tượng.
Cấu Trúc Phân Cấp Chính
Collection (interface)
├── List (interface)
│ ├── ArrayList
│ ├── LinkedList
│ └── Vector (legacy, synchronized)
│
├── Set (interface)
│ ├── HashSet
│ ├── LinkedHashSet
│ └── TreeSet (SortedSet)
│
└── Queue (interface)
├── ArrayDeque
├── PriorityQueue
└── LinkedList
Map (interface, separate from Collection)
├── HashMap
├── LinkedHashMap
├── TreeMap (SortedMap)
├── Hashtable (legacy, synchronized)
└── ConcurrentHashMap (thread-safe)Các Nguyên Tắc Thiết Kế Chính:
- Dựa trên interface: Lập trình theo interface, không phải implementation
- Fail-fast iterators: Hầu hết các collection ném
ConcurrentModificationExceptionkhi bị sửa đổi cấu trúc trong lúc iteration - Generic types: Collection an toàn kiểu với Java Generics
- API nhất quán: Các method chung trên tất cả các loại collection
List Interface: Collection Có Thứ Tự
List duy trì thứ tự chèn và cho phép các phần tử trùng lặp. Chọn giữa ArrayList và LinkedList dựa trên pattern truy cập của bạn.
ArrayList: Mảng Động
Tốt nhất cho: Truy cập ngẫu nhiên, iteration và khi bạn biết xấp xỉ kích thước.
List<String> names = new ArrayList<>(100); // Initial capacity
// O(1) - Add to end (amortized)
names.add("Alice");
names.add("Bob");
// O(1) - Random access by index
String first = names.get(0);
// O(n) - Insert in middle (requires shifting)
names.add(1, "Charlie");
// O(n) - Search by value
boolean hasAlice = names.contains("Alice");
// O(n) - Remove by index (requires shifting)
names.remove(0);Đặc Điểm Hiệu Suất:
- Truy cập theo index: O(1)
- Thêm vào cuối: O(1) amortized (O(n) khi resize)
- Thêm/xóa tại index: O(n) - yêu cầu dịch chuyển các phần tử
- Tìm kiếm: O(n)
- Memory overhead: Thấp - chỉ lưu trữ mảng
Cơ Chế Nội Bộ:
// Simplified ArrayList internals
public class ArrayList<E> {
private Object[] elementData;
private int size;
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length) {
// Grow by ~50% (old capacity + old capacity >> 1)
int newCapacity = elementData.length + (elementData.length >> 1);
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
}Best Practices:
// ✅ NÊN: Chỉ định capacity ban đầu nếu biết
List<Item> items = new ArrayList<>(expectedSize);
// ✅ NÊN: Sử dụng cho các scenario truy cập ngẫu nhiên
for (int i = 0; i < items.size(); i++) {
process(items.get(i)); // O(1) access
}
// ❌ KHÔNG NÊN: Thường xuyên chèn vào giữa
for (int i = 0; i < 1000; i++) {
items.add(0, newItem); // O(n) mỗi lần - chậm!
}LinkedList: Danh Sách Liên Kết Đôi
Tốt nhất cho: Thường xuyên chèn/xóa ở cả hai đầu, hoặc khi bạn cần một Deque.
LinkedList<String> queue = new LinkedList<>();
// O(1) - Add to either end
queue.addFirst("First");
queue.addLast("Last");
// O(1) - Remove from either end
String first = queue.removeFirst();
String last = queue.removeLast();
// O(n) - Random access by index
String item = queue.get(5); // Traverses from head or tail
// O(n) - Search
boolean found = queue.contains("item");Đặc Điểm Hiệu Suất:
- Truy cập theo index: O(n)
- Thêm/xóa ở đầu/cuối: O(1)
- Thêm/xóa tại vị trí bất kỳ: O(n) để tìm + O(1) để sửa đổi
- Tìm kiếm: O(n)
- Memory overhead: Cao - mỗi node có hai con trỏ (next, prev)
Cấu Trúc Nội Bộ:
// Simplified LinkedList node
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}ArrayList vs LinkedList: Khi Nào Sử Dụng
| Operation | ArrayList | LinkedList |
|---|---|---|
| get(index) | O(1) ✅ | O(n) ❌ |
| add(element) (end) | O(1)* ✅ | O(1) ✅ |
| add(0, element) | O(n) ❌ | O(1) ✅ |
| remove(index) | O(n) ❌ | O(n) ⚠️ |
| Iterator.remove() | O(n) ❌ | O(1) ✅ |
| Memory overhead | Thấp ✅ | Cao ❌ |
*Amortized - Thỉnh thoảng O(n) khi resize
Ma Trận Quyết Định:
// Sử dụng ArrayList khi:
// - Truy cập ngẫu nhiên thường xuyên
// - Thêm vào cuối là thao tác chính
// - Hiệu quả bộ nhớ quan trọng
// - Bạn iterate với vòng lặp dựa trên index
List<String> names = new ArrayList<>();
// Sử dụng LinkedList khi:
// - Implement queue/deque
// - Thường xuyên thêm/xóa ở đầu
// - Sử dụng iterator để xóa trong khi duyệt
Deque<Task> taskQueue = new LinkedList<>();Set Interface: Các Phần Tử Duy Nhất
Set đảm bảo tính duy nhất (không trùng lặp) nhưng khác nhau về thứ tự và hiệu suất.
HashSet: Nhanh, Không Có Thứ Tự
Tốt nhất cho: Kiểm tra membership nhanh, không cần thứ tự.
Set<String> uniqueUsers = new HashSet<>();
// O(1) - Add element
uniqueUsers.add("alice");
uniqueUsers.add("bob");
uniqueUsers.add("alice"); // Bị bỏ qua - đã tồn tại
// O(1) - Check membership
boolean hasAlice = uniqueUsers.contains("alice");
// O(1) - Remove
uniqueUsers.remove("bob");
// Unordered iteration
for (String user : uniqueUsers) {
System.out.println(user); // Thứ tự không được đảm bảo
}Đặc Điểm Hiệu Suất:
- Add: O(1)
- Contains: O(1)
- Remove: O(1)
- Iteration: O(n)
- Ordering: Không có (không dự đoán được)
Implementation Nội Bộ:
Sử dụng HashMap bên trong, trong đó các phần tử là key (value là một đối tượng PRESENT dummy).
// Simplified HashSet internals
public class HashSet<E> {
private transient HashMap<E, Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
}Các Cân Nhắc Quan Trọng:
// ✅ NÊN: Sử dụng các đối tượng immutable hoặc effectively immutable
Set<String> names = new HashSet<>();
names.add("Alice");
// ❌ KHÔNG NÊN: Sửa đổi đối tượng sau khi thêm
class Person {
String name;
@Override
public int hashCode() {
return name.hashCode();
}
}
Person p = new Person("Alice");
Set<Person> people = new HashSet<>();
people.add(p);
p.name = "Bob"; // ⚠️ PHÁ VỠ SET - không thể tìm thấy 'p' nữa!LinkedHashSet: Thứ Tự Theo Insertion
Tốt nhất cho: Khi bạn cần thứ tự iteration dự đoán được với ngữ nghĩa Set.
Set<String> orderedUsers = new LinkedHashSet<>();
orderedUsers.add("charlie");
orderedUsers.add("alice");
orderedUsers.add("bob");
// Iteration theo thứ tự chèn
for (String user : orderedUsers) {
System.out.println(user); // charlie, alice, bob
}Đặc Điểm Hiệu Suất:
- Giống
HashSet(O(1) cho add/contains/remove) - Bộ nhớ bổ sung: Duy trì doubly-linked list cho thứ tự
- Iteration: Thứ tự chèn dự đoán được
TreeSet: Thứ Tự Đã Sắp Xếp
Tốt nhất cho: Khi bạn cần các phần tử theo thứ tự đã sắp xếp.
Set<Integer> scores = new TreeSet<>();
scores.add(85);
scores.add(92);
scores.add(78);
// Iteration theo thứ tự đã sắp xếp
for (int score : scores) {
System.out.println(score); // 78, 85, 92
}
// Range operations
SortedSet<Integer> highScores = ((TreeSet<Integer>) scores).tailSet(80);
System.out.println(highScores); // [85, 92]Đặc Điểm Hiệu Suất:
- Add: O(log n)
- Contains: O(log n)
- Remove: O(log n)
- Iteration: O(n), theo thứ tự đã sắp xếp
- Implementation: Red-Black Tree
Yêu Cầu:
// Các phần tử phải là Comparable HOẶC cung cấp Comparator
Set<Person> people = new TreeSet<>(
Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
);Bảng So Sánh Set
| Feature | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| Ordering | Không có | Insertion | Đã sắp xếp |
| Add/Contains/Remove | O(1) | O(1) | O(log n) |
| Memory | Thấp | Trung bình | Trung bình |
| Use case | Tra cứu nhanh | Thứ tự dự đoán được | Dữ liệu đã sắp xếp |
Map Interface: Cặp Key-Value
Map lưu trữ các liên kết key-value với key duy nhất.
HashMap: Nhanh, Không Có Thứ Tự
Tốt nhất cho: Tra cứu dựa trên key nhanh, không cần thứ tự.
Map<String, Integer> ages = new HashMap<>();
// O(1) - Put key-value
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Alice", 31); // Ghi đè giá trị trước
// O(1) - Get by key
Integer aliceAge = ages.get("Alice");
// O(1) - Check key existence
boolean hasAlice = ages.containsKey("Alice");
// O(n) - Check value existence (không hiệu quả!)
boolean hasAge30 = ages.containsValue(30);
// O(1) - Remove by key
ages.remove("Bob");
// Iterate over entries
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}Đặc Điểm Hiệu Suất:
- Put/Get/Remove: O(1) trung bình, O(n) trường hợp xấu nhất (hash collision)
- ContainsKey: O(1)
- ContainsValue: O(n)
- Iteration: O(n)
Cơ Chế Nội Bộ:
// Simplified HashMap internals
public class HashMap<K, V> {
transient Node<K, V>[] table; // Array of buckets
static class Node<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next; // Chain for collisions
}
public V put(K key, V value) {
int hash = hash(key);
int bucket = hash & (table.length - 1);
// Handle collision with chaining or tree
// ...
}
}Xử Lý Hash Collision:
- Java 8+: Các bucket chuyển từ linked list thành red-black tree khi độ dài chuỗi vượt quá 8
- Load factor: Mặc định 0.75 - resize khi đầy 75%
- Initial capacity: Mặc định 16, tăng gấp đôi khi resize
Best Practices:
// ✅ NÊN: Chỉ định capacity ban đầu cho các map lớn
Map<String, User> users = new HashMap<>(10000);
// ✅ NÊN: Sử dụng compute methods để cập nhật atomic
map.compute("key", (k, v) -> v == null ? 1 : v + 1);
// ✅ NÊN: Sử dụng getOrDefault để tránh kiểm tra null
int count = map.getOrDefault("key", 0);
// ❌ KHÔNG NÊN: Sử dụng containsKey + get (hai lần tra cứu)
if (map.containsKey("key")) {
return map.get("key"); // Tra cứu lần thứ hai!
}
// ✅ NÊN: Tra cứu một lần
String value = map.get("key");
if (value != null) {
return value;
}Các Method Hiện Đại Java 8+:
Map<String, Integer> scores = new HashMap<>();
// putIfAbsent - chỉ thêm nếu key không tồn tại
scores.putIfAbsent("Alice", 100);
// computeIfAbsent - khởi tạo lazy
Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("admin", k -> new ArrayList<>()).add("Alice");
// merge - kết hợp các giá trị
scores.merge("Bob", 10, Integer::sum); // Thêm 10 vào giá trị hiện cóLinkedHashMap: Map Có Thứ Tự Insertion
Tốt nhất cho: Khi bạn cần thứ tự iteration dự đoán được (thứ tự chèn hoặc truy cập).
Map<String, Integer> cache = new LinkedHashMap<>();
cache.put("first", 1);
cache.put("second", 2);
cache.put("third", 3);
// Iteration theo thứ tự chèn
cache.forEach((k, v) -> System.out.println(k + ": " + v));
// Output: first: 1, second: 2, third: 3Implementation LRU Cache:
// LinkedHashMap theo access-order (LRU cache)
Map<String, String> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > MAX_ENTRIES;
}
};
lruCache.put("A", "1");
lruCache.put("B", "2");
lruCache.put("C", "3");
lruCache.get("A"); // Di chuyển A đến cuối (được sử dụng gần đây nhất)
lruCache.put("D", "4"); // Loại bỏ B (ít được sử dụng gần đây nhất)TreeMap: Map Đã Sắp Xếp
Tốt nhất cho: Khi bạn cần key theo thứ tự đã sắp xếp hoặc range queries.
TreeMap<Integer, String> sortedMap = new TreeMap<>();
sortedMap.put(3, "three");
sortedMap.put(1, "one");
sortedMap.put(2, "two");
// Iteration theo thứ tự key
sortedMap.forEach((k, v) -> System.out.println(k + ": " + v));
// Output: 1: one, 2: two, 3: three
// Range queries
SortedMap<Integer, String> subMap = sortedMap.subMap(1, 3); // [1, 3)
Map.Entry<Integer, String> firstEntry = sortedMap.firstEntry();
Map.Entry<Integer, String> lastEntry = sortedMap.lastEntry();
// Ceiling/Floor operations
Integer ceiling = sortedMap.ceilingKey(2); // Key nhỏ nhất >= 2
Integer floor = sortedMap.floorKey(2); // Key lớn nhất <= 2Đặc Điểm Hiệu Suất:
- Put/Get/Remove: O(log n)
- Iteration: O(n), theo thứ tự đã sắp xếp
- Range operations: O(log n + m) trong đó m là kích thước kết quả
ConcurrentHashMap: Map An Toàn Luồng
Tốt nhất cho: Các scenario có concurrency cao với nhiều thread.
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
// Các thao tác an toàn luồng không cần synchronization bên ngoài
concurrentMap.put("key", 1);
concurrentMap.compute("key", (k, v) -> v == null ? 1 : v + 1);
// Bulk operations
concurrentMap.forEach(10, (k, v) -> {
// Parallel forEach với ngưỡng parallelism
System.out.println(k + ": " + v);
});
// Atomic updates
concurrentMap.computeIfAbsent("counter", k -> 0);
concurrentMap.computeIfPresent("counter", (k, v) -> v + 1);Sự Khác Biệt Chính so với HashMap:
- Lock-free reads: Hầu hết các thao tác get không cần lock
- Fine-grained locking: Ghi chỉ lock các segment cụ thể
- Weak consistency: Iterator phản ánh trạng thái tại một thời điểm, có thể không thấy các cập nhật đồng thời
- Không có null key/value: Không giống
HashMap, null không được phép
Hiệu Suất:
// ConcurrentHashMap scale tốt hơn với nhiều thread
// Single thread: HashMap ~= ConcurrentHashMap
// Multiple threads: ConcurrentHashMap >> Collections.synchronizedMap(HashMap)Bảng So Sánh Map
| Feature | HashMap | LinkedHashMap | TreeMap | ConcurrentHashMap |
|---|---|---|---|---|
| Ordering | Không có | Insertion/Access | Đã sắp xếp | Không có |
| Get/Put | O(1) | O(1) | O(log n) | O(1) |
| Thread-safe | ❌ | ❌ | ❌ | ✅ |
| Null keys | ✅ (một) | ✅ (một) | ❌ | ❌ |
| Use case | Chung | LRU cache | Key đã sắp xếp | Concurrent |
Queue Interface: FIFO và Priority
Queue xử lý các phần tử theo thứ tự cụ thể (FIFO, priority, v.v.).
ArrayDeque: Queue Hai Đầu
Tốt nhất cho: Thao tác queue hoặc stack (nhanh hơn LinkedList và Stack).
Deque<String> deque = new ArrayDeque<>();
// Sử dụng như Queue (FIFO)
deque.offer("first");
deque.offer("second");
String first = deque.poll(); // "first"
// Sử dụng như Stack (LIFO)
deque.push("A");
deque.push("B");
String top = deque.pop(); // "B"
// Thao tác hai đầu
deque.addFirst("head");
deque.addLast("tail");
String head = deque.removeFirst();
String tail = deque.removeLast();Đặc Điểm Hiệu Suất:
- Add/Remove ở hai đầu: O(1) amortized
- Truy cập ngẫu nhiên: Không được hỗ trợ (không phải mục đích sử dụng)
- Memory: Hiệu quả hơn
LinkedList(không có node overhead)
Tại Sao ArrayDeque Thay Vì LinkedList?
// Ưu điểm của ArrayDeque:
// 1. Memory overhead thấp hơn (không có con trỏ node)
// 2. Locality cache tốt hơn (mảng liên tiếp)
// 3. Chèn/xóa nhanh hơn ở các đầu
Deque<Integer> deque = new ArrayDeque<>(); // ✅ Được ưu tiên
LinkedList<Integer> list = new LinkedList<>(); // ❌ Ít hiệu quả hơn với vai trò dequePriorityQueue: Priority Queue Dựa Trên Heap
Tốt nhất cho: Xử lý các phần tử theo mức độ ưu tiên (không phải thứ tự chèn).
// Min-heap mặc định (phần tử nhỏ nhất trước)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(5);
minHeap.offer(1);
minHeap.offer(3);
System.out.println(minHeap.poll()); // 1
System.out.println(minHeap.poll()); // 3
System.out.println(minHeap.poll()); // 5
// Max-heap với comparator tùy chỉnh
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(
Comparator.reverseOrder()
);
// Đối tượng tùy chỉnh với priority
class Task implements Comparable<Task> {
String name;
int priority;
@Override
public int compareTo(Task other) {
return Integer.compare(this.priority, other.priority);
}
}
PriorityQueue<Task> taskQueue = new PriorityQueue<>();
taskQueue.offer(new Task("Low", 3));
taskQueue.offer(new Task("High", 1));
taskQueue.offer(new Task("Medium", 2));
Task urgent = taskQueue.poll(); // Trả về "High" (priority 1)Đặc Điểm Hiệu Suất:
- Offer (add): O(log n)
- Poll (remove): O(log n)
- Peek: O(1)
- Implementation: Binary heap (dựa trên mảng)
Lưu Ý Quan Trọng:
// ❌ BẪY: Thứ tự iteration KHÔNG phải thứ tự ưu tiên!
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5);
pq.offer(1);
pq.offer(3);
for (Integer i : pq) {
System.out.println(i); // KHÔNG được đảm bảo là 1, 3, 5!
}
// ✅ ĐÚNG: Sử dụng poll() cho thứ tự ưu tiên
while (!pq.isEmpty()) {
System.out.println(pq.poll()); // 1, 3, 5 (được đảm bảo)
}BlockingQueue: Queue An Toàn Luồng
Tốt nhất cho: Các pattern producer-consumer với điều phối luồng.
import java.util.concurrent.*;
// Bounded queue - block khi đầy
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
// Producer thread
new Thread(() -> {
try {
queue.put("item"); // Block nếu queue đầy
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// Consumer thread
new Thread(() -> {
try {
String item = queue.take(); // Block nếu queue rỗng
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();Các Implementation Phổ Biến:
- ArrayBlockingQueue: Bounded, dựa trên mảng
- LinkedBlockingQueue: Optionally bounded, dựa trên node
- PriorityBlockingQueue: Unbounded, theo thứ tự ưu tiên
- DelayQueue: Các phần tử có sẵn sau delay
So Sánh Hiệu Suất: Ký Hiệu Big O
Tóm Tắt Độ Phức Tạp Thời Gian
| Collection | Add | Remove | Get | Contains | Iteration |
|---|---|---|---|---|---|
| ArrayList | O(1)* | O(n) | O(1) | O(n) | O(n) |
| LinkedList | O(1) | O(1)** | O(n) | O(n) | O(n) |
| HashSet | O(1) | O(1) | - | O(1) | O(n) |
| TreeSet | O(log n) | O(log n) | - | O(log n) | O(n) |
| HashMap | O(1) | O(1) | O(1) | O(1)† | O(n) |
| TreeMap | O(log n) | O(log n) | O(log n) | O(log n)† | O(n) |
| PriorityQueue | O(log n) | O(log n) | O(1)‡ | O(n) | O(n) |
| ArrayDeque | O(1) | O(1) | - | - | O(n) |
*Amortized - Thỉnh thoảng O(n) khi resize
**Tại vị trí đã biết - O(n) để tìm vị trí
†containsKey cho maps
‡chỉ peek - không phải get bất kỳ
Độ Phức Tạp Không Gian
| Collection | Space Complexity | Overhead |
|---|---|---|
| ArrayList | O(n) | Thấp - chỉ mảng |
| LinkedList | O(n) | Cao - 2 con trỏ mỗi node |
| HashSet/HashMap | O(n) | Trung bình - hash table + chains |
| TreeSet/TreeMap | O(n) | Trung bình - tree nodes |
Chọn Collection Phù Hợp
Cây Quyết Định
Cần cặp key-value?
├─ CÓ → Map
│ ├─ Cần sắp xếp? → TreeMap
│ ├─ Cần thứ tự chèn? → LinkedHashMap
│ ├─ Thread-safe? → ConcurrentHashMap
│ └─ Mặc định → HashMap
│
└─ KHÔNG → Collection
├─ Cần tính duy nhất?
│ ├─ CÓ → Set
│ │ ├─ Cần sắp xếp? → TreeSet
│ │ ├─ Cần thứ tự chèn? → LinkedHashSet
│ │ └─ Mặc định → HashSet
│ │
│ └─ KHÔNG → List hoặc Queue
│ ├─ Cần FIFO/LIFO? → Queue
│ │ ├─ Dựa trên priority? → PriorityQueue
│ │ ├─ Thread-safe? → BlockingQueue
│ │ └─ Mặc định → ArrayDeque
│ │
│ └─ Collection có thứ tự? → List
│ ├─ Truy cập ngẫu nhiên thường xuyên? → ArrayList
│ └─ Thao tác head/tail thường xuyên? → LinkedListVí Dụ Use Case
// 1. User session cache (bounded, access-order)
Map<String, Session> sessions = new LinkedHashMap<>(1000, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry<String, Session> eldest) {
return size() > 1000;
}
};
// 2. Unique visitor tracking (fast membership)
Set<String> uniqueVisitors = new HashSet<>();
// 3. Leaderboard (sorted scores)
TreeMap<Integer, String> leaderboard = new TreeMap<>(Comparator.reverseOrder());
// 4. Task queue (priority-based)
PriorityQueue<Task> taskQueue = new PriorityQueue<>(
Comparator.comparing(Task::getPriority)
);
// 5. Event processing (FIFO)
Deque<Event> eventQueue = new ArrayDeque<>();
// 6. Thread-safe counter
Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
counters.computeIfAbsent("requests", k -> new AtomicInteger()).incrementAndGet();
// 7. Sorted time-series data
TreeMap<Instant, Double> timeSeries = new TreeMap<>();
SortedMap<Instant, Double> lastHour = timeSeries.tailMap(
Instant.now().minus(1, ChronoUnit.HOURS)
);
// 8. Batch processing (ArrayList cho hiệu suất iteration)
List<Record> batch = new ArrayList<>(1000);
for (Record record : batch) {
process(record); // Truy cập tuần tự nhanh
}Best Practices
1. Khởi Tạo Với Capacity
// ✅ TỐT: Tránh overhead resize
List<String> items = new ArrayList<>(expectedSize);
Map<String, Integer> map = new HashMap<>(expectedSize);
// ❌ TỆ: Nhiều lần resize khi phát triển
List<String> items = new ArrayList<>(); // Mặc định 10
for (int i = 0; i < 10000; i++) {
items.add(String.valueOf(i)); // Resize ~14 lần!
}2. Sử Dụng Kiểu Interface
// ✅ TỐT: Linh hoạt, cho phép thay đổi implementation
List<String> names = new ArrayList<>();
Set<Integer> ids = new HashSet<>();
Map<String, User> users = new HashMap<>();
// ❌ TỆ: Gắn chặt với implementation
ArrayList<String> names = new ArrayList<>();
HashSet<Integer> ids = new HashSet<>();3. Iteration Fail-Fast vs Fail-Safe
// ❌ BẪY: ConcurrentModificationException
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : list) {
if (item.equals("B")) {
list.remove(item); // ❌ Ném exception!
}
}
// ✅ GIẢI PHÁP 1: Sử dụng Iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
if (item.equals("B")) {
it.remove(); // ✅ An toàn
}
}
// ✅ GIẢI PHÁP 2: Sử dụng removeIf (Java 8+)
list.removeIf(item -> item.equals("B"));
// ✅ GIẢI PHÁP 3: Thu thập và xóa (cho logic phức tạp)
List<String> toRemove = new ArrayList<>();
for (String item : list) {
if (shouldRemove(item)) {
toRemove.add(item);
}
}
list.removeAll(toRemove);4. Immutable vs Mutable
// ✅ Immutable collections (Java 9+)
List<String> immutable = List.of("A", "B", "C");
Set<Integer> immutableSet = Set.of(1, 2, 3);
Map<String, Integer> immutableMap = Map.of("A", 1, "B", 2);
// Immutable collections:
// - Thread-safe mặc định
// - Không có rủi ro sửa đổi vô tình
// - Bộ nhớ hiệu quả hơn (không có capacity không sử dụng)
// - Ném UnsupportedOperationException khi cố sửa đổi
// ❌ BẪY: Collections.unmodifiableList() chỉ là một view
List<String> original = new ArrayList<>(Arrays.asList("A", "B"));
List<String> unmodifiable = Collections.unmodifiableList(original);
original.add("C"); // ⚠️ Cũng ảnh hưởng unmodifiable!5. Equals và HashCode
// ✅ PHẢI implement cả equals() và hashCode() cho hash-based collections
class User {
private String id;
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
// ❌ BẪY: Vi phạm contract equals/hashCode
Set<User> users = new HashSet<>();
users.add(new User("1", "Alice"));
users.contains(new User("1", "Alice")); // Có thể trả về false nếu không có equals/hashCode đúng!6. Xử Lý Null
// Collections cho phép null
List<String> list = new ArrayList<>();
list.add(null); // ✅ OK
Map<String, Integer> map = new HashMap<>();
map.put(null, 1); // ✅ Một null key được phép
map.put("key", null); // ✅ Null values được phép
// Collections KHÔNG cho phép null
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add(null); // ❌ NullPointerException
TreeMap<String, Integer> treeMap = new TreeMap<>();
treeMap.put(null, 1); // ❌ NullPointerException
ConcurrentHashMap<String, Integer> concurrent = new ConcurrentHashMap<>();
concurrent.put(null, 1); // ❌ NullPointerException
concurrent.put("key", null); // ❌ NullPointerExceptionCác Cân Nhắc Về Thread-Safety
Tùy Chọn 1: Synchronized Wrappers (Legacy)
// ❌ Hiệu suất kém - coarse-grained locking
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// Phải synchronize thủ công cho iteration
synchronized (syncMap) {
for (Map.Entry<String, Integer> entry : syncMap.entrySet()) {
// Process entry
}
}Tùy Chọn 2: Concurrent Collections (Được Khuyến Nghị)
// ✅ Hiệu suất tốt hơn - fine-grained locking
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
BlockingQueue<Task> queue = new LinkedBlockingQueue<>();
// Không cần synchronization bên ngoài
concurrentMap.put("key", 1);
cowList.add("item");Tùy Chọn 3: Immutable Collections
// ✅ Thread-safe tự nhiên (không thể sửa đổi)
List<String> immutable = List.of("A", "B", "C");
// An toàn để chia sẻ giữa các thread mà không cần synchronizationKhi Nào Sử Dụng Mỗi Loại
| Scenario | Solution |
|---|---|
| Cập nhật hiếm, nhiều lần đọc | CopyOnWriteArrayList/Set |
| Cập nhật đồng thời thường xuyên | ConcurrentHashMap |
| Producer-consumer | BlockingQueue |
| Dữ liệu chỉ đọc | Immutable collections |
| Legacy code | Collections.synchronized*() |
Các Bẫy Phổ Biến
1. Chuyển Đổi Mảng
List<String> list = Arrays.asList("A", "B", "C");
list.add("D"); // ❌ UnsupportedOperationException - fixed-size!
// ✅ Tạo bản sao có thể thay đổi
List<String> mutableList = new ArrayList<>(Arrays.asList("A", "B", "C"));
mutableList.add("D"); // ✅ OK2. Sửa Đổi Sublist
List<String> original = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
List<String> sublist = original.subList(1, 3); // ["B", "C"] - một view!
sublist.clear(); // Xóa sublist
System.out.println(original); // ["A", "D"] - ⚠️ Original bị sửa đổi!3. Type Erasure Với Generics
// ❌ Không thể tạo mảng generic
List<String>[] arrayOfLists = new ArrayList<String>[10]; // Lỗi compile
// ✅ Sử dụng List của Lists thay thế
List<List<String>> listOfLists = new ArrayList<>();4. Sự Không Nhất Quán Của Comparator
// ❌ BẪY: Comparator không nhất quán với equals
class Person {
String name;
int age;
@Override
public boolean equals(Object o) {
// So sánh cả name và age
}
}
// TreeSet sử dụng comparator chỉ so sánh age
TreeSet<Person> people = new TreeSet<>(Comparator.comparing(Person::getAge));
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 30)); // ⚠️ Không được thêm - cùng age!
// TreeSet nghĩ chúng bằng nhau, nhưng equals() nói chúng khác nhauBenchmark Hiệu Suất
Kết quả microbenchmark (xấp xỉ, phụ thuộc JVM):
ArrayList.get(index): ~2 ns
LinkedList.get(index): ~100 ns (chậm hơn 50 lần!)
HashMap.get(key): ~10 ns
TreeMap.get(key): ~30 ns
ConcurrentHashMap.get(key): ~15 ns
ArrayList.add(end): ~5 ns
LinkedList.add(end): ~20 ns
ArrayDeque.add(end): ~5 ns
HashSet.contains(): ~10 ns
TreeSet.contains(): ~30 nsNhững Điểm Chính:
- Các cấu trúc dựa trên mảng (ArrayList, ArrayDeque) có cache locality tốt nhất
- Các cấu trúc dựa trên hash (HashMap, HashSet) xuất sắc trong tra cứu
- Các cấu trúc dựa trên tree (TreeMap, TreeSet) cung cấp thứ tự với chi phí O(log n)
- Các cấu trúc liên kết (LinkedList) có overhead từ pointer chasing
Kết Luận
Java Collections Framework cung cấp các cấu trúc dữ liệu mạnh mẽ, được thiết kế tốt cho mọi use case:
- Chọn ArrayList cho hầu hết nhu cầu list - nhanh, hiệu quả, đa năng
- Chọn HashMap cho tra cứu key-value nhanh không cần thứ tự
- Chọn HashSet cho kiểm tra tính duy nhất và kiểm tra membership nhanh
- Chọn TreeMap/TreeSet khi bạn cần dữ liệu đã sắp xếp hoặc range queries
- Chọn ArrayDeque cho thao tác queue/stack (không phải LinkedList!)
- Chọn ConcurrentHashMap cho các scenario có concurrency cao
- Chọn PriorityQueue cho xử lý dựa trên priority
Làm chủ các collection này, hiểu đặc điểm hiệu suất của chúng, và ứng dụng Java của bạn sẽ nhanh hơn, dễ bảo trì hơn và mạnh mẽ hơn.
Thuộc series Lộ Trình Học Java
Quay lại: Giai Đoạn 3: Core Java APIs
Các Deep Dive Liên Quan:
- Streams & Lập Trình Hàm
- Exception Handling Best Practices
- Java Generics Giải Thích
- Modules và Build Tools
Bước Tiếp Theo: Bắt Đầu Với Spring Boot
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.