Chuyển tới nội dung chính

Best Practices với Collections

Mục tiêu bài học

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 CaseBest ChoiceWhy
General listArrayListFast random access, most common
Frequent add/remove at headLinkedList or ArrayDequeO(1) operations
Unique elementsHashSetFast O(1) contains/add
Unique + orderedLinkedHashSetMaintains insertion order
Unique + sortedTreeSetAuto-sorted
Key-value lookupHashMapFast O(1) get/put
Key-value + orderedLinkedHashMapMaintains insertion order
Key-value + sortedTreeMapAuto-sorted by keys
Stack operationsArrayDequeFaster than Stack class
Queue operationsArrayDequeFaster than LinkedList
Priority queuePriorityQueueMin-heap by default
LRU cacheLinkedHashMapAccess-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?

  1. Flexibility: Dễ dàng thay đổi implementation
  2. Abstraction: Code phụ thuộc vào interface, không phụ thuộc implementation
  3. 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);
}
}
Exception: Khi cần specific features

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ả!

Collectionnull elementsnull keysnull valuesGhi chú
ArrayList✅ Yes (nhiều)N/AN/ACho phép bao nhiêu null cũng được
LinkedList✅ Yes (nhiều)N/AN/ACho phép bao nhiêu null cũng được
HashSet✅ 1 nullN/AN/AChỉ 1 null (vì Set không có duplicate)
LinkedHashSet✅ 1 nullN/AN/AChỉ 1 null
TreeSet❌ NoN/AN/ANullPointerException khi compare null
HashMapN/A✅ 1 null key✅ Yes (nhiều)1 null key, nhiều null values
LinkedHashMapN/A✅ 1 null key✅ Yes (nhiều)1 null key, nhiều null values
TreeMapN/A❌ No (Java 7+)✅ Yes (nhiều)NullPointerException cho null key
HashtableN/A❌ No❌ NoLegacy, NullPointerException
PriorityQueue❌ NoN/AN/ANullPointerException khi compare
ArrayDeque❌ NoN/AN/ANullPointerException khi add null
ConcurrentHashMapN/A❌ No❌ NoNullPointerException cho null key/value
List.of()❌ NoN/AN/ANullPointerException (immutable)
Set.of()❌ NoN/AN/ANullPointerException (immutable)
Map.of()N/A❌ No❌ NoNullPointerException (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());
}
}
}
💡 Cách nhớ

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?

  1. Thread-safe: Không cần synchronization
  2. Security: Không thể bị modify từ bên ngoài
  3. Caching: An toàn để cache
  4. 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ểmCollections.unmodifiableList()List.copyOf()List.of()
Java versionJava 1.2+Java 10+Java 9+
Truly immutable❌ No (VIEW wrapper)✅ Yes (TRUE COPY)✅ Yes
Original changes propagate✅ Yes (nguy hiểm!)❌ NoN/A
Null elements✅ Allowed❌ Not allowed❌ Not allowed
PerformanceO(1) wrapperO(n) copyO(n)
Use caseLegacy compatibilityDefensive 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?

ScenarioRecommendation
Return từ getterList.copyOf() hoặc List.of() nếu tạo mới
Defensive copy trong constructorList.copyOf() hoặc new ArrayList<>(original)
Wrap existing list tạm thờiCollections.unmodifiableList() (nhanh hơn)
Java 8 trở xuốngCollections.unmodifiableList() (copyOf chưa có)
Có null elementsCollections.unmodifiableList() (copyOf không hỗ trợ null)
True immutability cần thiếtList.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
}
}
🔥 Bẫy OCP

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!

Defensive Copy

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)
Performance Impact

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ặc new 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 MistakesAnti-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();
Performance Guidelines
  • 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ểmCollections.synchronizedMap()ConcurrentHashMap
Locking mechanismSingle lock toàn bộ mapSegment/bucket-level locking
Concurrency levelThấp (1 thread tại 1 thời điểm)Cao (nhiều threads đồng thời)
PerformanceChậm với high contentionNhanh với high contention
Null keyDepends on backing map (HashMap cho phép 1 null)❌ Không cho phép
Null valueDepends on backing map (HashMap cho phép)❌ Không cho phép
IterationCần manual synchronizationKhông cần (weakly consistent)
fail-fast iterator✅ Yes (throws ConcurrentModificationException)❌ No (weakly consistent)
size()AccurateApproximate (không lock toàn bộ)
Java versionJava 1.2+Java 1.5+
Use caseLegacy code, ít contentionModern 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?

ScenarioRecommendation
High concurrencyConcurrentHashMap
Need null keys/valuesCollections.synchronizedMap()
Java 8+ modern codeConcurrentHashMap
Legacy codeCollections.synchronizedMap()
Ít contentionCả hai đều OK
Cần accurate size()Collections.synchronizedMap()
Cần fail-fast iteratorCollections.synchronizedMap()
💡 Cách nhớ

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 CaseAvoidUse InsteadWhy
Large primitive dataList<Integer>int[] hoặc Eclipse CollectionsNo boxing, 4x less memory
Fixed-size constantsArrayListArray hoặc Arrays.asList()No resize overhead
Small constant mappingsHashMapEnum hoặc switchSimpler, faster
Billions of elementsStandard CollectionsCustom data structuresMemory efficiency
Real-time systemsCollections (GC pressure)Off-heap memory, customPredictable performance
Frequency countingMap<String, Integer>Map<String, AtomicInteger> hoặc primitive mapAvoid boxing
Lưu ý

Premature optimization is the root of all evil! - Donald Knuth

Chỉ optimize khi:

  1. Profile trước - chứng minh performance bottleneck
  2. Measure impact - đảm bảo optimization có hiệu quả
  3. 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

  1. Chọn đúng data structure để store contacts (hint: Map)
  2. Implement tất cả methods với efficient algorithms
  3. Override equals/hashCode cho Contact class
  4. Use appropriate collections cho internal storage (Set cho tags, etc.)
  5. Handle edge cases: null checks, duplicates, not found
  6. Write test code trong main method
  7. Use Java 8+ features: Stream API, lambda, method reference
  8. 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:

  1. ArrayList là default choice cho List (99% trường hợp)
  2. HashMap là default choice cho Map
  3. HashSet là default choice cho Set
  4. ArrayDeque cho Stack và Queue operations
  5. Program to interface (List, Set, Map), không phải implementation
  6. Override equals() VÀ hashCode() cho custom objects trong HashSet/HashMap
  7. Dùng Iterator.remove() hoặc removeIf() để xóa trong khi iterate
  8. Specify capacity khi biết trước size
  9. Return immutable hoặc defensive copy từ public methods
  10. 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!

Đọc thêm