Một vòng lặp nằm trong một vòng lặp khác là cấu trúc đầu tiên trong series này có chi phí nhân
lên. Bài trước đã nói về bản thân các dạng vòng lặp — for, while, do-while — và mỗi dạng
chạy phần thân một số lần nào đó. Lồng cái này vào cái kia thì phần thân chạy bằng tích của hai
số lần, và đó là nơi sinh ra cả những pattern hữu ích lẫn những chương trình chậm.
Hai keyword cắt ngang vòng lặp, break và continue, rất đơn giản khi đứng một mình và trở nên
dễ hiểu nhầm ngay khi có hai vòng lặp. break chỉ thoát một vòng lặp, không bao giờ thoát cả
hai. continue bỏ qua phần còn lại của thân vòng lặp, nhưng nó xử lý bước update thế nào thì lại
phụ thuộc vào dạng vòng lặp bạn dùng. Mọi chương trình và mọi thông báo lỗi bên dưới đều được
compile và chạy thật trên OpenJDK 21.0.6.
![]()
Bắt đầu từ việc đếm, vì mọi thứ còn lại trong bài đều là biến thể của nó.
Vòng lặp lồng nhau chạy như thế nào?
Quy tắc gói gọn trong một câu: với mỗi lần lặp của vòng ngoài, vòng trong chạy từ điều kiện khởi đầu tới tận lúc kết thúc. Không phải một bước của nó — mà là toàn bộ.
public class NestedBasics {
public static void main(String[] args) {
int bodyRuns = 0;
for (int i = 1; i <= 3; i++) {
System.out.println("outer i=" + i + " starts");
for (int j = 1; j <= 4; j++) {
bodyRuns++;
System.out.println(" (i=" + i + ", j=" + j + ")");
}
System.out.println("outer i=" + i + " ends");
}
System.out.println("inner body ran " + bodyRuns + " times");
}
}
outer i=1 starts
(i=1, j=1)
(i=1, j=2)
(i=1, j=3)
(i=1, j=4)
outer i=1 ends
outer i=2 starts
(i=2, j=1)
(i=2, j=2)
(i=2, j=3)
(i=2, j=4)
outer i=2 ends
outer i=3 starts
(i=3, j=1)
(i=3, j=2)
(i=3, j=3)
(i=3, j=4)
outer i=3 ends
inner body ran 12 times

