Java — Từ Zero đến Senior/Điều kiện & Vòng lặp/`for-each` — duyệt collection không cần index
5/7
~16 phútĐiều kiện & Vòng lặp

`for-each` — duyệt collection không cần index

Enhanced for từ Java 5, cơ chế Iterable/Iterator, khi nào nó tương đương for cổ điển và khi nào bạn vẫn cần index, ConcurrentModificationException.

Duyệt mảng bằng for (int i = 0; i < arr.length; i++) không sai, nhưng 90% lần bạn không cần i — bạn chỉ cần element. Java 5 thêm enhanced for (cộng đồng gọi for-each) làm sạch pattern đó: đọc thẳng element, bỏ qua index.

Bài này giải thích for-each không chỉ là "cú pháp ngắn hơn" — nó đứng trên interface Iterable / Iterator, cơ chế nào làm nó chạy, khi nào buộc phải quay về for có index, và bẫy ConcurrentModificationException đáng sợ.

1. Analogy — giỏ trái cây

Bạn có một giỏ trái cây. Bạn không cần biết "trái thứ 3 là gì" — bạn chỉ cần lần lượt lấy từng trái ra xem. Đó là for-each: xin collection "đưa tôi từng element", không bận tâm index.

Đời thườngfor-each
Giỏ trái câyIterable (List, Set, array...)
Lấy từng trái mộtMỗi vòng lặp nhận 1 element
Không cần biết "thứ tự"Không có biến index

💡 💡 Cách nhớ

for-each đọc là "for each element in collection" — "với mỗi phần tử trong tập". Tên cũng gợi ý rõ bạn quan tâm phần tử, không quan tâm chỉ số.

2. Cú pháp

for (<type> <var> : <iterable>) {
    // body dung <var>
}

Ví dụ:

int[] arr = {10, 20, 30};
for (int x : arr) {
    System.out.println(x);
}

Đọc: "với mỗi int x trong arr, in x".

Hoạt động trên:

  • Array — mọi kiểu, kể cả primitive int[], double[].
  • CollectionList, Set, Queue, Map.entrySet() / keySet() / values(), bất cứ class nào implement Iterable<T>.
List<String> names = List.of("An", "Binh", "Chau");
for (String name : names) {
    System.out.println("Chao " + name);
}

Set<Integer> ids = Set.of(1, 2, 3);
for (int id : ids) {                   // auto-unbox Integer -> int
    System.out.println(id);
}

Map<String, Integer> score = Map.of("An", 10, "Binh", 8);
for (Map.Entry<String, Integer> e : score.entrySet()) {
    System.out.println(e.getKey() + "=" + e.getValue());
}

3. Cơ chế bên dưới — IterableIterator

for-each không phải magic. Compiler rewrite nó thành code dùng Iterator:

// Ban viet:
for (String s : list) { doSomething(s); }

// Compiler sinh ra (don gian hoa):
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
    String s = it.next();
    doSomething(s);
}

Cần 2 interface:

  • Iterable<T> — "tôi có thể cung cấp iterator". Method duy nhất: Iterator<T> iterator();
  • Iterator<T> — "tôi đi qua từng element". Methods: boolean hasNext(), T next(), (optional) void remove().

Bất kỳ class nào implement Iterable<T> đều dùng được với for-each. Bạn có thể tự viết class iterable:

class Range implements Iterable<Integer> {
    private final int from, to;
    public Range(int from, int to) { this.from = from; this.to = to; }

    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<>() {
            int i = from;
            public boolean hasNext() { return i < to; }
            public Integer next() { return i++; }
        };
    }
}

// Dung:
for (int x : new Range(0, 5)) {
    System.out.println(x);  // 0 1 2 3 4
}

3.1 Trường hợp đặc biệt — Array

Array không implement Iterable (không có iterator()). Compiler xử lý array khác Collection:

// Array: compiler sinh for co index
for (int x : arr) { ... }
// thanh:
for (int i = 0; i < arr.length; i++) {
    int x = arr[i];
    ...
}

// Collection: compiler sinh for dung iterator
for (String s : list) { ... }
// thanh:
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
    String s = it.next();
    ...
}

Cả 2 cùng cú pháp, nhưng bytecode khác. Với array không có overhead Iterator — tối ưu như for có index.

flowchart LR
    A["for-each cu phap"] --> B{"array hay Iterable?"}
    B -->|array| C["for (int i = 0; ...) { arr[i] }"]
    B -->|Iterable| D["iterator() + hasNext + next"]

4. Khi nào for-each, khi nào for có index?

