Java Foundations/Vòng lặp `for` — đếm có chủ đích
23/39
Bài 23 / 39~18 phútĐiều kiện & Vòng lặpMiễn phí lượt xem

Vòng lặp `for` — đếm có chủ đích

Cú pháp for 3 phần init/condition/update, scope của biến counter, duyệt mảng truyền thống, off-by-one bugs, và những tối ưu JIT bạn vô tình bật/tắt.

while tốt khi không biết trước số vòng. Nhưng 80% thời gian bạn biết trước: duyệt mảng 100 phần tử, in 12 tháng, sinh 5 dòng. Viết bằng while phải tách init, update ra nhiều nơi — dễ quên biến, dễ off-by-one. for gom cả 3 vào 1 dòng.

Bài này giải thích cú pháp for 3 phần, scope biến counter, off-by-one bug kinh điển, duyệt mảng ngược, và một vài optimization JIT đáng biết.

1. Analogy — chương trình tập gym

Huấn luyện viên viết ra 3 thông tin: bắt đầu từ (đứng thẳng), dừng khi (đủ 12 cái), mỗi lần (làm 1 cái đẩy ngực). Đó là 3 phần của for.

Đời thườngfor (init; cond; update)
"Bắt đầu từ số 1"int i = 1
"Dừng khi quá 12"i <= 12
"Mỗi lần đếm thêm 1"i++
💡 Cách nhớ

for (init; condition; update): thực thi init 1 lần, test condition đầu mỗi vòng, chạy update cuối mỗi vòng body.

2. Cú pháp 3 phần

for (<init> ; <condition> ; <update>) {
    // body
}

Ví dụ in 1..5:

for (int i = 1; i <= 5; i++) {
    System.out.println(i);
}

Luồng chạy:

  1. int i = 1 — khai báo + gán. Chỉ chạy 1 lần duy nhất.
  2. i <= 5 — test. Sai → thoát loop.
  3. Body: println(i).
  4. i++ — tăng i.
  5. Quay lại bước 2.

Ba phần viết cạnh nhau trên một dòng, nhưng chúng không chạy theo thứ tự trái sang phải trong mỗi vòng. Trải phẳng trình tự thật ra theo thời gian:

Trình tự thực thi của vòng lặp for trải phẳng theo thời gian, init chạy một lần rồi mỗi lần lặp là kiểm tra, thân, update

update nằm sau thân, không phải trước — giữa hai lần chạy thân có đúng một lần tăng biến đếm. Chi tiết này là thứ quyết định hành vi của continue trong for, khác hẳn continue trong while.

Tương đương while:

{
    int i = 1;             // init
    while (i <= 5) {       // condition
        System.out.println(i);
        i++;               // update
    }
} // i ra khoi scope

Khác biệt: biến i khai báo trong for chỉ sống trong loop. Ra khỏi for, không còn gọi được i. Với while phải tự tạo block để giới hạn scope — lặp thủ công, dễ rò rỉ biến ra ngoài.

2.1 Duyệt mảng bằng for

Pattern kinh điển:

int[] arr = {10, 20, 30, 40, 50};

for (int i = 0; i < arr.length; i++) {
    System.out.println("arr[" + i + "] = " + arr[i]);
}
  • Array index trong Java bắt đầu từ 0, kết thúc length - 1.
  • Điều kiện dùng < không phải <=i <= arr.length sẽ ArrayIndexOutOfBoundsException ở vòng cuối.

2.2 Duyệt ngược

for (int i = arr.length - 1; i >= 0; i--) {
    System.out.println(arr[i]);
}

Hữu ích khi xóa element khỏi ArrayList — xóa từ cuối về đầu không làm shift index của phần tử chưa duyệt.

2.3 Bước nhảy khác 1

// So chan tu 2 den 20
for (int i = 2; i <= 20; i += 2) {
    System.out.println(i);
}

// Luy thua cua 2
for (int x = 1; x <= 1024; x *= 2) {
    System.out.println(x);
}

Ba phần của for linh hoạt — update là expression bất kỳ có side effect.

3. Các thành phần có thể bỏ

Mỗi phần trong 3 phần đều tùy chọn:

for (; ; ) { ... }               // infinite loop — tu C, it dung
for (int i = 0; i < 10; ) {      // update bo trong — tu lam trong body
    if (done) break;
    i++;
}