Ba điều mà đoạn output đó làm rõ:
- Counter của vòng trong khởi động lại mỗi lượt.
jchạy 1, 2, 3, 4 rồi được khai báo lại từ đầu ở lượt ngoài kế tiếp. Nó không mang số 4 sang hàng thứ hai. i++chạy ba lần,j++chạy mười hai lần. Counter của vòng ngoài chỉ nhích lên sau dòngouter i=1 ends, đúng chỗ điều kiện của vòng trong không còn đúng nữa.- Phần thân chạy 3 × 4 = 12 lần. Phép nhân đó chính là toàn bộ ý tưởng. Số lần ngoài nhân số lần trong, không phải cộng.
Chi phí của việc lồng vòng lặp, tính theo số lần lặp
Hai vòng lặp trên cùng một tập n phần tử sẽ chạy phần thân n × n lần. Con số thật đáng nhìn hơn công thức, nên đây là một bộ đếm — không đo thời gian, chỉ đếm số lần statement trong cùng được chạy tới:
public class NestedCost {
public static void main(String[] args) {
int[] sizes = {10, 100, 1000};
System.out.printf("%6s %12s %14s %18s%n", "n", "one loop", "two nested", "three nested");
for (int n : sizes) {
long one = 0, two = 0, three = 0;
for (int i = 0; i < n; i++) one++;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) two++;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
for (int k = 0; k < n; k++) three++;
System.out.printf("%6d %12d %14d %18d%n", n, one, two, three);
}
}
}
n one loop two nested three nested
10 10 100 1000
100 100 10000 1000000
1000 1000 1000000 1000000000
| n | một vòng lặp | hai vòng lồng nhau | ba vòng lồng nhau |
|---|---|---|---|
| 10 | 10 | 100 | 1.000 |
| 100 | 100 | 10.000 | 1.000.000 |
| 1.000 | 1.000 | 1.000.000 | 1.000.000.000 |
Dữ liệu nhiều gấp mười thì một vòng lặp tốn gấp mười, hai vòng tốn gấp trăm, ba vòng tốn gấp nghìn. Không có cấu hình máy nào đổi được tỉ lệ đó, và đó là lý do nên nhìn số lần lặp thay vì nhìn đồng hồ: chính cấu trúc của code quyết định con số này.
Với một vòng lặp lồng nhau chạy trên vài chục dòng của một bảng thì điều này không đáng bận tâm. Với một vòng lặp lồng nhau chạy trên danh sách lớn dần theo số người dùng, nó là khác biệt giữa một trang tải được và một trang timeout.
Ba dạng vòng lặp lồng nhau bạn sẽ viết đi viết lại
Gần như mọi vòng lặp lồng nhau bạn viết trong thực tế đều thuộc một trong ba dạng.
Lưới toạ độ
Vòng ngoài đi theo hàng, vòng trong đi theo cột, và việc xuống dòng thuộc về vòng ngoài:
public class Grid {
public static void main(String[] args) {
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 5; col++) {
System.out.printf("%8s", "(" + row + "," + col + ")");
}
System.out.println();
}
}
}
(0,0) (0,1) (0,2) (0,3) (0,4)
(1,0) (1,1) (1,2) (1,3) (1,4)
(2,0) (2,1) (2,2) (2,3) (2,4)
Lệnh System.out.println() không có argument nằm trong thân vòng ngoài, sau khi vòng trong đã
đóng lại. Đưa nó vào trong vòng trong thì bạn được mười lăm dòng, mỗi dòng một ô; bỏ hẳn nó đi thì
bạn được đúng một dòng rất dài.
Bảng cửu chương
Vẫn dạng đó, nhưng hai counter được dùng chung trong phần thân thay vì in riêng lẻ:
public class TimesTable {
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= 5; j++) {
System.out.printf("%4d", i * j);
}
System.out.println();
}
}
}
1 2 3 4 5
2 4 6 8 10
3 6 9 12 15
4 8 12 16 20
5 10 15 20 25
%4d lo phần căn cột. Chèn thêm space vào chuỗi cũng ra kết quả tương tự, nhưng độ rộng sẽ lệch
ngay khi có tích ba chữ số.
So sánh từng cặp trong một danh sách
Đây là dạng có mẹo thật sự. Để so sánh mỗi cặp đúng một lần, vòng trong bắt đầu từ i + 1 thay vì
từ 0:
public class Pairs {
public static void main(String[] args) {
String[] names = {"An", "Binh", "Chi", "Dung"};
int comparisons = 0;
for (int i = 0; i < names.length; i++) {
for (int j = i + 1; j < names.length; j++) {
comparisons++;
System.out.println(names[i] + " vs " + names[j]);
}
}
System.out.println("comparisons = " + comparisons);
}
}
An vs Binh
An vs Chi
An vs Dung
Binh vs Chi
Binh vs Dung
Chi vs Dung
comparisons = 6
Sáu lần so sánh cho bốn cái tên. Cho vòng trong bắt đầu từ 0 thì con số là mười sáu: mỗi cặp hai lần, cộng thêm bốn lần so sánh vô nghĩa giữa một phần tử với chính nó.
for (int i = 0; i < names.length; i++) {
for (int j = 0; j < names.length; j++) {
comparisons++;
}
}
comparisons = 16
j = i + 1 tốn n × (n − 1) / 2 thay vì n × n. Vẫn là bậc hai, nhưng làm một nửa khối lượng và —
quan trọng hơn — không bao giờ đưa cho code so sánh của bạn một cặp mà hai vế là cùng một phần tử,
vốn là một nguồn bug riêng.
break chỉ thoát khỏi vòng lặp gần nhất
break kết thúc đúng vòng lặp mà nó được viết bên trong. Trong một vòng lặp lồng nhau, đó là vòng
trong, còn vòng ngoài vẫn chạy tiếp như chưa có gì xảy ra:
public class BreakInner {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
System.out.println("outer i=" + i);
for (int j = 1; j <= 4; j++) {
if (j == 3) {
System.out.println(" break at j=3");
break;
}
System.out.println(" inner j=" + j);
}
System.out.println(" after inner loop, i=" + i);
}
System.out.println("done");
}
}
outer i=1
inner j=1
inner j=2
break at j=3
after inner loop, i=1
outer i=2
inner j=1
inner j=2
break at j=3
after inner loop, i=2
outer i=3
inner j=1
inner j=2
break at j=3
after inner loop, i=3
done
after inner loop in ra ba lần, mỗi lượt ngoài một lần. break cũng nổ ba lần — mỗi lượt một
lần — và lần nào nó cũng chỉ huỷ những giá trị j còn lại. Chương trình chạy tiếp từ statement
đầu tiên sau dấu ngoặc đóng của vòng trong, tức là vẫn nằm trong thân vòng ngoài.
Đây là hiểu nhầm phổ biến nhất về break, và cũng là lý do labeled break tồn tại ở phần dưới.
continue bỏ qua phần còn lại của lượt lặp
continue không kết thúc vòng lặp. Nó bỏ dở lượt hiện tại và đi tiếp sang lượt kế:
for (int i = 1; i <= 6; i++) {
if (i == 4) {
break;
}
System.out.println("i=" + i);
}
System.out.println("after the loop");
i=1
i=2
i=3
after the loop
for (int i = 1; i <= 6; i++) {
if (i == 4) {
continue;
}
System.out.println("i=" + i);
}
System.out.println("after the loop");
i=1
i=2
i=3
i=5
i=6
after the loop
Cùng một vòng lặp, cùng một điều kiện, khác đúng một keyword. break dừng hẳn ở 3. continue chỉ
bỏ qua giá trị 4 rồi chạy tiếp tới 5 và 6.

