Java — Từ Zero đến Senior/Điều kiện & Vòng lặp/Vòng lặp `for` — đếm có chủ đích
4/7
~18 phútĐiều kiện & Vòng lặp

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