Java Foundations/Vòng lặp `for` — đếm có chủ đích
19/35
Bài 19 / 35~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.

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
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);

In 60. arr.length = 3, i chạy 0, 1, 2. Tổng: 10 + 20 + 30 = 60.

Nếu viết nhầm i <= arr.length, vòng cuối i = 3arr[3]ArrayIndexOutOfBoundsException. Đây chính là off-by-one bug — lý do vì sao phải dùng < không phải <= cho index 0-based.

Q2
Viết for in các số chẵn từ 100 về 2 (theo thứ tự giảm).
for (int i = 100; i >= 2; i -= 2) {
    System.out.println(i);
}

3 phần: khởi tạo i = 100, điều kiện i >= 2 (chú ý >= để in cả 2), update i -= 2. Ra: 100, 98, 96, ..., 4, 2.

Q3
Tại sao i không gọi được sau khối for (int i = 0; i < 10; i++) { ... }?
JLS §14.14.1 quy định biến khai báo trong init của forscope giới hạn trong statement for — gồm condition, update, và body. Ra khỏi khối for, biến biến mất. Đây là thiết kế cố ý — tránh rò rỉ biến tạm ra ngoài (giống block { int x = 0; ... }). Nếu cần dùng sau, khai báo int i trước loop: int i; for (i = 0; i < 10; i++) { ... }
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);
}

Bug: xóa element làm các element sau shift lên 1 index, nhưng i++ vẫn chạy → element ngay sau "b" bị skip. Nếu list có 2 "b" liền nhau, chỉ xóa được 1.

Ngoài ra list.size() được tính lại mỗi vòng — loop vẫn chạy đúng số lần mới, chỉ sai logic skip.

Fix:

  • Duyệt ngược: for (int i = list.size() - 1; i >= 0; i--) — xóa từ cuối, shift không ảnh hưởng index chưa duyệt.
  • Hoặc gọi list.removeIf(s -> s.equals("b")) — an toàn, rõ ý.
  • Hoặc Iterator với iterator.remove() — xem bài for-each.
Q5
Vì sao "cache arr.length ra biến local" đã trở thành anti-pattern với Java hiện đại?
JIT compiler (HotSpot C2) nhận diện pattern for (int i = 0; i < arr.length; i++) từ rất sớm và đã hoist arr.length ra khỏi loop. Bytecode lần đầu có thể gọi arraylength mỗi vòng, nhưng sau khi JIT kick in (~10K iterations), instruction đó biến mất khỏi machine code — cộng với bounds check elimination. Viết int n = arr.length; for (int i = 0; i < n; i++) không nhanh hơn, mà chỉ thêm 1 biến noise. Worse: nếu bạn đang iterate và arr được reassign trong body (hiếm nhưng có), cache sẽ lệch. Viết đơn giản i < arr.length, để JIT làm việc.

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

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