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ường | for (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:
int i = 1— khai báo + gán. Chỉ chạy 1 lần duy nhất.i <= 5— test. Sai → thoát loop.- Body:
println(i). i++— tăngi.- 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.lengthsẽ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ơni. - 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ùngi < length, khôngi <= length. - Index 1-based (vd số thứ tự):
1 .. n. Dùngi <= n, khôngi < 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 rafor (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ống | Nên dùng |
|---|---|
| Biết trước số vòng / range 0..n-1 | for — init/cond/update gọn |
| Duyệt mảng theo index | for 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ần | do-while |
| Infinite loop | while (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:
- JLS §14.14.1 — The basic for Statement — cú pháp 3 phần, scope biến khai báo trong init.
- JLS §14.15 — break / §14.16 — continue — cách thoát/skip vòng.
- OpenJDK HotSpot Loop Optimizations — documentation nội bộ về loop unrolling, range check elimination, vectorization. Đọc để hiểu JIT.
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ự:
initchạy 1 lần → testcond→ body →update→ lặp test. - Biến khai báo trong
initchỉ sống trong scope loop. Ưu điểm lớn so vớiwhile. - 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
forpattern "đẹ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
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);
▸
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 = 3 → arr[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.
Q2Viết for in các số chẵn từ 100 về 2 (theo thứ tự giảm).▸
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.
Q3Tại sao i không gọi được sau khối for (int i = 0; i < 10; i++) { ... }?▸
i không gọi được sau khối for (int i = 0; i < 10; i++) { ... }?init của for có scope 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);
}
▸
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
Iteratorvớiiterator.remove()— xem bài for-each.
Q5Vì sao "cache arr.length ra biến local" đã trở thành anti-pattern với Java hiện đại?▸
arr.length ra biến local" đã trở thành anti-pattern với Java hiện đại?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