Thực tế chỉ nên bỏ nếu có lý do rõ — đa số dùng đầy đủ 3 phần.

4. Scope của biến counter

for (int i = 0; i < 5; i++) {
    System.out.println(i);
}
System.out.println(i);  // COMPILE ERROR — i ra khoi scope

Đây là ưu điểm lớn so với while. Viết:

int i = 0;
while (i < 5) { ... i++; }
// i van con o day — 5 (hoac gia tri khi thoat)

Biến i rò rỉ sau loop. Nếu 100 dòng sau có loop khác tái dùng tên i, dễ gây nhầm. for với int i khai báo ngay trong init tự giới hạn scope.

💡 Convention — tên biến counter
  • Loop đơn: i. Nested: i, j, k.
  • Collection lớn / có ý nghĩa: dùng tên mô tả. Vd for (int page = 0; page < totalPages; page++) dễ đọc hơn i.
  • Tránh I, l, o, O — dễ nhầm số 1 / 0.

5. Bẫy off-by-one — bug kinh điển nhất lịch sử loop

int[] arr = new int[5];          // arr[0], arr[1], arr[2], arr[3], arr[4]
for (int i = 0; i <= arr.length; i++) {  // SAI — <=
    arr[i] = i;                  // ArrayIndexOutOfBoundsException khi i = 5
}

Quy tắc:

  • Index 0-based: 0 .. length - 1. Dùng i < length, không i <= length.
  • Index 1-based (vd số thứ tự): 1 .. n. Dùng i <= n, không i < n.

Off-by-one là nguồn của nhiều bug nổi tiếng — Heartbleed (2014) ở OpenSSL cũng một phần do đọc buffer quá 1 byte.

⚠️ Khi đếm ngược cũng phải cẩn thận
for (int i = arr.length; i >= 0; i--) {   // SAI — bat dau tu length, khong ton tai
    System.out.println(arr[i]);            // AIOOBE ngay vong dau
}

Đúng: int i = arr.length - 1; i >= 0; i--.

6. Cơ chế bên dưới — JIT và loop optimization

Bytecode của for gần như giống while. Nhưng JIT (Just-In-Time compiler) nhận diện for pattern tốt hơn, dẫn đến nhiều tối ưu:

  • Loop unrolling — thay vì chạy 100 vòng, compiler có thể "gộp" 4 vòng vào 1 block, chạy 25 lần → giảm chi phí branch.
  • Bounds check elimination — Java kiểm tra arr[i] có out-of-bounds không; JIT nhận ra for (int i = 0; i < arr.length; i++) không bao giờ vượt giới hạn → xóa bỏ check, đạt tốc độ như C.
  • Vectorization — một số loop cộng số đơn giản được chuyển thành SIMD instruction (AVX, SSE) — xử lý 4–8 phần tử/CPU cycle.
// Pattern JIT nhan dien toi, nhanh gan nhu C
int sum = 0;
for (int i = 0; i < arr.length; i++) {
    sum += arr[i];
}

Nếu bạn viết "sáng tạo" quá — thay arr.length bằng method call phức tạp, hoặc update i kiểu i = f(i) — JIT có thể từ chối optimize.

📚 Cache `arr.length` — chiêu lỗi thời

Trước đây mọi người viết for (int i = 0, n = arr.length; i < n; i++) với lý do "gọi arr.length mỗi vòng tốn". Sai với Java hiện đại — JIT đã hoist arr.length ra ngoài loop từ rất lâu. Viết đơn giản i < arr.length cho dễ đọc.

7. Nested for — ma trận, cặp

// In bang cuu chuong
for (int i = 2; i <= 9; i++) {
    for (int j = 1; j <= 10; j++) {
        System.out.printf("%d x %d = %d%n", i, j, i * j);
    }
    System.out.println("---");
}

Complexity: O(n * m). 2 biến counter i, j ở 2 loop scope khác nhau, không xung đột.

// Tim cap (i, j) co tong = target
int[] arr = {1, 2, 3, 4, 5};
int target = 6;
for (int i = 0; i < arr.length; i++) {
    for (int j = i + 1; j < arr.length; j++) {  // j = i + 1 — khong trung, khong lap pair
        if (arr[i] + arr[j] == target) {
            System.out.println(arr[i] + " + " + arr[j]);
        }
    }
}