Điều quan trọng là continue nhảy tới đâu, và chỗ đó không giống nhau ở mọi dạng vòng lặp.
Trong vòng for, continue vẫn chạy bước update
continue trong vòng for nhảy tới phần update trên header của vòng lặp, rồi mới tới điều kiện.
Phần update không thuộc thân vòng lặp, nên bỏ qua thân không có nghĩa là bỏ qua nó. Muốn chứng
minh thì cần một biểu thức update tự lên tiếng:
public class ContinueUpdateProof {
public static void main(String[] args) {
for (int i = 1; i <= 4; i = bump(i)) {
if (i == 2) {
System.out.println("i=2 -> continue");
continue;
}
System.out.println("body i=" + i);
}
}
private static int bump(int i) {
System.out.println(" update: " + i + " -> " + (i + 1));
return i + 1;
}
}
body i=1
update: 1 -> 2
i=2 -> continue
update: 2 -> 3
body i=3
update: 3 -> 4
body i=4
update: 4 -> 5
Dòng update: 2 -> 3 xuất hiện ngay sau continue. Đó chính là bảo đảm khiến continue an toàn
trong vòng for: counter luôn nhích lên, nên vòng lặp luôn có ngày kết thúc.
Trong vòng while, continue bỏ qua luôn bước update
Vòng while không có chỗ dành riêng cho update. Lệnh tăng chỉ là một statement như mọi statement
khác trong thân, thường là dòng cuối cùng — nên continue nhảy vọt qua nó, quay thẳng về điều
kiện, với counter y nguyên:
public class ContinueWhileInfinite {
public static void main(String[] args) {
int i = 1;
while (i <= 6) {
if (i % 2 == 0) continue;
System.out.println("i=" + i);
i++;
}
System.out.println("done");
}
}
i=1
Đó là toàn bộ output. Chương trình in i=1, tăng lên 2, thấy 2 là số chẵn, gặp continue, quay
lại i <= 6 với i vẫn là 2, lại thấy 2 chẵn, và cứ thế mãi mãi. Phải kill nó từ bên ngoài; nó
không bao giờ tới được done.
Muốn nhìn thấy trạng thái nó bị kẹt, đây vẫn là vòng lặp đó nhưng có một bộ đếm vòng quay ép nó thoát ra:
public class ContinueWhileBug {
public static void main(String[] args) {
int i = 1;
int spins = 0;
while (i <= 6) {
if (++spins > 10) { // safety net, only so this can be printed
System.out.println("stuck: i is still " + i + " after 10 spins");
break;
}
if (i % 2 == 0) {
continue; // jumps straight back to the condition
}
System.out.println("i=" + i);
i++;
}
System.out.println("done");
}
}
i=1
stuck: i is still 2 after 10 spins
done
Mười vòng quay và i không hề rời khỏi số 2. Cách sửa là để bước update chạy trước khi có thể
chạm tới bất kỳ continue nào:
public class ContinueWhileFix {
public static void main(String[] args) {
int i = 0;
while (i < 6) {
i++; // update FIRST, before any continue
if (i % 2 == 0) {
continue;
}
System.out.println("i=" + i);
}
System.out.println("done");
}
}
i=1
i=3
i=5
done
| Dạng vòng lặp | Bước update nằm ở đâu | continue làm gì với nó |
|---|---|---|
for | trên header của vòng lặp | chạy nó, rồi kiểm tra lại điều kiện |
while | đâu đó trong thân | bỏ qua nếu nó nằm sau continue |
do-while | đâu đó trong thân | bỏ qua, rồi kiểm tra điều kiện ở cuối |
Quy tắc thực dụng: trong vòng while, hoặc đặt bước update ngay đầu thân vòng lặp, hoặc chuyển
sang dùng for. Vòng for chính là vòng while mà bước update không thể bị bỏ qua.
Labeled break và labeled continue
Label là một identifier kèm dấu hai chấm, đặt ngay trước một vòng lặp. break label và
continue label khi đó tác động lên chính vòng lặp đó thay vì vòng gần nhất. Cùng một cặp vòng
lặp lồng nhau, cùng một điều kiện, chạy ba kiểu:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (i * j == 4) {
break;
}
System.out.println("i=" + i + " j=" + j);
}
}
System.out.println("done");
i=1 j=1
i=1 j=2
i=1 j=3
i=2 j=1
i=3 j=1
i=3 j=2
i=3 j=3
done
break thường nổ khi i bằng 2 và j bằng 2, huỷ phần còn lại của lượt trong đó, rồi vòng ngoài
đi tiếp sang i=3. Bây giờ thêm label:
outer:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (i * j == 4) {
break outer;
}
System.out.println("i=" + i + " j=" + j);
}
}
System.out.println("done");
i=1 j=1
i=1 j=2
i=1 j=3
i=2 j=1
done