Tình huốngChọn
Chỉ cần đọc element, không quan tâm thứ tựfor-each
Cần biết index để truy xuất element khác hoặc in số thứ tựfor index
Cần thay đổi element tại index (set giá trị mới)for index hoặc ListIterator
Cần xóa element trong khi duyệtIterator.remove() (hoặc removeIf) — KHÔNG for-each rồi list.remove(x)
Duyệt ngượcfor index với i--
Tính toán perf-critical trên primitive arrayfor index (ít overhead nhất)
Duyệt 2 collection song song theo indexfor index (for-each không biết "cùng index")

4.1 Muốn index kèm element?

Không có cú pháp native. Workaround:

// Cach 1: tu dem
int i = 0;
for (String name : names) {
    System.out.println(i + ": " + name);
    i++;
}

// Cach 2: for co index
for (int j = 0; j < names.size(); j++) {
    System.out.println(j + ": " + names.get(j));
}

// Cach 3 (modern): IntStream + forEach
IntStream.range(0, names.size())
    .forEach(k -> System.out.println(k + ": " + names.get(k)));

Python có enumerate(), JavaScript có .entries(). Java 21 vẫn chưa có; chọn cách nào tuỳ style team.

5. Bẫy ConcurrentModificationException

List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
    if (s.equals("b")) {
        list.remove(s);   // NEM ConcurrentModificationException
    }
}

Vì sao? Iterator giữ state (vị trí hiện tại, "mod count" của list). Khi bạn gọi list.remove() trực tiếp (không qua iterator), mod count tăng → iterator lần next() kế tiếp thấy mod count lệch → ném ConcurrentModificationException.

Fix:

// Cach 1: Iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("b")) it.remove();
}

// Cach 2: removeIf (Java 8+)
list.removeIf(s -> s.equals("b"));

// Cach 3: build list moi
List<String> keep = new ArrayList<>();
for (String s : list) {
    if (!s.equals("b")) keep.add(s);
}

removeIf là ngắn gọn nhất — nội bộ dùng iterator an toàn.

⚠️ ⚠️ Không chỉ remove — add cũng ném exception

Cả list.add() trong khi for-each cũng ném ConcurrentModificationException. Rule: không modify collection đang duyệt, trừ khi qua iterator của collection đó.

5.1 "Fail-fast" không phải là đảm bảo thread-safety

Mod count check là best-effort — nó không đảm bảo phát hiện mọi race condition giữa thread. Tên gọi "fail-fast" nghĩa: nếu phát hiện được, fail sớm với exception thay vì cho ra dữ liệu sai. Nhưng nó có thể miss case — đừng dựa vào nó để implement concurrent code. Dùng CopyOnWriteArrayList, ConcurrentHashMap cho đa thread.

6. Pattern hay gặp

6.1 Sum / product trên mảng

int[] arr = {1, 2, 3, 4, 5};
int sum = 0;
for (int x : arr) sum += x;

Ngắn hơn hẳn so với index-based. JIT optimize tương đương.

6.2 Duyệt Map

Map<String, Integer> counts = Map.of("a", 1, "b", 2, "c", 3);

// Cach 1 — ca key va value
for (Map.Entry<String, Integer> e : counts.entrySet()) {
    System.out.println(e.getKey() + " = " + e.getValue());
}

// Cach 2 — chi key
for (String k : counts.keySet()) {
    System.out.println(k);
}

// Cach 3 — chi value
for (int v : counts.values()) {
    System.out.println(v);
}

Chọn cái nào tuỳ cần gì. entrySet() hiệu quả nhất khi cần cả key + value — keySet() rồi gọi get(k)2 lần lookup.

6.3 Lồng nested

int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
for (int[] row : matrix) {
    for (int cell : row) {
        System.out.print(cell + " ");
    }
    System.out.println();
}

7. Pitfall tổng hợp

Nhầm 1: Cố update element qua biến for-each.

for (int x : arr) {
    x = x * 2;   // KHONG doi arr — x la ban sao
}

✅ Đây là pass-by-value (bài Module 2). x là biến local copy. Muốn sửa mảng, dùng for có index: arr[i] = arr[i] * 2.

Nhầm 2: Modify collection trong for-each.

for (String s : list) {
    if (bad(s)) list.remove(s);  // ConcurrentModificationException
}

list.removeIf(...), Iterator.remove(), hoặc build list mới.

Nhầm 3: For-each trên null.

List<String> list = null;
for (String s : list) { ... }  // NullPointerException

✅ Kiểm tra null trước, hoặc dùng Collections.emptyList() làm default.

Nhầm 4: Unbox Integer null.

