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ườ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++ |
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.
- 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.
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.
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
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
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
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