Chương trình nhảy qua luôn dấu ngoặc đóng của vòng lặp có label, tới thẳng lệnh println đứng sau
nó. Còn continue outer bỏ dở phần còn lại của lượt ngoài hiện tại và bắt đầu lượt kế tiếp:
outer:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (i * j == 4) {
continue outer;
}
System.out.println("i=" + i + " j=" + j);
}
System.out.println(" end of row i=" + i);
}
System.out.println("done");
i=1 j=1
i=1 j=2
i=1 j=3
end of row i=1
i=2 j=1
i=3 j=1
i=3 j=2
i=3 j=3
end of row i=3
done
Để ý dòng end of row i=2 biến mất. continue outer bỏ qua cả phần còn lại của thân vòng ngoài
chứ không riêng vòng trong — statement đó thuộc về lượt lặp mà nó vừa bỏ dở. Giống mọi continue
trong vòng for, nó vẫn chạy bước update của vòng ngoài:
outer:
for (int i = 1; i <= 3; i = bump(i)) {
for (int j = 1; j <= 2; j++) {
if (j == 2) {
System.out.println(" continue outer at i=" + i + " j=" + j);
continue outer;
}
System.out.println("body i=" + i + " j=" + j);
}
System.out.println(" never reached");
}
body i=1 j=1
continue outer at i=1 j=2
outer update: 1 -> 2
body i=2 j=1
continue outer at i=2 j=2
outer update: 2 -> 3
body i=3 j=1
continue outer at i=3 j=2
outer update: 3 -> 4
Label phải trỏ tới một vòng lặp thật sự bao quanh statement đó, và phải viết đúng chính tả. Gõ sai là lỗi compile chứ không phải bất ngờ lúc runtime:
outer:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
break outter;
}
}
BadLabel.java:6: error: undefined label: outter
break outter;
^
1 error
Label rất hiếm, tách method rồi return thường tốt hơn
Label là cú pháp hợp lệ, đôi khi đúng là thứ bạn cần, và bạn có thể viết Java thật hàng tháng trời
mà không gặp cái nào. Lý do là một cặp vòng lặp lồng nhau đáng để thoát ra thường cũng là một mảng
logic đáng được đặt tên, và khi nó đã thành method riêng thì return làm đúng việc đó mà không cần
cú pháp mới:
public class FirstPairMethod {
public static void main(String[] args) {
printUntilProductIsFour();
System.out.println("done");
}
private static void printUntilProductIsFour() {
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (i * j == 4) {
return; // leaves the method, so both loops
}
System.out.println("i=" + i + " j=" + j);
}
}
}
}
i=1 j=1
i=1 j=2
i=1 j=3
i=2 j=1
done
Output giống hệt bản dùng label. Bản tách method còn có thêm một cái tên nói rõ hai vòng lặp đó để làm gì, và có thể test riêng. Hãy dùng label khi việc tách method đồng nghĩa với truyền sáu biến local ra vào; những lúc còn lại thì tách method.
break trong switch là thoát switch, không phải thoát loop
Đây là một cái bẫy thật sự, vì cùng một keyword mang hai nghĩa khác nhau tuỳ theo cái gì đang bao
quanh nó. Bên trong switch, break kết thúc switch. Nếu switch đó tình cờ nằm trong một
vòng lặp thì vòng lặp không hề bị đụng tới:
public class SwitchBreakTrap {
public static void main(String[] args) {
String[] commands = {"add", "add", "quit", "add"};
for (String cmd : commands) {
switch (cmd) {
case "add":
System.out.println("adding");
break;
case "quit":
System.out.println("quitting");
break; // this leaves the SWITCH, not the for loop
}
}
System.out.println("loop finished");
}
}
adding
adding
quitting
adding
loop finished
Chương trình in quitting rồi vẫn xử lý thêm một lệnh nữa. break đã làm đúng việc nó luôn làm
trong switch: chặn không cho rơi xuống case kế tiếp. Nó chưa bao giờ có khả năng dừng vòng lặp.
Label là cách sửa trực tiếp, và đây là chỗ hiếm hoi label thật sự xứng đáng, vì không có keyword
nào khác với tới được qua khỏi switch:
loop:
for (String cmd : commands) {
switch (cmd) {
case "add":
System.out.println("adding");
break;
case "quit":
System.out.println("quitting");
break loop; // this one leaves the for loop
}
}
System.out.println("loop finished");
adding
adding
quitting
loop finished
Cách còn lại là một cờ hiệu được kiểm tra ngay trên điều kiện của vòng lặp, và dùng kèm case dạng
mũi tên thì đọc rất gọn vì case mũi tên không cần break:
boolean running = true;
for (int i = 0; i < commands.length && running; i++) {
switch (commands[i]) {
case "add" -> System.out.println("adding");
case "quit" -> {
System.out.println("quitting");
running = false;
}
}
}
System.out.println("loop finished");
adding
adding
quitting
loop finished
Bản dùng cờ hiệu có một khác biệt về hành vi đáng nhớ: nó chạy hết lượt hiện tại rồi mới dừng,
trong khi break loop dừng ngay lập tức. Khi phần còn lại của lượt lặp không làm gì cả, như ở
đây, output của hai bản là như nhau.
Tìm trong cấu trúc hai chiều và dừng ngay khi thấy
Ứng dụng kinh điển của labeled break là quét một lưới để tìm một giá trị rồi thoát ra ngay khi tìm thấy. Mỗi lần dò thêm sau đó đều là công vô ích:
public class SearchLabeled {
public static void main(String[] args) {
int[][] grid = {
{4, 8, 15},
{16, 23, 42},
{7, 23, 99}
};
int target = 23;
int foundRow = -1;
int foundCol = -1;
int probes = 0;
search:
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[r].length; c++) {
probes++;
if (grid[r][c] == target) {
foundRow = r;
foundCol = c;
break search;
}
}
}
System.out.println("probes=" + probes);
System.out.println("found at row=" + foundRow + " col=" + foundCol);
}
}
probes=5
found at row=1 col=1
Năm lần dò trên chín ô, và số 23 thứ hai ở hàng 2 không bao giờ bị chạm tới. Bản dùng cờ hiệu
làm đúng chuyện đó với một boolean phụ và một điều kiện thêm trên vòng ngoài:
boolean found = false;
for (int r = 0; r < grid.length && !found; r++) {
for (int c = 0; c < grid[r].length; c++) {
probes++;
if (grid[r][c] == target) {
foundRow = r;
foundCol = c;
found = true;
break;
}
}
}
probes=5
found at row=1 col=1
Cùng số lần dò, cùng kết quả. Đánh đổi rất rõ ràng: bản dùng label có một cú nhảy và không cần
trạng thái phụ, bản dùng cờ hiệu không có cú pháp lạ nhưng phải giữ hai thứ đồng bộ — found = true
và lệnh break ngay sau đó. Quên break là vòng trong chạy hết hàng rồi vòng ngoài mới kiểm tra
lại điều kiện.
return là lối thoát mạnh nhất
Cả hai bản trên đều là code tìm kiếm nằm giữa main. Chuyển nó vào một method thì bài toán thoát
ra biến mất, vì return rời khỏi mọi thứ cùng lúc — mọi vòng lặp bao quanh, và cả method:
public class SearchMethod {
public static void main(String[] args) {
int[][] grid = {
{4, 8, 15},
{16, 23, 42},
{7, 23, 99}
};
int[] hit = find(grid, 23);
if (hit == null) {
System.out.println("not found");
} else {
System.out.println("found at row=" + hit[0] + " col=" + hit[1]);
}
System.out.println("missing 100 -> " + (find(grid, 100) == null ? "not found" : "found"));
}
private static int[] find(int[][] grid, int target) {
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[r].length; c++) {
if (grid[r][c] == target) {
return new int[] {r, c};
}
}
}
return null;
}
}
found at row=1 col=1
missing 100 -> not found
Không label, không cờ hiệu, không có foundRow khởi tạo bằng giá trị canh chừng -1 mà người gọi
phải nhớ kiểm tra. Trường hợp "không tìm thấy" gọn trong một return null ở cuối, chạy tới đúng
khi hai vòng lặp kết thúc mà không trúng gì.
| Lối thoát | Rời khỏi | Dùng khi |
|---|---|---|
break | vòng lặp hoặc switch gần nhất | bạn xong việc với riêng vòng lặp này |
break label | vòng lặp mang label và mọi thứ bên trong | phải thoát nhiều hơn một vòng lặp tại chỗ |
continue | lượt lặp hiện tại | phần tử này không đáng quan tâm |
continue label | lượt lặp hiện tại của vòng mang label | cả phần tử ngoài này không đáng quan tâm |
return | method, cùng mọi vòng lặp trong nó | hai vòng lặp là một phép tìm kiếm và method chính là phép tìm đó |
Lồng sâu tới đâu là quá sâu?
Hai tầng là bình thường. Ba tầng là dấu hiệu đáng ngờ. Bốn tầng thì gần như chắc chắn đang có một method ẩn trong đó. Triệu chứng đo được là phần thân trôi dần sang phải và các điều kiện không còn đọc chung với nhau được nữa:
public class DeepNesting {
public static void main(String[] args) {
String[][] rows = {
{"An", "", "HN"},
{"Binh", "b@x.vn", "SG"},
{"", "c@x.vn", "DN"}
};
for (int r = 0; r < rows.length; r++) {
boolean complete = true;
for (int c = 0; c < rows[r].length; c++) {
if (rows[r][c].isEmpty()) {
complete = false;
break;
}
}
System.out.println("row " + r + " complete=" + complete);
}
}
}
row 0 complete=false
row 1 complete=true
row 2 complete=false
Code chạy đúng, và nó cần cờ complete chỉ để mang câu trả lời của vòng trong ra ngoài cho vòng
ngoài. Tách vòng trong ra thì cái cờ đó thành giá trị trả về:
public class ExtractedLoop {
public static void main(String[] args) {
String[][] rows = {
{"An", "", "HN"},
{"Binh", "b@x.vn", "SG"},
{"", "c@x.vn", "DN"}
};
for (int r = 0; r < rows.length; r++) {
System.out.println("row " + r + " complete=" + isComplete(rows[r]));
}
}
private static boolean isComplete(String[] row) {
for (String field : row) {
if (field.isEmpty()) {
return false;
}
}
return true;
}
}
row 0 complete=false
row 1 complete=true
row 2 complete=false
Output giống hệt, chỉ còn một tầng lồng, không còn cờ thay đổi được, và vòng lặp trong giờ có tên
nói rõ nó quyết định điều gì. Đây đúng là nước đi của method find phía trên, và cũng là câu trả
lời cho gần như mọi câu hỏi "làm sao thoát khỏi cả hai vòng lặp".
Những lỗi hay gặp
Dùng lại counter của vòng ngoài cho vòng trong
Khai báo counter của vòng trong trùng tên với vòng ngoài thì không compile được, và đó là kết cục may mắn:
for (int i = 1; i <= 3; i++) {
for (int i = 1; i <= 3; i++) {
System.out.println(i);
}
}
ShadowCounter.java:4: error: variable i is already defined in method main(String[])
for (int i = 1; i <= 3; i++) {
^
1 error
Bản nguy hiểm là bản compile được: bỏ chữ int đi và dùng lại đúng biến cũ. Giờ hai vòng lặp dùng
chung một counter, và số lần lặp của vòng ngoài bị vòng trong phá hỏng:
int runs = 0;
for (int i = 1; i <= 3; i++) {
for (i = 1; i <= 3; i++) { // same i, no new declaration
runs++;
}
}
System.out.println("body ran " + runs + " times, expected 9");
body ran 3 times, expected 9
Ba, chứ không phải chín. Vòng trong đẩy i lên 4, lệnh i++ của vòng ngoài đưa nó lên 5, và
5 <= 3 sai ngay ở lượt ngoài đầu tiên. Hãy đặt tên khác nhau cho hai vòng lặp — i và j, hoặc
row và col — và khai báo mỗi cái ngay trên header của nó.
Khai báo counter của vòng trong ở ngoài vòng lặp
Nửa còn lại của cùng một vấn đề. Counter khai báo trước vòng ngoài sẽ không được reset ở mỗi lượt ngoài, nên vòng trong chạy một lần rồi cạn kiệt vĩnh viễn:
int j = 1; // declared outside, never reset
int runs = 0;
for (int i = 1; i <= 3; i++) {
for (; j <= 3; j++) {
runs++;
System.out.println("i=" + i + " j=" + j);
}
}
System.out.println("body ran " + runs + " times, expected 9");
i=1 j=1
i=1 j=2
i=1 j=3
body ran 3 times, expected 9
i=2 và i=3 không in ra gì cả, vì j đã bằng 4 từ trước khi chúng bắt đầu. Hãy khai báo counter
ngay trên header của for — for (int j = 1; ...) — để phần khởi tạo chạy lại mỗi lần bước vào
vòng lặp, đúng như bạn muốn.
Quên rằng continue bỏ qua bước update của while
Đã nói đầy đủ ở trên, và đáng nhắc lại vì đây là lỗi làm treo chương trình chứ không phải chỉ in
ra một con số sai. Nếu một vòng while có continue, hãy kiểm tra xem counter được tăng trước nó
hay sau nó.
Statement không bao giờ chạy tới sau break
Java từ chối những đoạn code chứng minh được là không thể chạy tới. Một statement nằm ngay sau
break trong cùng một block chính là loại đó:
for (int i = 1; i <= 3; i++) {
break;
System.out.println("i=" + i);
}
Unreachable.java:5: error: unreachable statement
System.out.println("i=" + i);
^
1 error
Điều tương tự xảy ra sau continue và sau return. Thường thì nó có nghĩa là break đã lọt ra
ngoài cái if lẽ ra phải bao nó — chính cái if đó mới làm statement phía sau trở nên có thể
chạy tới.
FAQ
Làm sao thoát khỏi hai vòng lặp cùng lúc trong Java?
Đặt label cho vòng ngoài rồi dùng break label, hoặc tách hai vòng lặp thành một method rồi
return. break thường không làm được: nó luôn kết thúc vòng lặp hoặc switch gần nhất. Bản
tách method thường là code tốt hơn, vì thứ bạn muốn thoát ra gần như luôn xứng đáng có một cái
tên.
Vì sao vòng lặp lồng nhau của tôi chỉ chạy đúng một lần?
Gần như luôn là do counter của vòng trong được khai báo bên ngoài và không bao giờ được reset,
hoặc do hai vòng lặp dùng chung một biến. Hãy khai báo mỗi counter ngay trên header for của nó,
và đặt tên khác nhau.
continue có bỏ qua lệnh tăng không?
Trong vòng for thì không — continue nhảy tới phần update trên header, chạy nó, rồi mới kiểm
tra điều kiện. Trong while hay do-while thì có, nếu lệnh tăng nằm sau continue trong thân
vòng lặp, và đó chính là cách continue tạo ra vòng lặp vô tận.
break trong switch nằm trong loop có dừng được loop không?
Không. Nó chỉ kết thúc switch, còn vòng lặp đi tiếp lượt kế. Hãy dùng break label trên vòng
lặp, hoặc đặt một cờ hiệu để điều kiện của vòng lặp kiểm tra, hoặc return nếu cả khối đó là một
method.
Vòng lặp lồng nhau có luôn chậm không?
Không — kích thước dữ liệu mới quyết định. Hai vòng lặp trên n phần tử chạy vào thân n² lần, tức
100 lần với 10 phần tử và một triệu lần với một nghìn phần tử. Lưới nhỏ cố định thì mãi mãi không
sao; đáng chú ý là những vòng lặp có cận lớn dần theo dữ liệu. Khi các cặp là đối xứng, cho vòng
trong bắt đầu từ i + 1 giảm được một nửa số lần lặp mà không mất gì.
Có thể đặt label cho thứ không phải vòng lặp không?
Có. Mọi statement đều mang được label, và break label nhảy tới cuối statement đó — kể cả một
block thường. Nó hợp lệ nhưng cực kỳ hiếm; hãy coi break trên một block có label là thứ để nhận
ra khi đọc code, không phải thứ để viết.
Kết luận
Vòng lặp lồng nhau nhân lên: phần thân trong chạy bằng số lần ngoài nhân số lần trong, tức là 12
với một lần chạy 3 × 4 và một triệu với hai vòng lặp trên một nghìn phần tử. break kết thúc vòng
lặp hoặc switch gần nhất, không hơn. continue bỏ dở lượt hiện tại, chạy bước update của vòng
for trên đường ra và bỏ qua bước update của vòng while, và đó là khác biệt giữa một vòng lặp
kết thúc được với một vòng lặp bạn phải kill. Label cho phép break và continue gọi tên một
vòng lặp xa hơn, và chúng là công cụ đúng để thoát khỏi switch nằm trong loop — nhưng phần lớn
thời gian, một method với return sớm mới là câu trả lời.
Đến đây là hết nửa control flow của series: điều kiện, vòng lặp, và các lối ra khỏi chúng. Bài kế
tiếp là một bộ bài tập tổng hợp về điều kiện và vòng lặp — một tập bài tập tăng dần độ khó, ghép
if, switch, for, while, lồng vòng lặp, break và continue lại với nhau, kèm lời giải và
output của từng bài.