List<Integer> list = Arrays.asList(1, null, 3);
for (int x : list) { ... }   // NPE khi gap null

✅ Dùng Integer x (wrapper, cho phép null), kiểm tra null trong body.

8. 📚 Deep Dive Oracle

ℹ️ 📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: JLS §14.14.2 định nghĩa chính xác cú pháp — trong đó có quy tắc: nếu RHS là array, compiler sinh for-index; nếu RHS implement Iterable, compiler sinh iterator loop. Đây là lý do array có performance tương đương for-index, còn Collection có thêm overhead Iterator (nhưng vẫn rất rẻ cho ArrayList).

9. Tóm tắt

  • for (type x : iterable) — duyệt mà không cần index. Đọc: "for each x in iterable".
  • Hoạt động với array và mọi class implement Iterable (List, Set, Queue, Map.entrySet()...).
  • Compiler rewrite 2 cách: array → for có index; Iterable → iterator loop.
  • Không access được index — cần index thì dùng for có counter.
  • Không modify element qua biến for-each — biến là pass-by-value copy.
  • Không add/remove collection trong for-each — dùng Iterator.remove(), removeIf(), hoặc tạo list mới.
  • ConcurrentModificationException là "fail-fast" best-effort, không đảm bảo detect 100%.

10. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn sau in gì? Mảng arr có bị thay đổi?
int[] arr = {1, 2, 3};
for (int x : arr) {
    x = x * 10;
}
for (int x : arr) {
    System.out.println(x);
}

In 1, 2, 3 — mảng không bị thay đổi.

Trong vòng đầu, x là biến local nhận bản copy giá trị từ mảng (Module 2 — primitive pass-by-value). Gán x = x * 10 chỉ đổi biến local, không ảnh hưởng ô nhớ gốc trong mảng.

Muốn update mảng, dùng for-index: for (int i = 0; i < arr.length; i++) arr[i] = arr[i] * 10;

Q2
Viết 2 cách xóa mọi phần tử rỗng "" khỏi List<String> list, không gây ConcurrentModificationException.

Cách 1 — removeIf (ngắn nhất):

list.removeIf(s -> s.isEmpty());

Cách 2 — Iterator.remove():

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().isEmpty()) it.remove();
}

Cả 2 đều đi qua iterator — mod count trong iterator được cập nhật đồng bộ, không ném ConcurrentModificationException. removeIf là default method Java 8, nội bộ gần giống cách 2.

Q3
Vì sao for-each không có cách lấy index mặc định, trong khi nhiều ngôn ngữ khác (Python enumerate, JS entries) đều có?

For-each thiết kế để dùng cho mọi Iterable — không chỉ list/array có index. Set, Map, custom collection chỉ có "element tiếp theo" chứ không có khái niệm index. Bắt buộc cung cấp index sẽ breakerate hợp đồng Iterator.

Khi cần index với List:

  • Dùng for (int i = 0; i < list.size(); i++).
  • Hoặc đếm thủ công ngoài for-each: int i = 0; for (T x : iter) { ... i++; }
  • Hoặc IntStream.range(0, list.size()).forEach(i -> ...).
Q4
Khi nào chọn list.entrySet() thay vì list.keySet() để duyệt Map?

Khi cần cả key và value. entrySet() trả Map.Entry<K,V> có sẵn cả 2 → 1 lần lookup. Nếu dùng keySet() rồi gọi map.get(k) bên trong, mỗi vòng là 2 lần lookup (kể cả HashMap O(1) cũng tốn hash + equals).

Nếu chỉ cần key → keySet(). Nếu chỉ cần value → values().

// Tot — 1 lookup
for (var e : map.entrySet()) use(e.getKey(), e.getValue());

// Te hon — 2 lookup
for (var k : map.keySet()) use(k, map.get(k));
Q5
Viết class Range có thể dùng với for-each để duyệt các số từ start đến end-1.
class Range implements Iterable<Integer> {
    private final int start, end;
    public Range(int start, int end) { this.start = start; this.end = end; }

    public Iterator<Integer> iterator() {
        return new Iterator<>() {
            int i = start;
            public boolean hasNext() { return i < end; }
            public Integer next() { return i++; }
        };
    }
}

// Dung:
for (int x : new Range(0, 5)) {
    System.out.println(x);  // 0 1 2 3 4
}

Yêu cầu duy nhất: implement Iterable.iterator(). Method trả IteratorhasNext()next(). for-each gọi 2 method đó lặp đến khi hasNext() == false. Java 21 cũng có IntStream.range(0, 5) cho use case này — không cần viết tay.


Bài tiếp theo: breakcontinue — điều khiển luồng bên trong vòng lặp