Best Practices với Collections
Sau bài này, bạn sẽ:
- Biết cách chọn đúng collection type dựa trên use case (ArrayList, HashMap, HashSet...)
- Hiểu programming to interface và tại sao return immutable/defensive copy
- Nắm được capacity planning, bulk operations, và tránh autoboxing overhead
- Tránh common mistakes: modify while iterating, không override equals/hashCode, raw types
- Áp dụng thread-safety với synchronized wrappers và concurrent collections
Bài trước: Iterator, Comparable và Comparator — Đã học về Iterator và sorting. Bài này sẽ tổng kết best practices và common pitfalls khi làm việc với Collections Framework.
Chọn đúng Collection Type
Decision Flowchart - Mermaid Diagram
Giải thích màu sắc:
- Xanh lá (Green): DEFAULT choices - dùng trong 80% trường hợp
- Vàng (Gold): Queue/Stack operations - ArrayDeque là best choice
- Xanh dương (Blue): Special use case - PriorityQueue cho priority ordering
Quick Reference Table
| Use Case | Best Choice | Why |
|---|---|---|
| General list | ArrayList | Fast random access, most common |
| Frequent add/remove at head | LinkedList or ArrayDeque | O(1) operations |
| Unique elements | HashSet | Fast O(1) contains/add |
| Unique + ordered | LinkedHashSet | Maintains insertion order |
| Unique + sorted | TreeSet | Auto-sorted |
| Key-value lookup | HashMap | Fast O(1) get/put |
| Key-value + ordered | LinkedHashMap | Maintains insertion order |
| Key-value + sorted | TreeMap | Auto-sorted by keys |
| Stack operations | ArrayDeque | Faster than Stack class |
| Queue operations | ArrayDeque | Faster than LinkedList |
| Priority queue | PriorityQueue | Min-heap by default |
| LRU cache | LinkedHashMap | Access-order mode |
Program to Interface
Luôn luôn khai báo biến với interface type, không phải implementation type.
// ✅ TỐT: Program to interface
List<String> list = new ArrayList<>();
Set<Integer> set = new HashSet<>();
Map<String, User> map = new HashMap<>();
Queue<Task> queue = new ArrayDeque<>();
// ❌ KHÔNG TỐT: Program to implementation
ArrayList<String> list = new ArrayList<>();
HashSet<Integer> set = new HashSet<>();
HashMap<String, User> map = new HashMap<>();
ArrayDeque<Task> queue = new ArrayDeque<>();
Tại sao?
- Flexibility: Dễ dàng thay đổi implementation
- Abstraction: Code phụ thuộc vào interface, không phụ thuộc implementation
- Best practice: Follow coding standard
// Example: Easy to switch implementation
public class UserService {
// Interface type cho parameter và return type
public List<User> getUsers() {
// Có thể đổi từ ArrayList sang LinkedList mà không ảnh hưởng caller
return new ArrayList<>(users);
}
public void addUsers(List<User> newUsers) {
// Caller có thể pass bất kỳ List implementation nào
users.addAll(newUsers);
}
}
Chỉ dùng concrete type khi cần features đặc thù của implementation:
// OK: Cần LinkedHashMap's access-order feature
LinkedHashMap<String, Data> lruCache = new LinkedHashMap<>(100, 0.75f, true);
// OK: Cần ArrayDeque's deque operations
ArrayDeque<String> deque = new ArrayDeque<>();
String last = deque.peekLast(); // Không có trong Queue interface
// OK: Cần TreeMap's NavigableMap methods
TreeMap<Integer, String> treeMap = new TreeMap<>();
Integer lowerKey = treeMap.lowerKey(50);
Null Handling Contract - Bảng tham chiếu nhanh
Ẩn dụ: Tưởng tượng mỗi collection là một cái hộp (box) với quy tắc riêng về việc chấp nhận "phong bì rỗng" (null). Một số hộp như ArrayList chấp nhận nhiều phong bì rỗng, một số như HashSet chỉ chấp nhận 1, một số như TreeSet không chấp nhận phong bì rỗng nào cả!
| Collection | null elements | null keys | null values | Ghi chú |
|---|---|---|---|---|
| ArrayList | ✅ Yes (nhiều) | N/A | N/A | Cho phép bao nhiêu null cũng được |
| LinkedList | ✅ Yes (nhiều) | N/A | N/A | Cho phép bao nhiêu null cũng được |
| HashSet | ✅ 1 null | N/A | N/A | Chỉ 1 null (vì Set không có duplicate) |
| LinkedHashSet | ✅ 1 null | N/A | N/A | Chỉ 1 null |
| TreeSet | ❌ No | N/A | N/A | NullPointerException khi compare null |
| HashMap | N/A | ✅ 1 null key | ✅ Yes (nhiều) | 1 null key, nhiều null values |
| LinkedHashMap | N/A | ✅ 1 null key | ✅ Yes (nhiều) | 1 null key, nhiều null values |
| TreeMap | N/A | ❌ No (Java 7+) | ✅ Yes (nhiều) | NullPointerException cho null key |
| Hashtable | N/A | ❌ No | ❌ No | Legacy, NullPointerException |
| PriorityQueue | ❌ No | N/A | N/A | NullPointerException khi compare |
| ArrayDeque | ❌ No | N/A | N/A | NullPointerException khi add null |
| ConcurrentHashMap | N/A | ❌ No | ❌ No | NullPointerException cho null key/value |
| List.of() | ❌ No | N/A | N/A | NullPointerException (immutable) |
| Set.of() | ❌ No | N/A | N/A | NullPointerException (immutable) |
| Map.of() | N/A | ❌ No | ❌ No | NullPointerException (immutable) |
Code examples minh họa
import java.util.*;
import java.util.concurrent.*;
public class NullHandlingDemo {
public static void main(String[] args) {
// ✅ ArrayList - allows multiple nulls
List<String> arrayList = new ArrayList<>();
arrayList.add(null);
arrayList.add(null);
arrayList.add(null);
System.out.println("ArrayList with nulls: " + arrayList); // [null, null, null]
// ✅ HashSet - allows 1 null
Set<String> hashSet = new HashSet<>();
hashSet.add(null);
hashSet.add(null); // Duplicate null, ignored
hashSet.add("A");
System.out.println("HashSet with null: " + hashSet); // [null, A]
// ❌ TreeSet - does NOT allow null
Set<String> treeSet = new TreeSet<>();
try {
treeSet.add(null); // NullPointerException!
} catch (NullPointerException e) {
System.out.println("TreeSet does not allow null: " + e.getClass().getSimpleName());
}
// ✅ HashMap - 1 null key, multiple null values
Map<String, String> hashMap = new HashMap<>();
hashMap.put(null, "value1"); // 1 null key OK
hashMap.put(null, "value2"); // Override previous null key
hashMap.put("A", null);
hashMap.put("B", null);
hashMap.put("C", null);
System.out.println("HashMap with nulls: " + hashMap); // {null=value2, A=null, B=null, C=null}
// ❌ TreeMap - does NOT allow null key (Java 7+)
Map<String, String> treeMap = new TreeMap<>();
try {
treeMap.put(null, "value"); // NullPointerException!
} catch (NullPointerException e) {
System.out.println("TreeMap does not allow null key: " + e.getClass().getSimpleName());
}
treeMap.put("A", null); // null value OK
System.out.println("TreeMap with null value: " + treeMap); // {A=null}
// ❌ PriorityQueue - does NOT allow null
Queue<Integer> pq = new PriorityQueue<>();
try {
pq.add(null); // NullPointerException!
} catch (NullPointerException e) {
System.out.println("PriorityQueue does not allow null: " + e.getClass().getSimpleName());
}
// ❌ ArrayDeque - does NOT allow null
Deque<String> deque = new ArrayDeque<>();
try {
deque.add(null); // NullPointerException!
} catch (NullPointerException e) {
System.out.println("ArrayDeque does not allow null: " + e.getClass().getSimpleName());
}
// ❌ ConcurrentHashMap - does NOT allow null key or value
Map<String, String> concurrentMap = new ConcurrentHashMap<>();
try {
concurrentMap.put(null, "value"); // NullPointerException!
} catch (NullPointerException e) {
System.out.println("ConcurrentHashMap does not allow null key: " + e.getClass().getSimpleName());
}
try {
concurrentMap.put("key", null); // NullPointerException!
} catch (NullPointerException e) {
System.out.println("ConcurrentHashMap does not allow null value: " + e.getClass().getSimpleName());
}
// ❌ List.of() - does NOT allow null
try {
List<String> immutableList = List.of("A", null, "C"); // NullPointerException!
} catch (NullPointerException e) {
System.out.println("List.of() does not allow null: " + e.getClass().getSimpleName());
}
// ❌ Map.of() - does NOT allow null key or value
try {
Map<String, String> immutableMap = Map.of(null, "value"); // NullPointerException!
} catch (NullPointerException e) {
System.out.println("Map.of() does not allow null: " + e.getClass().getSimpleName());
}
}
}
Sorted collections (TreeSet, TreeMap, PriorityQueue) KHÔNG cho phép null vì cần compare elements → null.compareTo(x) → NullPointerException!
Concurrent collections (ConcurrentHashMap) KHÔNG cho phép null vì ambiguity: map.get(key) == null có nghĩa là key không tồn tại hay value là null?
Immutable collections (List.of(), Set.of(), Map.of()) KHÔNG cho phép null theo design của Java 9+.
Legacy Hashtable KHÔNG cho phép null (design cũ).
Tất cả collections khác (ArrayList, LinkedList, HashSet, HashMap, LinkedHashMap) CHO PHÉP null.
Immutable Collections
Tại sao dùng Immutable Collections?
- Thread-safe: Không cần synchronization
- Security: Không thể bị modify từ bên ngoài
- Caching: An toàn để cache
- Defensive programming: Tránh unintended modifications
Cách tạo Immutable Collections
// Java 9+: List.of(), Set.of(), Map.of()
List<String> immutableList = List.of("A", "B", "C");
Set<Integer> immutableSet = Set.of(1, 2, 3);
Map<String, Integer> immutableMap = Map.of("A", 1, "B", 2);
// Nhiều entries: Map.ofEntries()
Map<String, Integer> map = Map.ofEntries(
Map.entry("A", 1),
Map.entry("B", 2),
Map.entry("C", 3),
Map.entry("D", 4)
);
// Java 8: Collections.unmodifiableList/Set/Map()
List<String> mutableList = new ArrayList<>(Arrays.asList("A", "B", "C"));
List<String> unmodifiableList = Collections.unmodifiableList(mutableList);
// Guava (third-party library)
ImmutableList<String> guavaList = ImmutableList.of("A", "B", "C");
ImmutableSet<Integer> guavaSet = ImmutableSet.of(1, 2, 3);
ImmutableMap<String, Integer> guavaMap = ImmutableMap.of("A", 1, "B", 2);
Collections.unmodifiableList() vs List.copyOf() - Deep Comparison
Ẩn dụ: Tưởng tượng bạn có một cuốn sách gốc (original list).
Collections.unmodifiableList(): Cho bạn một cái kính bảo vệ (glass cover) đặt lên cuốn sách → bạn không thể sửa qua kính, nhưng ai đó vẫn có thể lấy kính ra và sửa sách gốc → bạn nhìn qua kính cũng thấy thay đổi! (VIEW)List.copyOf(): Cho bạn một bản photocopy hoàn toàn mới của cuốn sách → ai sửa sách gốc cũng không ảnh hưởng bản copy của bạn! (TRUE COPY)
| Đặc điểm | Collections.unmodifiableList() | List.copyOf() | List.of() |
|---|---|---|---|
| Java version | Java 1.2+ | Java 10+ | Java 9+ |
| Truly immutable | ❌ No (VIEW wrapper) | ✅ Yes (TRUE COPY) | ✅ Yes |
| Original changes propagate | ✅ Yes (nguy hiểm!) | ❌ No | N/A |
| Null elements | ✅ Allowed | ❌ Not allowed | ❌ Not allowed |
| Performance | O(1) wrapper | O(n) copy | O(n) |
| Use case | Legacy compatibility | Defensive copy (Java 10+) | Creating new immutable list |
import java.util.*;
public class UnmodifiableVsCopyOfDemo {
public static void main(String[] args) {
// Original mutable list
List<String> original = new ArrayList<>(Arrays.asList("A", "B", "C"));
// 1. Collections.unmodifiableList() - VIEW wrapper
List<String> unmodifiable = Collections.unmodifiableList(original);
System.out.println("Original: " + original); // [A, B, C]
System.out.println("Unmodifiable: " + unmodifiable); // [A, B, C]
// ❌ NGUY HIỂM: Modify original → unmodifiable cũng thay đổi!
original.add("D");
System.out.println("\nAfter original.add(\"D\"):");
System.out.println("Original: " + original); // [A, B, C, D]
System.out.println("Unmodifiable: " + unmodifiable); // [A, B, C, D] <- CHANGED!
original.set(0, "X");
System.out.println("\nAfter original.set(0, \"X\"):");
System.out.println("Original: " + original); // [X, B, C, D]
System.out.println("Unmodifiable: " + unmodifiable); // [X, B, C, D] <- CHANGED!
// 2. List.copyOf() - TRUE COPY
original = new ArrayList<>(Arrays.asList("A", "B", "C"));
List<String> copyOf = List.copyOf(original);
System.out.println("\n--- List.copyOf() ---");
System.out.println("Original: " + original); // [A, B, C]
System.out.println("CopyOf: " + copyOf); // [A, B, C]
// ✅ AN TOÀN: Modify original → copyOf KHÔNG thay đổi!
original.add("D");
System.out.println("\nAfter original.add(\"D\"):");
System.out.println("Original: " + original); // [A, B, C, D]
System.out.println("CopyOf: " + copyOf); // [A, B, C] <- UNCHANGED!
original.set(0, "X");
System.out.println("\nAfter original.set(0, \"X\"):");
System.out.println("Original: " + original); // [X, B, C, D]
System.out.println("CopyOf: " + copyOf); // [A, B, C] <- UNCHANGED!
// 3. Null handling
List<String> withNull = new ArrayList<>(Arrays.asList("A", null, "C"));
// ✅ unmodifiableList() accepts null
List<String> unmodifiableWithNull = Collections.unmodifiableList(withNull);
System.out.println("\nUnmodifiable with null: " + unmodifiableWithNull); // [A, null, C]
// ❌ List.copyOf() does NOT accept null
try {
List<String> copyWithNull = List.copyOf(withNull); // NullPointerException!
} catch (NullPointerException e) {
System.out.println("List.copyOf() throws NPE for null elements");
}
}
}
// Output:
// Original: [A, B, C]
// Unmodifiable: [A, B, C]
//
// After original.add("D"):
// Original: [A, B, C, D]
// Unmodifiable: [A, B, C, D]
//
// After original.set(0, "X"):
// Original: [X, B, C, D]
// Unmodifiable: [X, B, C, D]
//
// --- List.copyOf() ---
// Original: [A, B, C]
// CopyOf: [A, B, C]
//
// After original.add("D"):
// Original: [A, B, C, D]
// CopyOf: [A, B, C]
//
// After original.set(0, "X"):
// Original: [X, B, C, D]
// CopyOf: [A, B, C]
//
// Unmodifiable with null: [A, null, C]
// List.copyOf() throws NPE for null elements
Khi nào dùng cái nào?
| Scenario | Recommendation |
|---|---|
| Return từ getter | List.copyOf() hoặc List.of() nếu tạo mới |
| Defensive copy trong constructor | List.copyOf() hoặc new ArrayList<>(original) |
| Wrap existing list tạm thời | Collections.unmodifiableList() (nhanh hơn) |
| Java 8 trở xuống | Collections.unmodifiableList() (copyOf chưa có) |
| Có null elements | Collections.unmodifiableList() (copyOf không hỗ trợ null) |
| True immutability cần thiết | List.copyOf() |
public class User {
private List<String> roles = new ArrayList<>();
// ❌ NGUY HIỂM: Caller có thể modify internal list qua reference gốc
public List<String> getRoles() {
return roles;
}
// ⚠️ KHÔNG AN TOÀN: unmodifiableList() là VIEW, nếu roles thay đổi thì returned list cũng thay đổi
public List<String> getRoles() {
return Collections.unmodifiableList(roles);
}
// ✅ TỐT: List.copyOf() - true defensive copy (Java 10+)
public List<String> getRoles() {
return List.copyOf(roles);
}
// ✅ TỐT: new ArrayList() - defensive copy (tất cả Java versions)
public List<String> getRoles() {
return new ArrayList<>(roles);
}
// ✅ TỐT NHẤT: Immutable copy (Java 10+)
public List<String> getRoles() {
return List.copyOf(roles); // Immutable + true copy
}
}
Câu hỏi: Đoạn code sau output gì?
List<String> original = new ArrayList<>(Arrays.asList("A", "B"));
List<String> unmodifiable = Collections.unmodifiableList(original);
original.add("C");
System.out.println(unmodifiable);
A) [A, B]
B) [A, B, C] ✅ ĐÚNG
C) UnsupportedOperationException
D) ConcurrentModificationException
Giải thích: Collections.unmodifiableList() chỉ là VIEW wrapper, modify original list → unmodifiable view cũng thấy thay đổi!
Khi return collection từ method, return immutable copy để tránh modification:
public class User {
private List<String> roles = new ArrayList<>();
// ❌ NGUY HIỂM: Caller có thể modify internal list
public List<String> getRoles() {
return roles;
}
// ✅ TỐT: Return immutable copy
public List<String> getRoles() {
return List.copyOf(roles); // Java 10+
// Hoặc: return Collections.unmodifiableList(roles);
}
// ✅ TỐT: Return defensive copy
public List<String> getRoles() {
return new ArrayList<>(roles);
}
}
Capacity Planning
Ẩn dụ: Tưởng tượng bạn tổ chức tiệc (party). Nếu book phòng quá nhỏ (small initial capacity) → khách đến đông → phải đổi phòng lớn hơn giữa chừng (resize) → tốn thời gian, tiền bạc, và effort! Tốt hơn là ước lượng số khách từ đầu và book phòng đủ lớn ngay → không phải dọn đồ giữa tiệc!
ArrayList Initial Capacity
ArrayList có initial capacity mặc định là 10. Khi đầy, capacity tăng 50% (new = old * 1.5).
// ❌ KHÔNG TỐT: Nhiều resize operations
List<String> list = new ArrayList<>(); // capacity = 10
for (int i = 0; i < 10000; i++) {
list.add("Item " + i); // Resize nhiều lần: 10→15→22→33→49→73→109→...
}
// Resize sequence: 10 → 15 → 22 → 33 → 49 → 73 → 109 → 163 → 244 → 366 → 549 → 823 → 1234 → 1851 → 2776 → 4164 → 6246 → 9369 → 14053
// Total: ~19 resize operations, mỗi lần copy toàn bộ array → O(n) per resize!
// ✅ TỐT: Specify initial capacity
List<String> list = new ArrayList<>(10000);
for (int i = 0; i < 10000; i++) {
list.add("Item " + i); // Không resize
}
// Total: 0 resize operations!
HashMap Initial Capacity và Load Factor
HashMap có:
- Initial capacity: Mặc định 16
- Load factor: Mặc định 0.75
Resize khi size > capacity * loadFactor
Công thức tính capacity tối ưu:
capacity = expectedSize / loadFactor + 1
= expectedSize / 0.75 + 1
≈ expectedSize * 1.34
// ❌ KHÔNG TỐT: Default capacity cho large map
Map<String, String> users = new HashMap<>(); // capacity = 16
for (int i = 0; i < 10000; i++) {
users.put("user" + i, "User " + i); // Resize nhiều lần
}
// Resize khi size > 16 * 0.75 = 12
// Resize sequence: 16 → 32 → 64 → 128 → 256 → 512 → 1024 → 2048 → 4096 → 8192 → 16384
// Total: ~10 resize operations, mỗi lần rehash toàn bộ entries → O(n) per resize!
// ✅ TỐT: Calculate capacity
int expectedSize = 10000;
int capacity = (int) (expectedSize / 0.75 + 1); // = 13334
Map<String, String> users = new HashMap<>(capacity);
for (int i = 0; i < 10000; i++) {
users.put("user" + i, "User " + i); // Không resize hoặc resize rất ít
}
// ✅ TỐT HƠN: Dùng helper method
Map<String, String> users = new HashMap<>(calculateCapacity(10000));
private static int calculateCapacity(int expectedSize) {
return (int) Math.ceil(expectedSize / 0.75);
}
Performance Comparison với và không có Initial Capacity
import java.util.*;
public class CapacityPlanningBenchmark {
public static void main(String[] args) {
final int SIZE = 1_000_000;
// Test 1: ArrayList without initial capacity
long start1 = System.nanoTime();
List<Integer> list1 = new ArrayList<>();
for (int i = 0; i < SIZE; i++) {
list1.add(i);
}
long time1 = System.nanoTime() - start1;
// Test 2: ArrayList WITH initial capacity
long start2 = System.nanoTime();
List<Integer> list2 = new ArrayList<>(SIZE);
for (int i = 0; i < SIZE; i++) {
list2.add(i);
}
long time2 = System.nanoTime() - start2;
System.out.println("=== ArrayList Benchmark ===");
System.out.println("Without initial capacity: " + time1 / 1_000_000 + " ms");
System.out.println("With initial capacity: " + time2 / 1_000_000 + " ms");
System.out.println("Speedup: " + (time1 / (double) time2) + "x faster");
// Test 3: HashMap without initial capacity
long start3 = System.nanoTime();
Map<Integer, Integer> map1 = new HashMap<>();
for (int i = 0; i < SIZE; i++) {
map1.put(i, i);
}
long time3 = System.nanoTime() - start3;
// Test 4: HashMap WITH initial capacity
long start4 = System.nanoTime();
Map<Integer, Integer> map2 = new HashMap<>((int) (SIZE / 0.75 + 1));
for (int i = 0; i < SIZE; i++) {
map2.put(i, i);
}
long time4 = System.nanoTime() - start4;
System.out.println("\n=== HashMap Benchmark ===");
System.out.println("Without initial capacity: " + time3 / 1_000_000 + " ms");
System.out.println("With initial capacity: " + time4 / 1_000_000 + " ms");
System.out.println("Speedup: " + (time3 / (double) time4) + "x faster");
}
}
// Typical output:
// === ArrayList Benchmark ===
// Without initial capacity: 45 ms
// With initial capacity: 28 ms
// Speedup: 1.6x faster
//
// === HashMap Benchmark ===
// Without initial capacity: 180 ms
// With initial capacity: 95 ms
// Speedup: 1.9x faster
Kết quả thực nghiệm:
- ArrayList: ~1.5-2x faster với initial capacity
- HashMap: ~1.5-2x faster với initial capacity
- Memory: Giảm GC pressure (ít allocation/deallocation hơn)
Resize là expensive operation O(n):
- ArrayList: Phải copy toàn bộ array sang array mới lớn hơn
- HashMap: Phải rehash toàn bộ entries (recalculate hash, redistribute to new buckets)
Với large collections (> 1000 elements), LUÔN specify capacity ngay từ đầu!
Rule of thumb:
new ArrayList<>(expectedSize)new HashMap<>(expectedSize * 4 / 3 + 1)hoặcnew HashMap<>((int)(expectedSize / 0.75 + 1))
ArrayList Capacity Growth Diagram
Lesson: Resize operations are expensive (O(n) copy). Specify initial capacity khi biết trước size!
Capacity Planning Best Practices
public class CapacityBestPractices {
// ✅ TỐT: Method biết trước output size
public List<String> processItems(List<Item> items) {
List<String> result = new ArrayList<>(items.size()); // Exact size
for (Item item : items) {
result.add(item.getName());
}
return result;
}
// ✅ TỐT: Ước lượng size cho HashMap
public Map<String, User> loadUsers(List<User> users) {
int capacity = (int) (users.size() / 0.75 + 1);
Map<String, User> map = new HashMap<>(capacity);
for (User user : users) {
map.put(user.getId(), user);
}
return map;
}
// ✅ TỐT: Batch processing với known size
public Set<String> extractUniqueEmails(List<User> users) {
// Set capacity = estimate 50% unique emails
int capacity = (int) (users.size() * 0.5 / 0.75 + 1);
Set<String> emails = new HashSet<>(capacity);
for (User user : users) {
emails.add(user.getEmail());
}
return emails;
}
// ❌ KHÔNG TỐT: Không specify capacity cho large collection
public List<String> badMethod(int count) {
List<String> list = new ArrayList<>(); // Default 10
for (int i = 0; i < count; i++) { // count có thể là 1,000,000!
list.add("Item " + i); // Nhiều resize!
}
return list;
}
}
## Common Mistakes và Anti-patterns
### 1. Modify collection trong khi iterate
```java
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
// ❌ SAI: ConcurrentModificationException
for (Integer num : numbers) {
if (num % 2 == 0) {
numbers.remove(num); // EXCEPTION!
}
}
// ✅ ĐÚNG: Dùng Iterator
Iterator<Integer> iter = numbers.iterator();
while (iter.hasNext()) {
if (iter.next() % 2 == 0) {
iter.remove();
}
}
// ✅ ĐÚNG: Dùng removeIf() (Java 8+)
numbers.removeIf(num -> num % 2 == 0);
// ✅ ĐÚNG: Collect then remove
List<Integer> toRemove = new ArrayList<>();
for (Integer num : numbers) {
if (num % 2 == 0) toRemove.add(num);
}
numbers.removeAll(toRemove);
2. Không override equals() và hashCode() cho HashSet/HashMap
// ❌ SAI: Không override equals/hashCode
class Person {
String name;
int age;
}
Set<Person> people = new HashSet<>();
people.add(new Person("Alice", 25));
people.add(new Person("Alice", 25));
System.out.println(people.size()); // 2 - WRONG! (Should be 1)
// ✅ ĐÚNG: Override equals và hashCode
class Person {
String name;
int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
3. Dùng == thay vì equals() để compare
List<String> list = new ArrayList<>(Arrays.asList("Hello", "World"));
// ❌ SAI: Dùng ==
if (list.get(0) == "Hello") { // Có thể false! So sánh reference
System.out.println("Found");
}
// ✅ ĐÚNG: Dùng equals()
if ("Hello".equals(list.get(0))) { // So sánh value
System.out.println("Found");
}
4. Dùng raw type (không dùng Generics)
// ❌ SAI: Raw type
List list = new ArrayList();
list.add("String");
list.add(123); // OK, nhưng dễ gây bug
String s = (String) list.get(1); // ClassCastException at runtime!
// ✅ ĐÚNG: Dùng Generics
List<String> list = new ArrayList<>();
list.add("String");
// list.add(123); // Compile error - type-safe!
String s = list.get(0); // Không cần cast
5. Không check null
Map<String, User> userMap = new HashMap<>();
// ❌ SAI: Không check null
User user = userMap.get("alice");
String email = user.getEmail(); // NullPointerException nếu không tồn tại!
// ✅ ĐÚNG: Check null
User user = userMap.get("alice");
if (user != null) {
String email = user.getEmail();
}
// ✅ ĐÚNG HƠN: Dùng getOrDefault() (Java 8+)
User user = userMap.getOrDefault("alice", new User("Guest"));
String email = user.getEmail();
// ✅ ĐÚNG HƠN: Dùng Optional (Java 8+)
Optional<User> userOpt = Optional.ofNullable(userMap.get("alice"));
String email = userOpt.map(User::getEmail).orElse("[email protected]");
6. Dùng LinkedList khi nên dùng ArrayList
// ❌ KHÔNG TỐT: LinkedList cho general use
List<String> list = new LinkedList<>();
for (int i = 0; i < 10000; i++) {
list.add("Item " + i);
}
String item = list.get(5000); // O(n) - chậm!
// ✅ TỐT: ArrayList cho general use
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
list.add("Item " + i);
}
String item = list.get(5000); // O(1) - nhanh!
7. Dùng subList() mà không hiểu view behavior
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List<Integer> subList = list.subList(1, 4); // [2, 3, 4]
// ❌ NGUY HIỂM: subList là view, không phải copy
list.add(6); // Modify original list
// subList.get(0); // ConcurrentModificationException!
// ✅ TỐT: Tạo copy nếu cần independent list
List<Integer> subListCopy = new ArrayList<>(list.subList(1, 4));
list.add(6); // OK, không ảnh hưởng subListCopy
8. Inefficient iteration qua Map
Map<String, Integer> map = new HashMap<>();
// ... populate map
// ❌ KHÔNG HIỆU QUẢ: Iterate qua keySet và gọi get()
for (String key : map.keySet()) {
Integer value = map.get(key); // Extra O(1) lookup per iteration
System.out.println(key + " = " + value);
}
// ✅ HIỆU QUẢ: Iterate qua entrySet
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
// ✅ HIỆU QUẢ HƠN: forEach với lambda (Java 8+)
map.forEach((key, value) -> System.out.println(key + " = " + value));
Performance Tips
1. Chọn đúng Collection cho use case
// Frequent contains() checks → HashSet, không phải ArrayList
// ❌ CHẬM: O(n) cho contains()
List<String> allowedUsers = new ArrayList<>(Arrays.asList("alice", "bob", "charlie"));
if (allowedUsers.contains(userId)) { // O(n)
// ...
}
// ✅ NHANH: O(1) cho contains()
Set<String> allowedUsers = new HashSet<>(Arrays.asList("alice", "bob", "charlie"));
if (allowedUsers.contains(userId)) { // O(1)
// ...
}
2. Avoid autoboxing trong loops
// ❌ CHẬM: Autoboxing trong mỗi iteration
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
numbers.add(i); // int → Integer autoboxing
}
int sum = 0;
for (Integer num : numbers) {
sum += num; // Integer → int unboxing
}
// ✅ NHANH: Dùng primitive array nếu có thể
int[] numbers = new int[100000];
for (int i = 0; i < 100000; i++) {
numbers[i] = i;
}
int sum = 0;
for (int num : numbers) {
sum += num; // No boxing/unboxing
}
3. Bulk operations
List<String> source = Arrays.asList("A", "B", "C", "D");
List<String> target = new ArrayList<>();
// ❌ CHẬM: Add từng phần tử
for (String item : source) {
target.add(item);
}
// ✅ NHANH: Bulk operation
target.addAll(source);
// Remove multiple
Set<String> toRemove = new HashSet<>(Arrays.asList("B", "D"));
// ❌ CHẬM
for (String item : toRemove) {
target.remove(item);
}
// ✅ NHANH
target.removeAll(toRemove);
4. ArrayList vs LinkedList benchmark
// ArrayList: Fast random access, slow insert/delete
// LinkedList: Slow random access, fast insert/delete at head/tail
// Random access - ArrayList wins
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// Populate both with 100000 elements
// ArrayList: ~0ms for 10000 random gets
// LinkedList: ~50000ms for 10000 random gets
// Insert at beginning - LinkedList wins (if using LinkedList-specific methods)
// ArrayList: ~5000ms for 10000 inserts at index 0
// LinkedList.addFirst(): ~0ms for 10000 inserts at head
5. Stream vs for-loop
List<Integer> numbers = new ArrayList<>();
// Populate with millions of numbers
// Traditional for-loop: Fastest for simple operations
int sum = 0;
for (int num : numbers) {
sum += num;
}
// Stream: Slower for simple ops, but more readable and powerful
int sum = numbers.stream().mapToInt(Integer::intValue).sum();
// Parallel stream: Faster for heavy computations on large collections
int sum = numbers.parallelStream().mapToInt(Integer::intValue).sum();
- Small collections (< 1000 elements): Performance difference negligible, choose for readability
- Large collections (> 10000 elements): Choose based on operation patterns
- Profile first: Don't optimize prematurely, measure actual performance
ConcurrentHashMap vs Collections.synchronizedMap() - Deep Comparison
Ẩn dụ: Tưởng tượng một kho hàng (warehouse) lớn.
Collections.synchronizedMap(): Chỉ có 1 cửa vào/ra duy nhất (single lock) → mỗi lần chỉ 1 người vào kho được → chậm, dù kho rất rộng!ConcurrentHashMap: Kho được chia thành nhiều khu (segments/buckets) → nhiều người có thể vào các khu khác nhau cùng lúc → nhanh hơn nhiều!
ConcurrentHashMap vs synchronizedMap Locking Diagram
So sánh:
- synchronizedMap: 1 lock toàn bộ map → chỉ 1 thread tại 1 thời điểm → slow
- ConcurrentHashMap: Segment/bucket-level locks → nhiều threads đồng thời → fast
So sánh chi tiết
| Đặc điểm | Collections.synchronizedMap() | ConcurrentHashMap |
|---|---|---|
| Locking mechanism | Single lock toàn bộ map | Segment/bucket-level locking |
| Concurrency level | Thấp (1 thread tại 1 thời điểm) | Cao (nhiều threads đồng thời) |
| Performance | Chậm với high contention | Nhanh với high contention |
| Null key | Depends on backing map (HashMap cho phép 1 null) | ❌ Không cho phép |
| Null value | Depends on backing map (HashMap cho phép) | ❌ Không cho phép |
| Iteration | Cần manual synchronization | Không cần (weakly consistent) |
| fail-fast iterator | ✅ Yes (throws ConcurrentModificationException) | ❌ No (weakly consistent) |
| size() | Accurate | Approximate (không lock toàn bộ) |
| Java version | Java 1.2+ | Java 1.5+ |
| Use case | Legacy code, ít contention | Modern code, high concurrency |
Code minh họa
import java.util.*;
import java.util.concurrent.*;
public class SynchronizedMapVsConcurrentHashMap {
public static void main(String[] args) throws InterruptedException {
// 1. Collections.synchronizedMap()
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// 2. ConcurrentHashMap
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
// Test 1: Null handling
System.out.println("=== Null Handling ===");
// ✅ synchronizedMap allows 1 null key and null values (từ HashMap)
syncMap.put(null, 1);
syncMap.put("A", null);
System.out.println("synchronizedMap with nulls: " + syncMap); // {null=1, A=null}
// ❌ ConcurrentHashMap does NOT allow null key or value
try {
concurrentMap.put(null, 1); // NullPointerException!
} catch (NullPointerException e) {
System.out.println("ConcurrentHashMap null key: NPE");
}
try {
concurrentMap.put("A", null); // NullPointerException!
} catch (NullPointerException e) {
System.out.println("ConcurrentHashMap null value: NPE");
}
// Test 2: Iteration behavior
System.out.println("\n=== Iteration Behavior ===");
syncMap = Collections.synchronizedMap(new HashMap<>());
syncMap.put("A", 1);
syncMap.put("B", 2);
syncMap.put("C", 3);
concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("A", 1);
concurrentMap.put("B", 2);
concurrentMap.put("C", 3);
// ❌ synchronizedMap - cần manual sync khi iterate
synchronized (syncMap) {
for (Map.Entry<String, Integer> entry : syncMap.entrySet()) {
System.out.println("syncMap: " + entry.getKey() + " = " + entry.getValue());
}
}
// ✅ ConcurrentHashMap - không cần manual sync
for (Map.Entry<String, Integer> entry : concurrentMap.entrySet()) {
System.out.println("concurrentMap: " + entry.getKey() + " = " + entry.getValue());
}
// Test 3: Concurrent modification during iteration
System.out.println("\n=== Concurrent Modification ===");
syncMap = Collections.synchronizedMap(new HashMap<>());
syncMap.put("A", 1);
syncMap.put("B", 2);
// ❌ synchronizedMap - fail-fast iterator throws exception
try {
synchronized (syncMap) {
for (String key : syncMap.keySet()) {
syncMap.put("C", 3); // ConcurrentModificationException!
}
}
} catch (ConcurrentModificationException e) {
System.out.println("synchronizedMap: ConcurrentModificationException during iteration");
}
concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("A", 1);
concurrentMap.put("B", 2);
// ✅ ConcurrentHashMap - weakly consistent iterator, NO exception
for (String key : concurrentMap.keySet()) {
concurrentMap.put("C", 3); // OK! Weakly consistent
}
System.out.println("concurrentMap after modification during iteration: " + concurrentMap);
// Test 4: Performance comparison
System.out.println("\n=== Performance Test ===");
testPerformance();
}
static void testPerformance() throws InterruptedException {
final int THREADS = 10;
final int OPERATIONS = 100000;
// Test synchronizedMap
Map<Integer, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
long start1 = System.currentTimeMillis();
Thread[] threads1 = new Thread[THREADS];
for (int i = 0; i < THREADS; i++) {
threads1[i] = new Thread(() -> {
for (int j = 0; j < OPERATIONS; j++) {
syncMap.put(j, j);
syncMap.get(j);
}
});
threads1[i].start();
}
for (Thread t : threads1) t.join();
long time1 = System.currentTimeMillis() - start1;
// Test ConcurrentHashMap
Map<Integer, Integer> concurrentMap = new ConcurrentHashMap<>();
long start2 = System.currentTimeMillis();
Thread[] threads2 = new Thread[THREADS];
for (int i = 0; i < THREADS; i++) {
threads2[i] = new Thread(() -> {
for (int j = 0; j < OPERATIONS; j++) {
concurrentMap.put(j, j);
concurrentMap.get(j);
}
});
threads2[i].start();
}
for (Thread t : threads2) t.join();
long time2 = System.currentTimeMillis() - start2;
System.out.println("synchronizedMap time: " + time1 + "ms");
System.out.println("ConcurrentHashMap time: " + time2 + "ms");
System.out.println("ConcurrentHashMap is " + (time1 / (double) time2) + "x faster");
}
}
Weakly Consistent Iterator
ConcurrentHashMap sử dụng weakly consistent iterator - không throw ConcurrentModificationException khi map bị modify trong khi iterate.
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
// Weakly consistent: có thể thấy hoặc không thấy modifications trong khi iterate
for (String key : map.keySet()) {
System.out.println(key);
map.put("D", 4); // OK - không throw exception
map.remove("B"); // OK - có thể thấy hoặc không thấy trong iteration này
}
Khi nào dùng cái nào?
| Scenario | Recommendation |
|---|---|
| High concurrency | ConcurrentHashMap |
| Need null keys/values | Collections.synchronizedMap() |
| Java 8+ modern code | ConcurrentHashMap |
| Legacy code | Collections.synchronizedMap() |
| Ít contention | Cả hai đều OK |
| Cần accurate size() | Collections.synchronizedMap() |
| Cần fail-fast iterator | Collections.synchronizedMap() |
ConcurrentHashMap: Fast, No nulls, Weakly consistent Collections.synchronizedMap(): Slow, Allows nulls, Fail-fast
Rule of thumb: Prefer ConcurrentHashMap cho modern applications với high concurrency!
Khi KHÔNG nên dùng Collections
Ẩn dụ: Collections giống như chiếc ô tô tự động (automatic car) - tiện lợi, dễ dùng, nhưng không phải lúc nào cũng là lựa chọn tốt nhất. Đôi khi xe số sàn (manual car) hoặc xe đạp (specialized tools) lại phù hợp hơn!
1. Primitive Arrays - Tránh Autoboxing Overhead
// ❌ CHẬM: Collections với wrapper types → autoboxing overhead
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
list.add(i); // int → Integer boxing (tạo object, tốn memory)
}
int sum = 0;
for (Integer num : list) {
sum += num; // Integer → int unboxing
}
// Memory: ~1,000,000 Integer objects * ~16 bytes = ~16 MB
// Performance: Boxing/unboxing overhead + GC pressure
// ✅ NHANH: Primitive array - no boxing
int[] array = new int[1_000_000];
for (int i = 0; i < 1_000_000; i++) {
array[i] = i; // Direct primitive storage
}
int sum2 = 0;
for (int num : array) {
sum2 += num; // No boxing/unboxing
}
// Memory: 1,000,000 * 4 bytes = ~4 MB (4x less!)
// Performance: Much faster, no GC pressure
Performance benchmark:
- Primitive array: ~2 ms
ArrayList<Integer>: ~50 ms (25x slower!)
2. Specialized Libraries - Eclipse Collections, Trove, Fastutil
// ❌ Standard Collections - autoboxing
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
list.add(i);
}
// ✅ Eclipse Collections - primitive collections, no boxing
// (Third-party library: org.eclipse.collections)
// import org.eclipse.collections.impl.list.mutable.primitive.IntArrayList;
// IntArrayList list = new IntArrayList();
// for (int i = 0; i < 1_000_000; i++) {
// list.add(i); // No boxing!
// }
// ✅ Trove - primitive collections
// (Third-party library: net.sf.trove)
// import gnu.trove.list.array.TIntArrayList;
// TIntArrayList list = new TIntArrayList();
// ✅ Fastutil - high-performance primitive collections
// (Third-party library: it.unimi.dsi.fastutil)
// import it.unimi.dsi.fastutil.ints.IntArrayList;
// IntArrayList list = new IntArrayList();
3. Fixed-size Data - Dùng Arrays
// ❌ Overkill: ArrayList cho fixed-size data
List<String> daysOfWeek = new ArrayList<>();
daysOfWeek.add("Mon");
daysOfWeek.add("Tue");
// ... 5 more lines
// Overhead: dynamic resizing capability không cần thiết
// ✅ TỐT HƠN: Array cho fixed-size
String[] daysOfWeek = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
// Hoặc: List<String> days = Arrays.asList("Mon", "Tue", ...);
// ❌ Overkill: HashMap cho small constant mapping
Map<String, Integer> map = new HashMap<>();
map.put("RED", 1);
map.put("GREEN", 2);
map.put("BLUE", 3);
// ✅ TỐT HƠN: Enum hoặc switch-case
enum Color {
RED(1), GREEN(2), BLUE(3);
final int code;
Color(int code) { this.code = code; }
}
4. Ultra-high-performance - Custom Data Structures
// ❌ General-purpose collections không tối ưu cho specific use case
// Ví dụ: Cần queue với bounded size và overwrites oldest
// ✅ TỐT HƠN: Custom circular buffer
class CircularBuffer<T> {
private Object[] buffer;
private int head = 0;
private int tail = 0;
private int size = 0;
public CircularBuffer(int capacity) {
buffer = new Object[capacity];
}
public void add(T item) {
buffer[tail] = item;
tail = (tail + 1) % buffer.length;
if (size < buffer.length) size++;
else head = (head + 1) % buffer.length; // Overwrite oldest
}
@SuppressWarnings("unchecked")
public T get(int index) {
return (T) buffer[(head + index) % buffer.length];
}
public int size() { return size; }
}
Decision Table
| Use Case | Avoid | Use Instead | Why |
|---|---|---|---|
| Large primitive data | List<Integer> | int[] hoặc Eclipse Collections | No boxing, 4x less memory |
| Fixed-size constants | ArrayList | Array hoặc Arrays.asList() | No resize overhead |
| Small constant mappings | HashMap | Enum hoặc switch | Simpler, faster |
| Billions of elements | Standard Collections | Custom data structures | Memory efficiency |
| Real-time systems | Collections (GC pressure) | Off-heap memory, custom | Predictable performance |
| Frequency counting | Map<String, Integer> | Map<String, AtomicInteger> hoặc primitive map | Avoid boxing |
Premature optimization is the root of all evil! - Donald Knuth
Chỉ optimize khi:
- Profile trước - chứng minh performance bottleneck
- Measure impact - đảm bảo optimization có hiệu quả
- Trade-off - cân nhắc complexity vs performance gain
Trong 99% trường hợp, standard Collections đủ tốt!
Thread-Safe Collections
Collections Framework không thread-safe by default. Có 3 cách để làm thread-safe:
1. Synchronized Wrappers
List<String> list = Collections.synchronizedList(new ArrayList<>());
Set<Integer> set = Collections.synchronizedSet(new HashSet<>());
Map<String, User> map = Collections.synchronizedMap(new HashMap<>());
// Lưu ý: Iteration vẫn cần manual synchronization
synchronized (list) {
for (String item : list) {
System.out.println(item);
}
}
2. Concurrent Collections (java.util.concurrent)
// Prefer concurrent collections over synchronized wrappers
Map<String, User> map = new ConcurrentHashMap<>();
Queue<Task> queue = new ConcurrentLinkedQueue<>();
List<String> list = new CopyOnWriteArrayList<>();
Set<Integer> set = new CopyOnWriteArraySet<>();
// No need for external synchronization
map.put("key", user);
queue.offer(task);
3. Immutable Collections
// Immutable = thread-safe
List<String> immutableList = List.of("A", "B", "C");
Set<Integer> immutableSet = Set.of(1, 2, 3);
Map<String, Integer> immutableMap = Map.of("A", 1, "B", 2);
Bài tập: Contact Manager Mini Project
Implement một Contact Manager application với các features sau:
class Contact {
private String name;
private String phone;
private String email;
private Set<String> tags;
// Constructor, getters, setters
// Override equals, hashCode, toString
}
class ContactManager {
// Internal data structure: chọn đúng collection type
// CRUD operations
public void addContact(Contact contact);
public Contact getContact(String name);
public void updateContact(String name, Contact newContact);
public void deleteContact(String name);
// Search operations
public List<Contact> searchByName(String query);
public List<Contact> searchByTag(String tag);
public List<Contact> getAllContacts();
// Sorting
public List<Contact> getContactsSortedByName();
public List<Contact> getContactsSortedByEmail();
// Statistics
public int getTotalContacts();
public Map<String, Integer> getContactsByTag(); // Tag frequency
// Import/Export
public void importFromList(List<Contact> contacts);
public List<Contact> exportToList();
}
public class ContactManagerApp {
public static void main(String[] args) {
ContactManager manager = new ContactManager();
// Test all operations
// Add contacts
// Search contacts
// Sort contacts
// Get statistics
// etc.
}
}
Requirements
- Chọn đúng data structure để store contacts (hint: Map)
- Implement tất cả methods với efficient algorithms
- Override equals/hashCode cho Contact class
- Use appropriate collections cho internal storage (Set cho tags, etc.)
- Handle edge cases: null checks, duplicates, not found
- Write test code trong main method
- Use Java 8+ features: Stream API, lambda, method reference
- Performance: Operations nên efficient (O(1) hoặc O(log n) khi có thể)
Tổng kết
Trong bài này, bạn đã học:
- Chọn đúng Collection: Decision flowchart, quick reference table
- Program to interface: Khai báo với interface type, không phải concrete type
- Immutable collections: List.of(), Set.of(), Map.of() (Java 9+), Collections.unmodifiable*()
- Capacity planning: Specify initial capacity cho ArrayList, HashMap
- Common mistakes: Modify while iterating, không override equals/hashCode, raw types, null checks
- Performance tips: Chọn đúng collection, avoid autoboxing, bulk operations, benchmark
- Thread-safety: Synchronized wrappers, concurrent collections, immutable collections
Quy tắc vàng của Collections Framework:
- ArrayList là default choice cho List (99% trường hợp)
- HashMap là default choice cho Map
- HashSet là default choice cho Set
- ArrayDeque cho Stack và Queue operations
- Program to interface (List, Set, Map), không phải implementation
- Override equals() VÀ hashCode() cho custom objects trong HashSet/HashMap
- Dùng Iterator.remove() hoặc removeIf() để xóa trong khi iterate
- Specify capacity khi biết trước size
- Return immutable hoặc defensive copy từ public methods
- Profile before optimize - đừng optimize quá sớm!
Collections Framework là foundation của Java programming. Master nó tốt sẽ giúp bạn viết code efficient, maintainable và bug-free!