Chú ý j = i + 1 — tối ưu cho bài "tìm cặp không có thứ tự" từ O(n²) thành O(n²/2).

8. So sánh for vs while

Tình huốngNên dùng
Biết trước số vòng / range 0..n-1for — init/cond/update gọn
Duyệt mảng theo indexfor hoặc for-each (bài sau)
Không biết khi nào dừng (đọc file, poll queue)while
Phải chạy ít nhất 1 lầndo-while
Infinite loopwhile (true) (ưu tiên) hoặc for (;;)

9. Pitfall tổng hợp

Nhầm 1: <= thay vì < khi duyệt mảng.

for (int i = 0; i <= arr.length; i++) { arr[i] = 0; }  // AIOOBE

i < arr.length cho index 0-based. Trực giác: đếm từ 0 đến length-1, tức length phần tử.

Nhầm 2: Modify collection trong vòng for (int i = 0; ...).

for (int i = 0; i < list.size(); i++) {
    if (isBad(list.get(i))) list.remove(i);  // skip element ke tiep
}

✅ Dùng Iterator.remove(), removeIf(), hoặc build list mới.

Nhầm 3: i dùng tiếp sau vòng for.

for (int i = 0; i < 10; i++) { ... }
if (i > 5) { ... }  // COMPILE ERROR — i khong ton tai

✅ Khai báo i ngoài for nếu cần dùng sau; nhưng thường design lại logic sạch hơn.

Nhầm 4: Shadow biến counter trong nested.

for (int i = 0; i < 10; i++) {
    for (int i = 0; i < 5; i++) { ... }  // COMPILE ERROR — i da khai bao o ngoai
}

✅ Dùng tên khác: for (int j = 0; j < 5; j++).

Nhầm 5: Update biến counter trong body làm loop vỡ logic.

for (int i = 0; i < 10; i++) {
    if (isSpecial(i)) i += 5;  // bo qua 5 vong, deu cung bat ngo
    process(i);
}

✅ Nếu cần bước nhảy động, cân nhắc while — rõ intent hơn.

10. 📚 Deep Dive Oracle

📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: JLS §14.14.1 quy định rõ scope: biến khai báo trong init chỉ thấy được trong for. Đây là khác biệt chính với C — C89 yêu cầu khai báo biến đầu block, C99 cho phép giống Java. Với JVM hot loop, JIT thường compile thành SIMD / unroll — nhưng điều kiện là loop pattern "đẹp" (biến đếm tuyến tính, không side effect bất thường).

11. Tóm tắt

  • for (init; cond; update) { body } — gom 3 phần của loop vào 1 dòng.
  • Thứ tự: init chạy 1 lần → test cond → body → update → lặp test.
  • Biến khai báo trong init chỉ sống trong scope loop. Ưu điểm lớn so với while.
  • Duyệt mảng: for (int i = 0; i < arr.length; i++). Dùng < cho index 0-based.
  • Off-by-one là bug phổ biến nhất của for — kiểm tra kỹ biên.
  • JIT tối ưu tốt for pattern "đẹp" (unrolling, bounds-check elimination, vectorization). Viết đơn giản để JIT giúp.
  • Không cần cache arr.length — JIT đã hoist ra từ lâu.
  • Nested for → complexity O(n*m). Chú ý tên biến không shadow.

12. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Đoạn sau in gì?
    int[] arr = {10, 20, 30};
    int sum = 0;
    for (int i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
    System.out.println(sum);
  2. Q2
    Viết for in các số chẵn từ 100 về 2 (theo thứ tự giảm).
  3. Q3
    Tại sao i không gọi được sau khối for (int i = 0; i < 10; i++) { ... }?
  4. Q4
    Đoạn sau có bug gì? Sửa thế nào?
    List<String> list = new ArrayList<>(List.of("a", "b", "c", "d"));
    for (int i = 0; i < list.size(); i++) {
        if (list.get(i).equals("b")) list.remove(i);
    }
  5. Q5
    Vì sao "cache arr.length ra biến local" đã trở thành anti-pattern với Java hiện đại?

Bài tiếp theo: for-each — duyệt collection không cần index

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.

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

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

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

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