Vòng lặp
Xem thửTình huống mở đầu
Giỏ hàng của khách ở tiệm Tạp Hoá Bình An có ba món: 45.000, 18.500 và 32.000 đồng, tổng cộng 95.500 đồng. Bạn viết một vòng for cộng giá cả giỏ. Sợ bỏ sót món cuối, bạn đổi i < prices.length thành i <= prices.length cho chắc. Chạy thử, tổng 95.500 được tính xong, rồi chương trình sập với ArrayIndexOutOfBoundsException ngay lúc in phiếu.
Giỏ chỉ có ba món, mà máy đã chạy vòng thứ tư. Vòng lặp đã chạy mấy lần, và vì sao lần cuối lại sập? Giữ câu hỏi này trong đầu, cuối bài bạn sẽ tự sửa nó. Khi giỏ có 50 món, hiểu rõ vòng lặp là cách duy nhất để không viết 50 dòng cộng.
Ôn nhanh bài trước: trong switch cổ điển, lệnh nào chặn máy chạy xuyên sang nhãn kế tiếp?
Đáp án: break. Không có nó, máy chạy tiếp mọi dòng bên dưới nhãn khớp cho tới khi gặp break hoặc hết switch.
Mục tiêu bài học
Sau bài này, bạn có thể:
- Viết được vòng
forcộng giá của các món trong mảng, và nói đúng thân vòng chạy bao nhiêu lần. - Mô tả được thứ tự khởi tạo, kiểm tra, thân, cập nhật của vòng
for, và vì sao điều kiện được kiểm tra nhiều hơn thân đúng một lần. - Chọn đúng
while,do-whilehayfor-eachcho từng việc, và nói được dạng nào có thể chạy thân 0 lần. - Sửa được lỗi lệch một đơn vị khi duyệt mảng, và dùng
break,continueđúng chỗ.
Bài này đo được bằng năm câu trong phần Tự kiểm tra, mỗi câu ghi rõ nó kiểm tra mục tiêu nào.
Hãy xem video dưới đây để có cái nhìn đầu tiên, sau đó mình đi chậm hơn từng bước ở các phần sau.
Bức tranh lớn (Big Picture)
Trước khi đi vào chi tiết, hãy xem các dạng vòng lặp và khi nào dùng từng dạng.
Cách đọc hình: ô trên cùng là một việc phải lặp lại, như cộng 50 giá hay quét một kệ hàng. Bốn ô ở giữa là bốn công cụ: for khi bạn biết hoặc đếm được số vòng, while và do-while khi chưa biết số vòng, for-each khi chỉ cần đọc từng phần tử, và break với continue để rời vòng sớm hoặc bỏ một vòng. Ô xám ở dưới cho thấy điểm khác nhau chính của while và do-while: kiểm tra điều kiện trước thân hay sau thân. Màu chỉ là tín hiệu phụ, mỗi ô đều có nhãn chữ.
Xuyên suốt bài, mình dùng một hình ảnh: người soát vé đi dọc một hàng ghế, từng ghế một, và làm cùng một động tác ở mỗi ghế. Dự án xuyên suốt là cộng giá và quét kệ hàng của tiệm Tạp Hoá Bình An. Có chỗ hình ảnh này không còn đúng, và mình sẽ chỉ rõ chỗ đó khi tới.
Khái niệm cốt lõi
Bài này có bốn khái niệm. Mỗi khái niệm đi kèm một hình.
Khái niệm 1: vòng for và bốn phần của nó
Vòng lặp (loop) là cách bảo máy chạy lại một khối lệnh nhiều lần. for gộp cả cơ chế đếm vào một dòng: for (khởi tạo; điều kiện; cập nhật) { thân }. Mỗi lần thân chạy xong được gọi là một vòng. Người soát vé của chúng ta bắt đầu ở ghế đầu, hỏi "còn ghế nào không", làm việc, rồi bước sang ghế kế.
Cách đọc hình: hàng trên đi từ trái sang phải. Khởi tạo int i = 0 chạy một lần. Rồi điều kiện i < prices.length được kiểm tra, và nếu đúng thì thân total += prices[i] chạy, rồi cập nhật i++, rồi quay về kiểm tra điều kiện. Mũi tên "false" đi xuống ô đỏ: khi điều kiện sai thì vòng kết thúc, và với ba món thì thân đã chạy 3 lần còn điều kiện đã được kiểm tra 4 lần.
Đây là khái niệm quan trọng nhất của bài, vì tình huống mở đầu đến từ đây: nếu điều kiện cho phép thêm một vòng thì vòng đó sẽ chạy thật. Chương trình CartSum ở mục Ví dụ đầu tiên bên dưới là ví dụ nhỏ nhất của vòng này.
Chỗ hình ảnh người soát vé không còn đúng: người soát vé thật biết hàng ghế dài bao nhiêu từ đầu. Máy thì không biết gì cả: nó chỉ hỏi điều kiện trước mỗi vòng, và nếu bạn viết điều kiện cho phép một vòng thừa, máy vẫn chạy vòng đó và đọc một ghế không tồn tại.
Concept Check 1. Vòng for (int i = 1; i <= 3; i++). Thân chạy bao nhiêu lần và điều kiện i <= 3 được kiểm tra bao nhiêu lần? Hãy chọn đáp án đúng trước khi đọc tiếp.
- A. Thân chạy 3 lần và điều kiện cũng được kiểm tra 3 lần.
- B. Thân chạy 3 lần và điều kiện được kiểm tra 4 lần.
- C. Thân chạy 4 lần và điều kiện cũng được kiểm tra 4 lần.
- D. Thân chạy 3 lần và điều kiện được kiểm tra 2 lần.
Đáp án: B. Điều kiện đúng ở i bằng 1, 2, 3 nên thân chạy ba lần. Sau lần cập nhật cuối i bằng 4, điều kiện được kiểm tra lần thứ tư và sai, nên vòng dừng. A quên mất lần kiểm tra cuối làm vòng dừng. C nhầm lần kiểm tra cuối với một vòng thân. D không có cơ sở: điều kiện luôn được kiểm tra trước mỗi vòng. [Đã chạy, JDK 17.0.15, xem phần Ví dụ.]
Khái niệm 2: while và do-while, kiểm tra trước hay sau
while kiểm tra điều kiện trước mỗi vòng, nên có thể chạy thân 0 lần. do-while chạy thân trước rồi mới kiểm tra, nên luôn chạy ít nhất một lần. Chọn while khi bạn không biết trước số vòng, như bán từng gói hàng cho tới khi hết kho.
Cách đọc hình: hàng trên là while (stock >= 4) với tồn kho 3: kiểm tra trước, sai ngay, nên thân không bao giờ bắt đầu và không bán gói nào. Hàng dưới là do-while cùng điều kiện: thân chạy trước và bán một gói, tồn kho thành -1, rồi mới kiểm tra thấy sai và dừng. Ô đỏ cho thấy hậu quả: kho âm.
Đoạn code dưới đây chạy cả hai dạng với cùng tồn kho 3:
Forms.java
public class Forms {
public static void main(String[] args) {
int stock = 3;
int whilePacks = 0;
while (stock >= 4) { // kiểm tra trước nên sai ngay từ đầu, thân chạy 0 lần
stock -= 4;
whilePacks++;
}
System.out.println("while: " + whilePacks + " packs, stock " + stock);
int doPacks = 0;
do {
stock -= 4; // thân chạy trước rồi mới kiểm tra, nên luôn chạy ít nhất một lần
doPacks++;
} while (stock >= 4);
System.out.println("do-while: " + doPacks + " pack, stock " + stock);
}
}
Kết quả [Đã chạy, JDK 17.0.15]:
while: 0 packs, stock 3
do-while: 1 pack, stock -1
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
while (stock >= 4) { ... } | Kiểm tra 3 >= 4, sai nên bỏ qua thân | Kiểm tra trước, nên an toàn khi kho không đủ |
stock -= 4; trong do | Trừ 4 khỏi kho, kho thành -1 | Thân của do-while chạy trước, không ai kiểm tra kho |
} while (stock >= 4); | Kiểm tra sau thân, -1 >= 4 sai nên dừng | Có dấu chấm phẩy ở cuối, quên nó là lỗi biên dịch |
doPacks++ | Đếm một gói đã bán | Cho thấy do-while đã chạy thân đúng một lần |
Dùng do-while khi thân phải chạy trước, như hiện thực đơn rồi mới hỏi khách có muốn tiếp không. Với việc bán hàng theo kho, while mới đúng.
Khái niệm 3: duyệt mảng, for-each hay chỉ số, và lệch một đơn vị
for-each là vòng for (int price : prices) đi qua từng phần tử, không có chỉ số. Biến price là một bản sao của phần tử: đổi nó không đổi mảng. Muốn sửa phần tử hay biết vị trí thì dùng vòng for với chỉ số i. Lệch một đơn vị (off by one) là lỗi viết điều kiện hơn hoặc kém một vòng, thường là viết <= thay vì < khi duyệt mảng.
Cách đọc hình: hàng trên màu đỏ là vòng for-each: price là bản sao, price = price * 2 chỉ đổi bản sao nên prices[0] vẫn là 45000. Hàng dưới màu lục là vòng for với chỉ số: prices[i] = prices[i] * 2 ghi thẳng vào mảng nên prices[0] thành 90000.
Đoạn trích dưới đây là phần chính của ForEach:
for (int price : prices) {
price = price * 2; // chỉ đổi bản sao, mảng không đổi
}
for (int i = 0; i < prices.length; i++) {
prices[i] = prices[i] * 2; // muốn đổi phần tử thì phải dùng chỉ số
}
sum = 95500, prices[0] = 45000
after index loop, prices[0] = 90000
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
for (int price : prices) | Gán lần lượt từng phần tử vào price | Chỉ đọc nên gọn hơn và không thể lệch một đơn vị |
price = price * 2; | Nhân đôi hộp price | Chỉ đổi bản sao, nên in ra prices[0] vẫn 45000 |
i < prices.length | Dừng khi i bằng độ dài | Chỉ số cuối hợp lệ là length - 1, nên dùng < |
prices[i] = prices[i] * 2; | Ghi giá trị mới vào ô thứ i | Đây là cách duy nhất sửa phần tử, prices[0] thành 90000 |
Còn tình huống mở đầu là viết i <= prices.length: vòng thứ tư gọi prices[3] và máy ném ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3, sau khi ba vòng đầu đã cộng đủ 95500 [Đã chạy, JDK 17.0.15].
Khái niệm 4: break và continue
break thoát hẳn khỏi vòng lặp ngay lập tức. continue bỏ phần còn lại của vòng hiện tại và đi tiếp sang vòng sau. Người soát vé đứng lại không đi tiếp nữa là break, còn bỏ qua một ghế trống rồi đi tiếp là continue.
Cách đọc hình: hàng trên là tìm ô trống đầu tiên trên kệ 5, 8, 0, 3, 0: máy gặp ô trống ở chỉ số 2, break thoát vòng, firstEmpty bằng 2 và ô 3, ô 4 không bao giờ được thăm. Hàng dưới là đếm ô còn hàng trên cùng kệ: mỗi ô trống gặp continue nên bỏ qua lệnh đếm, nhưng cả năm ô đều được thăm và filled bằng 3.
BreakContinue.java
public class BreakContinue {
public static void main(String[] args) {
int[] shelf = {5, 8, 0, 3, 0};
int firstEmpty = -1;
for (int i = 0; i < shelf.length; i++) {
if (shelf[i] == 0) {
firstEmpty = i;
break; // thoát hẳn vòng lặp khi đã tìm thấy
}
}
int filled = 0;
for (int units : shelf) {
if (units == 0) {
continue; // bỏ qua phần còn lại của vòng này, sang vòng sau
}
filled++;
}
System.out.println("first empty at " + firstEmpty + ", filled slots " + filled);
}
}
Kết quả [Đã chạy, JDK 17.0.15]:
first empty at 2, filled slots 3
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
if (shelf[i] == 0) { ... } | Kiểm tra từng ô theo chỉ số | Cần chỉ số để ghi lại vị trí ô trống |
firstEmpty = i; rồi break; | Ghi chỉ số 2 rồi thoát hẳn vòng | Đã tìm thấy thì không cần xem các ô sau |
for (int units : shelf) | Đi qua từng ô, không cần chỉ số | Chỉ đếm nên dùng for-each là đủ |
continue; | Bỏ qua filled++ của ô trống, sang ô kế | Chỉ bỏ một vòng, các ô sau vẫn được thăm |
Hãy xem từng bước máy thực hiện:
Đang tải Animation…
Bảng trace (bản chữ tương đương của animation, [Suy luận] và khớp với kết quả chạy thật):
| Vòng | Ô được xét | Giá trị | Việc xảy ra | firstEmpty hoặc filled |
|---|---|---|---|---|
| Vòng tìm | 0 | 5 | Khác 0, đi tiếp | firstEmpty vẫn -1 |
| Vòng tìm | 1 | 8 | Khác 0, đi tiếp | firstEmpty vẫn -1 |
| Vòng tìm | 2 | 0 | Ghi chỉ số rồi break | firstEmpty bằng 2 |
| Vòng đếm | 0 và 1 | 5 và 8 | Đếm | filled bằng 2 |
| Vòng đếm | 2 | 0 | continue, không đếm | filled vẫn 2 |
| Vòng đếm | 3 và 4 | 3 và 0 | Đếm ô 3, continue ô 4 | filled bằng 3 |
Ví dụ đầu tiên (Worked Example) và "Xem từng bước"
Đây là chương trình đầu tiên của dự án: cộng giá ba món trong giỏ hàng của tiệm Tạp Hoá Bình An bằng một vòng for.
CartSum.java
public class CartSum {
public static void main(String[] args) {
int[] prices = {45_000, 18_500, 32_000};
int total = 0;
for (int i = 0; i < prices.length; i++) { // khởi tạo một lần, kiểm tra trước mỗi vòng, cập nhật sau mỗi vòng
total += prices[i];
System.out.println("i=" + i + " total=" + total);
}
System.out.println("sum = " + total);
}
}
Predict: hãy dừng lại, lấy giấy nháp và đoán: bạn đoán nó in ra gì? Và trạng thái bên trong ra sao, tức là i và total mang giá trị nào ở mỗi vòng, thân chạy mấy lần, điều kiện được kiểm tra mấy lần? Đoán xong rồi hãy đọc tiếp.
Kết quả mong đợi [Đã chạy, JDK 17.0.15]:
i=0 total=45000
i=1 total=63500
i=2 total=95500
sum = 95500
Đọc code từng dòng:
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
int[] prices = {45_000, 18_500, 32_000}; | Làm một mảng ba phần tử, đánh chỉ số 0, 1, 2 | Mảng là dãy hộp liền nhau, mỗi hộp có một chỉ số bắt đầu từ 0 |
for (int i = 0; ... | Làm hộp i bằng 0, đúng một lần | Phần khởi tạo chỉ chạy một lần trước mọi vòng |
i < prices.length | Kiểm tra trước mỗi vòng, đúng thì vào thân | length bằng 3, nên i chạy từ 0 đến 2, không có i bằng 3 |
i++ | Tăng i thêm 1 sau mỗi thân | Phần cập nhật chạy sau thân, rồi máy quay về kiểm tra |
total += prices[i]; | Cộng giá ở chỉ số i vào total | Mỗi vòng đọc một ô khác nhau nhờ i thay đổi |
Bây giờ xem từng bước. Mỗi bước nêu phần của vòng for đang chạy, giá trị của i và total, và lý do:
Đang tải Animation…
Bảng trace (bản chữ tương đương của animation, [Suy luận] và khớp với kết quả chạy thật):
| Bước | Lần kiểm tra | i | Điều kiện | Thân chạy | total |
|---|---|---|---|---|---|
| Khởi tạo | chưa có | 0 | chưa có | không | 0 |
| Vòng 1 | 1 | 0 | 0 < 3 đúng | có | 45000 |
| Vòng 2 | 2 | 1 | 1 < 3 đúng | có | 63500 |
| Vòng 3 | 3 | 2 | 2 < 3 đúng | có | 95500 |
| Kết thúc | 4 | 3 | 3 < 3 sai | không | 95500 |
Bạn vừa làm được: đọc một vòng for và nói đúng nó chạy mấy vòng, điều kiện kiểm tra mấy lần, và i, total mang giá trị nào sau từng vòng.
Concept Check xen kẽ
Chọn đáp án của bạn trước, rồi đọc phần đáp án ngay bên dưới mỗi câu.
Concept Check 2. Cho int stock = 3; rồi chạy do { stock -= 4; } while (stock >= 4);. Giá trị của stock sau đó là gì?
- A. 3, vì điều kiện
stock >= 4sai ngay nên thân không chạy lần nào. - B. Lỗi biên dịch, vì một biến kho không được phép nhận giá trị âm.
- C. Chương trình chạy mãi, vì
stockluôn nhỏ hơn 4 nên vòng không dừng. - D. -1, vì thân chạy một lần trước khi điều kiện được kiểm tra.
Đáp án: D. do-while chạy thân trước, nên 3 trừ 4 thành -1, rồi điều kiện -1 >= 4 sai nên dừng. A là hành vi của while, không phải do-while. B sai vì không có quy tắc nào cấm giá trị âm. C đảo lại điều kiện: điều kiện đúng mới làm vòng chạy tiếp, mà nó sai. [Đã chạy, JDK 17.0.15.]
Concept Check 3. int[] prices = {45_000, 18_500, 32_000}; rồi for (int p : prices) { p = p * 2; } và in prices[0]. Kết quả là gì?
- A. 90000, vì
plà chính phần tử của mảng nên đổiplà đổi mảng. - B. Lỗi biên dịch, vì không được gán lại biến của vòng for-each.
- C. 45000, vì
plà bản sao của phần tử nên mảng không bị đổi. - D. 0, vì sau vòng for-each các phần tử của mảng bị xoá.
Đáp án: C. Mỗi vòng, p nhận một bản sao của phần tử, nên nhân đôi p không chạm tới mảng. A là hiểu lầm hay gặp nhất. B sai vì gán lại p hợp lệ, chỉ là vô ích. D sai vì for-each chỉ đọc, không xoá gì. [Đã chạy, JDK 17.0.15.]
Làm cùng mình (Guided Practice)
Bạn sẽ dựng dần class CartDesk qua năm bước nhỏ, mỗi bước thêm vào chương trình của bước trước. Hỗ trợ giảm dần: bước 1 gần như làm sẵn, bước 5 chỉ còn đề. Gõ code vào file CartDesk.java rồi chạy bằng java CartDesk.java.
Bước 1: cộng giá cả giỏ.
public class CartDesk {
public static void main(String[] args) {
int[] prices = {45_000, 18_500, 32_000};
int sum = 0;
// TODO 1: dùng vòng for theo chỉ số để cộng mọi phần tử của prices vào sum
System.out.println("step 1: " + sum);
}
}
Chạy xong bạn sẽ thấy: step 1: 95500.
Vẽ ra giấy: kẻ bảng bốn dòng với cột lần kiểm tra, i, điều kiện, sum. Điền từng vòng. Đối chiếu: có 4 lần kiểm tra, 3 vòng thân, và dòng cuối 3 < 3 sai.
Bước 2: đếm món đắt bằng for-each. Thêm ngay dưới dòng in của bước 1:
int bigOnes = 0;
// TODO 2: đếm số món có giá từ 30000 trở lên bằng vòng for-each và if
System.out.println("step 2: " + bigOnes);
Chạy xong bạn sẽ thấy: step 2: 2.
Bước 3: nhân đôi giá.
// TODO 3: nhân đôi mọi phần tử của prices bằng vòng for theo chỉ số
System.out.println("step 3: " + prices[0]);
Chạy xong bạn sẽ thấy: step 3: 90000. Gợi ý: for-each chỉ đổi bản sao, nên phải dùng chỉ số.
Bước 4: bán gói hàng cho tới khi kho không đủ.
int stock = 10;
int packs = 0;
// TODO 4: dùng while, mỗi vòng bán một gói 4 sản phẩm khi stock còn từ 4 trở lên
System.out.println("step 4: " + packs + " packs, " + stock + " left");
Chạy xong bạn sẽ thấy: step 4: 2 packs, 2 left. Gợi ý: mỗi vòng trừ 4 khỏi stock và tăng packs.
Bước 5: tìm ô trống đầu tiên. Bạn tự quyết cách viết.
int[] shelf = {5, 8, 0, 3, 0};
int firstEmpty = -1;
// TODO 5: tìm chỉ số của ô bằng 0 đầu tiên rồi dừng ngay khi tìm thấy
System.out.println("step 5: " + firstEmpty);
Chạy xong bạn sẽ thấy: step 5: 2.
Vẽ ra giấy: vẽ năm ô của kệ và đánh dấu những ô máy thật sự đọc. Đối chiếu: máy chỉ đọc ô 0, 1, 2 rồi dừng, không đọc ô 3 và ô 4. Lời giải đầy đủ nằm ở cuối bài.
Bạn vừa làm được: dựng một chương trình dùng đủ for, for-each, while và break, và tự dự đoán số vòng chạy trước khi chạy. Toàn bộ mã nguồn của bài cũng nằm trong thư mục 02-kien-thuc-nen-tang/05-vong-lap của kho GitHub, nhưng bạn không cần mở nó để học.
Thử phá nó đi (Lỗi thường gặp)
Bốn lỗi dưới đây là những lỗi người mới gặp nhiều nhất với vòng lặp. Mỗi dòng đã được chạy thật bằng java Ten.java [Đã chạy, JDK 17.0.15; thông báo có thể khác chữ ở phiên bản Java khác].
| Code lỗi | Thông báo lỗi | Nguyên nhân bằng lời thường | Cách sửa |
|---|---|---|---|
for (int i = 0; i <= prices.length; i++) | ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3, sau khi đã cộng đủ 95500 | Chỉ số cuối hợp lệ là 2, mà <= cho thêm vòng với i bằng 3 | Dùng i < prices.length |
for (int i = 0; i < 3; i++); rồi khối { runs++; } | Không báo lỗi, runs bằng 1 | Dấu chấm phẩy thừa làm thân rỗng, và khối bên dưới chỉ chạy một lần sau vòng | Bỏ dấu chấm phẩy sau tiêu đề for |
for (int price : prices) { price = price * 2; } | Không báo lỗi, prices[0] vẫn 45000 | price chỉ là bản sao của phần tử | Dùng vòng for theo chỉ số: prices[i] = prices[i] * 2; |
do { stock -= 4; } while (stock >= 4); với stock bằng 3 | Không báo lỗi, stock bằng -1 | Thân của do-while chạy trước khi kiểm tra điều kiện | Dùng while (stock >= 4) |
Lỗi đầu tiên là lỗi của tình huống mở đầu. Ba lỗi còn lại âm thầm hơn vì không có thông báo nào. Hình dưới đây gom hiểu lầm lệch một đơn vị lại một chỗ.
Cách đọc hình: hàng trên màu đỏ là mô hình sai: nhiều người tưởng độ dài là 3 nên chỉ số 3 là phần tử cuối, và vòng cộng đủ ba món rồi dừng. Hàng dưới màu lục là mô hình đúng: chỉ số chạy từ 0 đến 2, nên chỉ số 3 không tồn tại và vòng thứ tư làm chương trình sập. Dòng cuối của hình nêu cách sửa.
Bạn vừa làm được: đọc bốn lỗi phổ biến của vòng lặp và nói được nguyên nhân bằng lời thường.
Tự tay làm (Bài tập)
Làm lần lượt từng mức. Mỗi bài có đề, dữ liệu vào, kết quả mong đợi, tiêu chí nghiệm thu và gợi ý. Lời giải nằm riêng ở cuối bài, hãy thử trước rồi mới so.
Mức Cơ bản
Bài 1. Vẽ bảng trace. Dữ liệu vào là đoạn code sau, với total bắt đầu bằng 0:
int total = 0;
for (int i = 3; i > 0; i--) {
total += i;
}
Điền bảng gồm các cột: lần kiểm tra, i, điều kiện i > 0, total sau vòng. Kết quả mong đợi: bảng bốn dòng, và total cuối cùng bằng 6. Tiêu chí nghiệm thu: khớp với kết quả chạy thật total = 6, và bạn chỉ ra được điều kiện bị kiểm tra bốn lần còn thân chạy ba lần. Gợi ý: i-- giảm i đi 1 sau mỗi vòng.
Bài 2. Đếm lùi theo bước ba. Dữ liệu vào: không có. Viết một vòng for in 10 7 4 1 trên cùng một dòng. Kết quả mong đợi: 10 7 4 1. Tiêu chí nghiệm thu: in đúng bốn số, và bạn viết được vòng thứ hai làm cùng việc bằng while. Gợi ý: phần cập nhật có thể là i -= 3.
Bài 3. Sửa lỗi lệch một đơn vị. Dữ liệu vào: vòng for (int i = 0; i <= prices.length; i++) cộng mảng ba giá 45_000, 18_500, 32_000. Sửa để không còn ngoại lệ. Kết quả mong đợi: sum = 95500. Tiêu chí nghiệm thu: không ngoại lệ, tổng đúng, và bạn nói được chỉ số hợp lệ cuối cùng là bao nhiêu. Gợi ý: nhìn câu hỏi ở tình huống mở đầu.
Mức Vận dụng
Bài 4. Báo cáo kệ hàng. Viết chương trình StockReport cho kệ {5, 8, 0, 3, 0, 12}: đếm số ô còn hàng và tổng số đơn vị (bỏ ô trống bằng continue), rồi tìm chỉ số ô trống đầu tiên (dừng bằng break). Kết quả mong đợi:
non-empty slots = 4
total units = 28
first empty slot = 2
Tiêu chí nghiệm thu: ba dòng khớp từng ký tự, có đúng một continue và một break, và bạn giải thích được vì sao cần hai vòng riêng. Gợi ý: break ở vòng tìm sẽ làm vòng đếm dừng sớm nếu gộp chung.
Mức Thử thách (tuỳ chọn)
Bài 5. Bảng nhân ba nhân ba. Dùng hai vòng for lồng nhau in bảng nhân từ 1 đến 3. Kết quả mong đợi:
1 2 3
2 4 6
3 6 9
Tiêu chí nghiệm thu: ba dòng đúng, mỗi dòng ba số cách nhau bằng dấu cách, và bạn đếm được thân vòng trong chạy tổng cộng 9 lần. Gợi ý: vòng ngoài là hàng, vòng trong là cột, và println() rỗng đặt sau vòng trong.
Tự kiểm tra (Retrieval Practice)
Trả lời bằng trí nhớ, không xem lại bài. Mỗi câu ghi rõ nó kiểm tra mục tiêu nào trong bốn mục tiêu ở đầu bài.
Câu 1 (nhớ lại, mục tiêu 2). Nêu bốn phần của vòng for theo thứ tự máy chạy chúng. Nếu thân chạy 3 lần thì điều kiện được kiểm tra mấy lần?
Câu 2 (dự đoán kết quả code, mục tiêu 1). Đoạn sau in ra gì?
int s = 0;
for (int i = 1; i <= 4; i++) {
s += i * i;
}
System.out.println(s);
Câu 3 (dự đoán trạng thái sau nhiều bước, mục tiêu 3). Chạy int n = 20; int count = 0; while (n > 1) { n = n / 2; count++; }. Viết n và count sau từng vòng, và giá trị cuối cùng của chúng.
Câu 4 (tìm lỗi, mục tiêu 4). Đoạn for (int i = 0; i < 3; i++); { System.out.println("hi"); } in ra mấy dòng hi? Chỉ ra lỗi và sửa.
Câu 5 (giải thích cho bạn chưa học, mục tiêu 3). Bạn của bạn hỏi: "Khi nào dùng do-while thay vì while?" Hãy giải thích bằng ví dụ người soát vé và kho hàng, rồi nói một rủi ro của do-while.
Điều cần nhớ
- Vòng
forchạy khởi tạo một lần, rồi lặp: kiểm tra, thân, cập nhật; thân chạy N lần thì điều kiện được kiểm tra N cộng 1 lần (mục tiêu 1 và 2). whilekiểm tra trước nên có thể chạy 0 lần,do-whilechạy thân trước nên luôn chạy ít nhất một lần, và for-each chỉ đưa bản sao của phần tử (mục tiêu 3).- Khi duyệt mảng dùng
i < length, chỉ số hợp lệ cuối làlength - 1, và không đặt dấu chấm phẩy sau tiêu đềfor(mục tiêu 4). breakthoát cả vòng,continuechỉ bỏ một vòng (mục tiêu 4).
Cheat sheet một trang.
| Việc cần làm | Cú pháp | Ghi chú |
|---|---|---|
| Lặp theo chỉ số | for (int i = 0; i < n; i++) | Dùng < với độ dài |
| Đọc từng phần tử | for (int x : arr) | x chỉ là bản sao |
| Lặp khi chưa biết số vòng | while (cond) { ... } | Có thể chạy 0 lần |
| Lặp ít nhất một lần | do { ... } while (cond); | Có dấu chấm phẩy cuối |
| Đếm lùi theo bước | for (int i = 10; i >= 1; i -= 3) | Cập nhật là câu lệnh bất kỳ |
| Thoát cả vòng | break; | Chỉ vòng gần nhất |
| Bỏ một vòng | continue; | Sang vòng kế tiếp |
| Sửa phần tử mảng | arr[i] = arr[i] * 2; | Cần chỉ số |
Cách đọc hình: mỗi hàng là một việc cần làm bên trái và công cụ nên chọn bên phải. Đọc theo hàng ngang: đếm hoặc đi theo chỉ số thì for, đọc mọi phần tử thì for-each, chưa biết số vòng thì while, và phải chạy ít nhất một lần thì do-while.
Câu hỏi để dành cho ngày mai: với 50 sản phẩm, bạn muốn gom các hàm như "cộng giá", "đếm món đắt" thành những khối có tên để gọi lại nhiều lần mà không chép code. Java có cách nào để đặt tên cho một đoạn code rồi gọi nó? Hãy nghĩ trước khi sang chương sau.
Đào sâu
Phần này dành cho bạn tò mò. Bỏ qua cũng không thiếu gì ở trên.
Bên trong break có nhãn. break thường chỉ thoát vòng gần nhất. Khi có hai vòng lồng nhau và bạn muốn thoát cả hai, đặt một nhãn trước vòng ngoài rồi viết break search;. Chương trình Deeper tìm số 4 đầu tiên trong một lưới 3 nhân 3 [Đã chạy, JDK 17.0.15]:
plain break: (2,1)
labeled break: (1,0)
float counter: 14 runs, d = 1.4000000000000001
Với break thường, vòng ngoài vẫn chạy tiếp và ghi đè kết quả bằng số 4 thứ hai ở (2,1). Với break search, cả hai vòng dừng ngay ở số 4 đầu tiên, (1,0). Nhãn hiếm khi cần, và thường rõ hơn khi tách đoạn tìm kiếm ra một phương thức (chương sau).
So với lựa chọn khác. Có thể viết mọi vòng for bằng while: khởi tạo trước vòng, cập nhật cuối thân. Nhưng for gom cả ba phần vào một dòng nên dễ thấy toàn bộ cơ chế đếm, đó là lý do người ta chuộng nó khi biết trước số vòng.
Hiểu lầm nâng cao. Bộ đếm là số thực rất dễ làm vòng không dừng. Dòng cuối của kết quả trên chạy while (d != 1.0 && runs < 14) với d += 0.1: sau mười lần cộng d là 0.9999999999999999, không bao giờ đúng bằng 1.0, nên vòng chỉ dừng nhờ giới hạn 14 vòng [Đã chạy, JDK 17.0.15]. Dùng bộ đếm là số nguyên, như bài biến và kiểu dữ liệu đã cảnh báo.
Giới hạn của bài. Bài này chưa nói về duyệt danh sách và tập hợp (chương Collections), về xoá phần tử khi đang lặp, về Iterator, và về vòng lặp vô hạn có chủ đích. Số liệu thuộc JDK 17.0.15 trên máy người soạn bài.
Đọc thêm (tài liệu chính thống, Java SE 17):
- The Java Tutorials: The for Statement
- The Java Tutorials: The while and do-while Statements
- The Java Tutorials: Branching Statements
Bài tiếp theo
Bài sau thuộc chương Phương thức: cách đặt tên cho một đoạn code, truyền dữ liệu vào và nhận kết quả về, để gọi lại nhiều lần mà không chép code. Chương Kiến thức nền tảng tới đây là đủ để bạn viết những chương trình đầu tiên.
Mini-dự án nối tiếp: ở chương sau, quầy thu ngân sẽ tách "cộng giá giỏ hàng" và "xếp hạng thành viên" thành hai phương thức riêng. Hãy giữ lại file CartDesk.java và TierDesk.java bạn vừa viết làm điểm xuất phát.
Lời giải và đáp án
Hãy thử xong rồi mới đọc phần này.
Lời giải Làm cùng mình. Thay các dòng // TODO bằng những dòng dưới đây, ghép lại chạy ra đúng năm dòng step ở trên [Đã chạy, JDK 17.0.15]:
for (int i = 0; i < prices.length; i++) {
sum += prices[i];
}
for (int price : prices) {
if (price >= 30_000) {
bigOnes++;
}
}
for (int i = 0; i < prices.length; i++) {
prices[i] = prices[i] * 2; // đổi phần tử bằng chỉ số
}
while (stock >= 4) {
stock -= 4;
packs++;
}
for (int i = 0; i < shelf.length; i++) {
if (shelf[i] == 0) {
firstEmpty = i;
break; // dừng ở ô trống đầu tiên
}
}
| Dòng code | Máy làm gì | Lý do chọn cách này |
|---|---|---|
for (int i = 0; i < prices.length; i++) | Đi qua chỉ số 0, 1, 2 và cộng giá vào sum | Dùng < để không đọc ngoài mảng |
for (int price : prices) rồi if | Duyệt từng giá, tăng bigOnes khi từ 30000 | Chỉ đọc nên for-each gọn và không lệch |
prices[i] = prices[i] * 2; | Ghi giá gấp đôi vào đúng ô | Phải có chỉ số mới sửa được phần tử |
while (stock >= 4) | Bán gói khi kho còn đủ, dừng ở 2 | Kiểm tra trước nên không bao giờ bán quá kho |
if (shelf[i] == 0) { ...; break; } | Ghi chỉ số 2 rồi thoát vòng | Tìm thấy rồi thì không cần đọc các ô sau |
Lời giải Bài 1. [Suy luận, khớp với kết quả chạy thật total = 6]
| Lần kiểm tra | i | Điều kiện i > 0 | total sau vòng |
|---|---|---|---|
| 1 | 3 | đúng | 3 |
| 2 | 2 | đúng | 5 |
| 3 | 1 | đúng | 6 |
| 4 | 0 | sai | không có vòng |
Điều kiện được kiểm tra 4 lần, thân chạy 3 lần, và total cuối là 6.
Lời giải Bài 2. [Đã chạy, in 10 7 4 1]
for (int i = 10; i >= 1; i -= 3) {
System.out.print(i + " ");
}
Với while: int i = 10; while (i >= 1) { System.out.print(i + " "); i -= 3; }. Cả hai làm cùng việc, và for gom khởi tạo, điều kiện, cập nhật vào một dòng.
Lời giải Bài 3. [Suy luận] Đổi <= thành <. Mảng có ba phần tử nên chỉ số hợp lệ là 0, 1, 2, tức length - 1 là 2. Khi đó vòng chạy ba lần và tổng là 95500 mà không có ngoại lệ.
Lời giải Bài 4. [Đã chạy, ba dòng khớp với kết quả mong đợi]
for (int s : stock) {
if (s == 0) {
continue; // ô trống không tính
}
filled++;
units += s;
}
for (int i = 0; i < stock.length; i++) {
if (stock[i] == 0) {
firstEmpty = i;
break; // thấy ô trống đầu tiên thì dừng
}
}
| Dòng code | Máy làm gì | Lý do chọn cách này |
|---|---|---|
continue; ở vòng đếm | Bỏ qua ô trống rồi sang ô kế | Chỉ bỏ một vòng, các ô sau vẫn cần đếm |
filled++; units += s; | Đếm ô và cộng số lượng, ra 4 và 28 | Chỉ chạy cho ô còn hàng nhờ continue |
break; ở vòng tìm | Dừng sau khi ghi chỉ số 2 | Chỉ cần ô trống đầu tiên |
Hai vòng phải riêng vì break ở vòng tìm sẽ dừng việc đếm sớm, còn continue ở vòng đếm không dừng được việc tìm.
Gợi ý lời giải Bài 5. [Đã chạy] Hai vòng lồng nhau:
for (int row = 1; row <= 3; row++) {
for (int col = 1; col <= 3; col++) {
System.out.print(row * col + " ");
}
System.out.println(); // xuống dòng sau mỗi hàng
}
Vòng trong chạy trọn 3 lần cho mỗi vòng ngoài, nên thân vòng trong chạy 9 lần. println() nằm sau vòng trong, trong vòng ngoài, nên mỗi hàng xuống dòng đúng một lần.
Đáp án Tự kiểm tra.
- Bốn phần theo thứ tự: khởi tạo (một lần), kiểm tra điều kiện, thân, cập nhật, rồi quay lại kiểm tra. Thân chạy 3 lần thì điều kiện được kiểm tra 4 lần. Kiểm tra mục tiêu 2.
- In ra
30[Đã chạy]: 1 + 4 + 9 + 16. Kiểm tra mục tiêu 1. - Sau vòng 1:
nbằng 10,countbằng 1. Sau vòng 2: 5 và 2. Sau vòng 3: 2 và 3. Sau vòng 4: 1 và 4. Cuối cùngnbằng 1 vàcountbằng 4 [Đã chạy]. Vòng dừng vì1 > 1sai. Kiểm tra mục tiêu 3. - In ra một dòng
hi[Đã chạy]. Dấu chấm phẩy sau tiêu đềforlàm thân rỗng, nên vòng chạy ba lần không làm gì, và khối{ ... }bên dưới chỉ là một khối thường chạy một lần. Sửa: bỏ dấu chấm phẩy. Kiểm tra mục tiêu 4. - Dùng do-while khi thân phải chạy ít nhất một lần, như người soát vé luôn phải nhìn ghế đầu tiên trước khi hỏi còn ghế không. Rủi ro: với kho hàng, thân bán gói chạy trước khi kiểm tra, nên kho 3 bị bán một gói thành -1. Kiểm tra mục tiêu 3.