Vòng lặp for — Kiểm soát số lần lặp và duyệt mảng
Tìm hiểu cú pháp vòng lặp for 3 phần trong Java, scope của biến counter, kỹ thuật duyệt mảng, tránh bẫy off-by-one và cách JIT tối ưu hóa loop.
TL;DR: Vòng lặp for gom 3 bước điều khiển (khởi tạo, kiểm tra điều kiện, cập nhật biến đếm) vào một dòng duy nhất. Biến counter được giới hạn phạm vi chặt chẽ trong thân vòng lặp, giúp duyệt mảng an toàn và cho phép trình biên dịch JIT tối ưu hóa unrolling và loại bỏ bounds-check.
Ở bài trước, ta đã thấy vòng lặp while và do-while tỏa sáng khi số lần lặp không biết trước. Tuy nhiên, phần lớn công việc lập trình hàng ngày lại biết trước số lần: duyệt qua mảng 100 phần tử, in 12 tháng trong năm, hoặc xử lý danh sách trang. Vòng lặp for sinh ra để giải quyết chính xác nhu cầu này bằng cách gom toàn bộ logic khởi tạo và cập nhật lên một dòng.
1. Analogy chương trình tập gym minh họa vòng lặp for ra sao?
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 của vòng lặp for hoạt động theo thứ tự nào?
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 init, condition, update có thể được lược bỏ không?
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. Phạm vi hoạt động (scope) của biến counter được kiểm soát như thế nào?
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.
Thử ngẫmvòng while cũ dùng chung biến i ở scope ngoài cho ba lần lặp liên tiếp. Đổi sang for khai int i ngay trong init, dòng nào sau loop thứ hai chắc chắn không compile được nữa?
5. Bẫy lỗi off-by-one khi duyệt mảng nguy hiểm ra sao?
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--.
Thử ngẫmHeartbleed một phần do đọc buffer quá 1 byte — cùng họ lỗi với off-by-one vừa học. Đoạn duyệt ngược sai ở trên đọc thiếu hay đọc thừa so với mảng thật?
6. Trình biên dịch JIT tối ưu hóa vòng lặp for như thế nào?
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.
Thử ngẫmđồng nghiệp cũ để lại code cache arr.length vào biến n "cho nhanh". Xoá optimization thừa đó có làm code chậm đi không, hay chỉ đổi độ dễ đọc?
7. Kỹ thuật lồng vòng lặp (nested for) xử lý mảng và ma trận ra sao?
// In bang cuu chuong
for (int i = 2; i <= 9; i++) {
for (int j = 1; j <= 10; j++) {
System.out.println(i + " x " + 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. Khi nào nên dùng vòng lặp for thay vì 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. Các bẫy lỗi thường gặp nhất với vòng lặp for là gì?
❌ 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. Tài liệu Deep Dive Oracle quy định gì về vòng lặp for?
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).
Đọc thêm: For và danh sách — nếu bẫy off-by-one (mục 5) vẫn còn khó hình dung, khoá Nhập môn lập trình dựng lại ý tưởng vòng lặp bằng trace tay từng bước, trước khi chạm cú pháp for cụ thể của bất kỳ ngôn ngữ nào.
11. Điểm cốt lõi cần nhớ về vòng lặp for là gì?
- Vòng lặp
forgom toàn bộ logic khởi tạo, kiểm tra điều kiện và cập nhật bước nhảy trên một dòng. - Biến đếm khai báo trong
initđược cô lập trong phạm vi vòng lặp, ngăn ngừa rò rỉ trạng thái. - Luôn sử dụng toán tử
< arr.lengthkhi duyệt mảng với chỉ mục bắt đầu từ 0 để tránh lỗi off-by-one. - JIT compiler tự động tối ưu hóa loop unrolling và loại bỏ bounds-check mà không cần can thiệp thủ công.
12. Tự kiểm tra kiến thức về vòng lặp for như thế nào?
- 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 có giúp bạn hiểu bản chất không?
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.
while và do-while — Lặp khi chưa biết trước số lần
Bài tiếpfor-each — Duyệt collection và mảng không cần index
Lộ trình Junior Java Backend Developer
Chặng 2/21 · Lập trình từ số 0 với Java — bài 23/32 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này