Java Foundations
24/39
Bài 24 / 39~12 phútĐiều kiện & Vòng lặpLộ trình · chặng 02/21Miễn phí… lượt xem

for-each — Duyệt collection và mảng không cần index

Tìm hiểu enhanced for từ Java 5, cơ chế Iterable và Iterator, cách phòng tránh ConcurrentModificationException và lựa chọn vòng lặp phù hợp.

TL;DR: Vòng lặp for-each (enhanced for) cung cấp cú pháp súc tích để đọc trực tiếp từng phần tử từ mảng hoặc cấu trúc Iterable mà không cần quản lý biến chỉ số. Trình biên dịch sẽ tự động desugar cú pháp này thành vòng lặp chỉ số (với mảng) hoặc vòng lặp Iterator (với collection), đồng thời kích hoạt cơ chế fail-fast nếu cấu trúc dữ liệu bị can thiệp trái phép.

Khác với vòng lặp for truyền thống nơi bạn phải tự quản lý biến đếm chỉ số i, phần lớn nhu cầu thực tế chỉ đơn thuần là đọc qua từng phần tử. Java 5 giới thiệu cú pháp enhanced for (thường gọi là for-each) để tối giản hóa mẫu thao tác này.

1. Analogy giỏ trái cây minh họa tư duy duyệt tập hợp ra sao?

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 vòng lặp for-each trong Java được viết như thế nào?

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

Ví dụ:

Java
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[].
  • Collection — List, Set, Queue, Map.entrySet() / keySet() / values(), bất cứ class nào implement Iterable<T>.
Java
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ế Iterable và Iterator bên dưới tầng bytecode hoạt động ra sao?

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

Java
// 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:

Java
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:

Java
// 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.

Một dòng cú pháp for-each rẽ thành hai cách compiler dịch: vế phải là array thì sinh vòng for có index truy cập phần tử theo chỉ số, còn vế phải là Iterable thì sinh vòng gọi iterator rồi hasNext và next

Thử ngẫmbảng dưới liệt kê "perf-critical trên mảng primitive → dùng for index". Với desugar vừa thấy, for-each trên mảng có thật sự chậm hơn, hay chỉ là thói quen truyền miệng?

4. Khi nào nên dùng for-each và khi nào cần quay về 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:

Java
// 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.

Thử ngẫmbạn quen enumerate() của Python, chuyển sang Java thấy chưa có cú pháp tương đương. Trong ba cách workaround ở trên, cách nào ít có khả năng bạn gõ nhầm chỉ số nhất?

5. Bẫy ngoại lệ ConcurrentModificationException phát sinh khi nào?

Java
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:

Java
// 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.

Thử ngẫm"fail-fast" chỉ best-effort, không đảm bảo an toàn đa luồng. Code cũ từng dựa vào "chưa thấy CME nghĩa là an toàn" để bỏ qua đồng bộ hoá — giả định đó sai ở đâu?

6. Các mẫu ứng dụng for-each phổ biến trong thực tế là gì?

6.1 Sum / product trên mảng

Java
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

Java
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) là 2 lần lookup.

6.3 Lồng nested

Java
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. Các bẫy lỗi thường gặp nhất khi sử dụng for-each là gì?

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

Java
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.

Java
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.

Java
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.

Java
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. Tài liệu Deep Dive Oracle quy định gì về enhanced for?

📚 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. Điểm cốt lõi cần nhớ về vòng lặp for-each là gì?

  • Cú pháp for-each duyệt mảng và cấu trúc Iterable mà không cần quản lý biến đếm index thủ công.
  • Trình biên dịch tự động desugar cú pháp thành lặp chỉ số đối với mảng và lặp Iterator đối với collection.
  • Tuyệt đối không thêm hoặc xóa phần tử trực tiếp từ collection trong quá trình duyệt để tránh ConcurrentModificationException.
  • Biến lặp chỉ là bản sao giá trị (pass-by-value); thay đổi biến lặp sẽ không làm thay đổi dữ liệu trong mảng gốc.

10. Tự kiểm tra kiến thức về vòng lặp for-each như thế nào?

Tự kiểm tra
0/5 câu đã trả lời
  1. 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);
    }
  2. 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.
  3. 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ó?
  4. Q4
    Khi nào chọn list.entrySet() thay vì list.keySet() để duyệt Map?
  5. Q5
    Viết class Range có thể dùng với for-each để duyệt các số từ start đến end-1.

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

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 2/21 · Lập trình từ số 0 với Java — bài 24/32 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

break và continue — Điều khiển luồng trong vòng lặp