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.
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:

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.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); - Q2Viết
forin các số chẵn từ 100 về 2 (theo thứ tự giảm). - Q3Tại sao
ikhông gọi được sau khốifor (int 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); } - Q5Vì sao "cache
arr.lengthra 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
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