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 for cộ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-while hay for-each cho 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

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

text
while: 0 packs, stock 3
do-while: 1 pack, stock -1
Dòng codeMá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ânKiểm tra trước, nên an toàn khi kho không đủ
stock -= 4; trong doTrừ 4 khỏi kho, kho thành -1Thâ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ừngCó 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ánCho 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:

java
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ố
}
text
sum = 95500, prices[0] = 45000
after index loop, prices[0] = 90000
Dòng codeMá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 priceChỉ đọ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 priceChỉ đổi bản sao, nên in ra prices[0] vẫn 45000
i < prices.lengthDừng khi i bằng độ dàiChỉ 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

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

text
first empty at 2, filled slots 3
Dòng codeMá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étGiá trịViệc xảy rafirstEmpty hoặc filled
Vòng tìm05Khác 0, đi tiếpfirstEmpty vẫn -1
Vòng tìm18Khác 0, đi tiếpfirstEmpty vẫn -1
Vòng tìm20Ghi chỉ số rồi breakfirstEmpty bằng 2
Vòng đếm0 và 15 và 8Đếmfilled bằng 2
Vòng đếm20continue, không đếmfilled vẫn 2
Vòng đếm3 và 43 và 0Đếm ô 3, continue ô 4filled 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

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

text
i=0 total=45000
i=1 total=63500
i=2 total=95500
sum = 95500

Đọc code từng dòng:

Dòng codeMá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, 2Mả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ầnPhần khởi tạo chỉ chạy một lần trước mọi vòng
i < prices.lengthKiểm tra trước mỗi vòng, đúng thì vào thânlength 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ânPhầ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 totalMỗ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ướcLần kiểm traiĐiều kiệnThân chạytotal
Khởi tạochưa có0chưa cókhông0
Vòng 1100 < 3 đúngcó45000
Vòng 2211 < 3 đúngcó63500
Vòng 3322 < 3 đúngcó95500
Kết thúc433 < 3 saikhông95500

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 >= 4 sai 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ì stock luô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ì p là chính phần tử của mảng nên đổi p là đổ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ì p là 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ỏ.

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

java
        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á.

java
        // 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 đủ.

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

java
        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ỗiThông báo lỗiNguyên nhân bằng lời thườngCách sửa
for (int i = 0; i <= prices.length; i++)ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3, sau khi đã cộng đủ 95500Chỉ số cuối hợp lệ là 2, mà <= cho thêm vòng với i bằng 3Dù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 1Dấ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òngBỏ 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 45000price 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 3Không báo lỗi, stock bằng -1Thân của do-while chạy trước khi kiểm tra điều kiệnDù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:

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

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

text
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ì?

java
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 for chạ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).
  • while kiểm tra trước nên có thể chạy 0 lần, do-while chạ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).
  • break thoát cả vòng, continue chỉ bỏ một vòng (mục tiêu 4).

Cheat sheet một trang.

Việc cần làmCú phápGhi 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òngwhile (cond) { ... }Có thể chạy 0 lần
Lặp ít nhất một lầndo { ... } while (cond);Có dấu chấm phẩy cuối
Đếm lùi theo bướcfor (int i = 10; i >= 1; i -= 3)Cập nhật là câu lệnh bất kỳ
Thoát cả vòngbreak;Chỉ vòng gần nhất
Bỏ một vòngcontinue;Sang vòng kế tiếp
Sửa phần tử mảngarr[i] = arr[i] * 2;Cần chỉ số
Hình: tóm tắt vòng lặp nào cho việc nào ở quầy thu ngân

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

text
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):

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

java
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 codeMá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 sumDùng < để không đọc ngoài mảng
for (int price : prices) rồi ifDuyệt từng giá, tăng bigOnes khi từ 30000Chỉ đọ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 ở 2Kiể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òngTì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 traiĐiều kiện i > 0total sau vòng
13đúng3
22đúng5
31đúng6
40saikhô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]

java
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]

java
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 codeMáy làm gìLý do chọn cách này
continue; ở vòng đếmBỏ 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à 28Chỉ chạy cho ô còn hàng nhờ continue
break; ở vòng tìmDừng sau khi ghi chỉ số 2Chỉ 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:

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

  1. 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.
  2. In ra 30 [Đã chạy]: 1 + 4 + 9 + 16. Kiểm tra mục tiêu 1.
  3. Sau vòng 1: n bằng 10, count bằ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ùng n bằng 1 và count bằng 4 [Đã chạy]. Vòng dừng vì 1 > 1 sai. Kiểm tra mục tiêu 3.
  4. In ra một dòng hi [Đã chạy]. Dấu chấm phẩy sau tiêu đề for là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.
  5. 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.