Câu lệnh điều kiện

Xem thử

Tình huống mở đầu

Tạp Hóa Cô Tấm chia thành viên thành ba hạng theo điểm tích luỹ: từ 80 điểm là GOLD, từ 65 là SILVER, từ 50 là BRONZE. Bạn viết chương trình xếp hạng và thử với một khách có 95 điểm. Mong đợi: GOLD. Thực tế màn hình in BRONZE. Chạy cho 40 khách có từ 50 điểm trở lên thì cả 40 người, kể cả người cao điểm nhất, đều nhận quà hạng đồng.

Chương trình không báo lỗi, từng điều kiện bạn viết đều đúng. Vậy máy đã đi qua các điều kiện theo cách nào mà dừng sớm như vậy? Giữ câu hỏi này trong đầu, cuối bài bạn sẽ tự sửa nó.

Ôn nhanh bài trước: lệnh nào nhìn trước token kế tiếp mà không lấy nó ra khỏi bộ đệm: next() hay hasNextInt()?

Đáp án: hasNextInt(). Nó chỉ nhìn và trả về true hoặc false, còn next() lấy token ra khỏi bộ đệm.

Mục tiêu bài học

Sau bài này, bạn có thể:

  • Viết đúng chuỗi if, else if, else cho ba hạng thành viên và mô tả được máy thử các điều kiện theo thứ tự nào, dừng ở đâu.
  • Nhận ra hai lỗi điều kiện hay gặp: else đi nhầm if, và dấu = đơn viết thay cho ==, rồi sửa được.
  • Giải thích được vì sao switch cổ điển chạy xuyên các nhãn khi thiếu break, theo từng bước máy làm.
  • Viết được switch biểu thức với -> và yield để chọn phí giao hàng, và nêu được một lỗi biên dịch nó bắt thay bạn.

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 hai cách chương trình chọn một con đường.

Cách đọc hình: ô trên cùng là một quyết định cần đưa ra. Nhánh bên trái là chuỗi if, else if, else: máy thử các điều kiện từ trên xuống và nhánh đúng đầu tiên thắng, nên bẫy là thứ tự điều kiện. Nhánh bên phải là switch: máy chọn nhánh theo giá trị của một biến, và có hai dạng, dạng cổ điển với dấu hai chấm và dạng biểu thức với mũi tê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: cổng soát vé ở rạp. Mỗi cổng có một điều kiện, người soát đi từ cổng đầu và cho khách qua cổng đầu tiên mà khách thoả điều kiện. Dự án xuyên suốt là hạng thành viên và phí giao hàng của Tạp Hóa Cô Tấm. 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: câu lệnh if, else và nhánh gần nhất

if chạy một khối lệnh khi điều kiện (một biểu thức boolean) đúng. else là nhánh cho trường hợp điều kiện sai. Khối lệnh được bao bằng ngoặc nhọn { }. Ở cổng soát vé, if là một cổng và else là lối đi riêng cho người không qua được cổng đó.

Hãy xem một chương trình ngắn với else dễ gây nhầm:

Dangling.java

java
public class Dangling {
    public static void main(String[] args) {
        boolean member = true;
        boolean bigOrder = false;
        if (member)
            if (bigOrder)
                System.out.println("big member order");
        else // cách thụt dòng gợi ý thuộc if (member), nhưng thật ra thuộc if (bigOrder)
            System.out.println("who gets this else?");
    }
}

Kết quả [Đã chạy, JDK 17.0.15]:

text
who gets this else?

Cách đọc hình: hàng trên là ba mảnh của code: if (member) bao ngoài, if (bigOrder) đứng sát ngay trên, và else. Hàng dưới màu đỏ là điều cách thụt dòng gợi ý: else thuộc if (member). Ô màu lục là điều Java làm: else đi với if (bigOrder) vì đó là if gần nhất. Với member đúng và bigOrder sai, nhánh else chạy.

Dòng codeMáy làm gìVì sao viết như vậy
if (member) rồi if (bigOrder)Mở hai if lồng nhau, không có ngoặc nhọnĐây là cố ý, để thấy hậu quả của việc thiếu ngoặc
elseNối vào if (bigOrder) gần nhấtQuy tắc của Java: else luôn đi với if gần nhất chưa có else
System.out.println("who gets this else?")Chạy vì member đúng và bigOrder saiNgười đọc tưởng else thuộc member nên sẽ không chạy, nhưng thật ra có chạy

Cách phòng tránh: luôn viết ngoặc nhọn cho thân if, kể cả khi chỉ có một dòng. Khi đó không còn chuyện else nhập nhằng.

Khái niệm 2: chuỗi else if dừng ở nhánh đúng đầu tiên

Khi có nhiều điều kiện, bạn xâu chúng thành chuỗi if, else if, else. Máy thử các điều kiện từ trên xuống, nhánh có điều kiện đúng đầu tiên chạy, và mọi nhánh còn lại bị bỏ qua. Vì vậy thứ tự các điều kiện là một phần của logic: phải xếp từ chặt nhất đến lỏng nhất.

Cách đọc hình: hàng trên xếp từ chặt đến lỏng với 79 điểm: điều kiện lớn hơn hoặc bằng 80 sai nên thử tiếp, lớn hơn hoặc bằng 65 đúng nên chạy và dừng, kết quả SILVER. Hàng dưới màu đỏ xếp từ lỏng đến chặt: lớn hơn hoặc bằng 50 đúng ngay nên chạy và dừng, kết quả BRONZE, hai điều kiện chặt hơn không bao giờ được thử.

Đâ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. Một điều kiện lỏng đứng đầu giống như cổng soát vé đầu tiên mở cho tất cả mọi người: các cổng sau không còn ai đến.

Chỗ hình ảnh cổng soát vé không còn đúng: ở rạp thật, người soát vé có thể nhìn khách rồi chọn cổng tốt nhất. Máy Java không nhìn: nó chỉ thử cổng theo thứ tự bạn viết và dùng cổng đầu tiên thoả, dù phía dưới có cổng tốt hơn.

Concept Check 1. Chuỗi if (points >= 50) { tier = "BRONZE"; } else if (points >= 65) { tier = "SILVER"; } else if (points >= 80) { tier = "GOLD"; }. Với points bằng 79, tier là gì? Hãy chọn đáp án đúng trước khi đọc tiếp.

  • A. GOLD, vì Java thử mọi điều kiện rồi chọn hạng cao nhất mà khách đủ điểm.
  • B. SILVER, vì 79 gần với mốc 65 hơn mốc 50 nên Java chọn hạng đó.
  • C. BRONZE, vì điều kiện đầu tiên đã đúng nên chuỗi dừng ở đó.
  • D. NONE, vì có hai điều kiện sau mâu thuẫn với điều kiện đầu.

Đáp án: C. Điều kiện lớn hơn hoặc bằng 50 đứng đầu và đúng với 79, nên nhánh BRONZE chạy và các nhánh sau bị bỏ qua. A là hiểu lầm hay gặp nhất: Java không so sánh để chọn kết quả tốt nhất. B sai vì Java không có khái niệm gần hơn, chỉ có đúng hay sai. D sai vì không có điều kiện nào mâu thuẫn, chúng chỉ không bao giờ được thử. [Đã chạy, JDK 17.0.15, xem phần Ví dụ và Thử phá nó đi.]

Khái niệm 3: switch cổ điển và hiện tượng chạy xuyên nhãn

switch chọn một nhánh theo giá trị của một biến (kiểu int, char, String, enum). Ở dạng cổ điển với dấu hai chấm, nhãn case chỉ là điểm vào: máy nhảy tới nhãn khớp rồi chạy mọi dòng bên dưới cho tới khi gặp break hoặc hết switch. Hiện tượng đó gọi là chạy xuyên (fall through).

Cách đọc hình: hàng trên đi từ trái sang phải. Biến zone bằng 1, nên máy nhảy vào case 1 và in phí 20000. Hai mũi tên nét đứt cho thấy máy chạy tiếp vào case 2 và default mà không kiểm tra nhãn nữa, mỗi chỗ in thêm một dòng. Ô màu lục ở hàng dưới cho thấy cách sửa: một break ngay sau case 1 để máy dừng.

Đoạn code dưới đây chạy ra ba dòng cho một đơn:

Fallthrough.java

java
public class Fallthrough {
    public static void main(String[] args) {
        int zone = 1;
        switch (zone) {
            case 1:
                System.out.println("fee: 20000 (Pho Rom Ra)"); // không có break nên chạy tiếp xuống nhãn dưới
            case 2:
                System.out.println("fee: 30000 (Xom Vui)");
            default:
                System.out.println("fee: 35000 (other)");
        }
    }
}

Kết quả [Đã chạy, JDK 17.0.15]:

text
fee: 20000 (Pho Rom Ra)
fee: 30000 (Xom Vui)
fee: 35000 (other)
Dòng codeMáy làm gìVì sao viết như vậy
switch (zone) {So zone bằng 1 với các nhãn, tìm điểm vàoswitch so khớp theo giá trị của biến
case 1: rồi dòng in đầuNhảy vào nhãn này và in phí 20000Nhãn chỉ là điểm vào, không phải một ngăn kín
case 2: rồi dòng in haiChạy tiếp vì phía trên không có breakMáy không kiểm tra lại nhãn khi đã chạy xuyên
default: rồi dòng in baChạy tiếp nốt và hết switchQuên break cho kết quả thừa mà không báo lỗi

Hãy xem từng bước máy thực hiện:

Đang tải Animation…

Bản chữ tương đương của animation:

BướcĐiều xảy raDòng đã in
1Máy so zone bằng 1 với các nhãn0
2Khớp case 1, nhảy vào làm điểm vào0
3In phí 20000, không có break1
4Chạy xuyên vào case 2, in 300002
5Chạy xuyên vào default, in 350003
6Cách sửa: break sau dòng của case 11

Khái niệm 4: switch biểu thức, một nhánh và có giá trị

Từ Java 14, switch có dạng biểu thức với mũi tên -> và trả về một giá trị. Mỗi nhãn chỉ chạy đúng nhánh của nó, không cần break. Một nhánh có thể gộp nhiều nhãn bằng dấu phẩy. Khi nhánh cần nhiều dòng thì dùng khối { } và kết thúc bằng yield để trả giá trị. Biểu thức bắt buộc bao phủ mọi đầu vào: thiếu nhánh là lỗi biên dịch.

Cách đọc hình: hàng trên là ba mảnh của một switch biểu thức: dòng gán, nhánh gộp hai nhãn bằng dấu phẩy, và nhánh default có khối dùng yield. Hàng dưới là ba bảo đảm: không cần break, thiếu đầu vào thì trình biên dịch dừng lại, và một switch chỉ dùng một kiểu, không trộn mũi tên với dấu hai chấm.

Chương trình ZoneFee chọn phí giao hàng theo vùng:

ZoneFee.java

java
public class ZoneFee {
    public static void main(String[] args) {
        int zone = 4;
        int fee = switch (zone) {
            case 1 -> 20_000;
            case 2, 3 -> 30_000; // gộp nhiều nhãn bằng dấu phẩy
            default -> {
                int base = 35_000;
                yield base + 5_000; // khối nhiều lệnh dùng yield để trả giá trị
            }
        };
        System.out.println("zone " + zone + " fee " + fee);
    }
}

Kết quả [Đã chạy, JDK 17.0.15]:

text
zone 4 fee 40000
Dòng codeMáy làm gìVì sao viết như vậy
int fee = switch (zone) {Chọn nhánh theo zone rồi cất giá trị nhánh vào feeswitch ở dạng biểu thức nên có giá trị và gán được
case 1 -> 20_000;Nếu zone bằng 1 thì giá trị là 20000Mũi tên chạy đúng nhánh này, không chạy xuyên
case 2, 3 -> 30_000;Gộp hai vùng chung một phíDấu phẩy thay cho hai nhãn trống liền nhau của dạng cũ
default -> { ... yield base + 5_000; }Chạy khối rồi trả base + 5000 ra làm giá trịKhối nhiều dòng phải kết thúc bằng yield

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: xếp hạng thành viên đúng thứ tự.

Tier.java

java
public class Tier {
    public static void main(String[] args) {
        int points = 79;
        String tier;
        if (points >= 80) { // điều kiện chặt nhất đứng đầu
            tier = "GOLD";
        } else if (points >= 65) { // chỉ được thử khi điều kiện trên sai
            tier = "SILVER";
        } else if (points >= 50) {
            tier = "BRONZE";
        } else { // nhận mọi trường hợp còn lại
            tier = "NONE";
        }
        System.out.println(points + " points: " + tier);
    }
}

Predict: hãy dừng lại, lấy giấy nháp và đoán: với 79 điểm chương trình in ra hạng nào, và điều kiện nào không bao giờ được thử? Đoán xong rồi hãy đọc tiếp.

Kết quả mong đợi [Đã chạy, JDK 17.0.15]:

text
79 points: SILVER

Đọc code từng dòng:

Dòng codeMáy làm gìVì sao viết như vậy
String tier;Làm hộp tier, chưa cất gìGán sau trong các nhánh nên chưa cần giá trị đầu
if (points >= 80)Thử điều kiện chặt nhất trướcChặt nhất đứng đầu để không bị điều kiện lỏng chặn
else if (points >= 65)Chỉ thử khi điều kiện trên sai, 79 thoả nên chạyelse if chỉ được xét khi mọi nhánh phía trên đều sai
else if (points >= 50)Không được thử với 79Nhánh đúng đầu tiên đã chạy, các nhánh sau bị bỏ qua
elseNhận mọi trường hợp còn lạiBảo đảm tier luôn có giá trị, kể cả khi dưới 50 điểm

Còn đây là đoạn sai, chỉ khác thứ tự điều kiện [Đã chạy, JDK 17.0.15]:

java
if (points >= 50) { // điều kiện lỏng nhất đứng đầu nên chặn hết các nhánh sau
    tier = "BRONZE";
} else if (points >= 65) {
    tier = "SILVER";
}
text
95 points: BRONZE

Bây giờ xem từng bước. Mỗi bước nêu điều kiện đang thử và kết quả của nó:

Đang tải Animation…

Bảng trace (bản chữ tương đương của animation, [Suy luận] bằng cách đi từng dòng và khớp với kết quả chạy thật):

ĐiểmĐiều kiện được thử theo thứ tựNhánh chạytier
79, đúng thứ tự>= 80 sai, >= 65 đúngnhánh thứ haiSILVER
95, đúng thứ tự>= 80 đúngnhánh thứ nhấtGOLD
95, đảo thứ tự>= 50 đúngnhánh thứ nhấtBRONZE

Bạn vừa làm được: đọc một chuỗi else if và nói đúng điều kiện nào được thử, điều kiện nào bị bỏ qua, nên giải thích được vì sao khách 95 điểm nhận hạng đồ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. Dùng đoạn Fallthrough ở trên nhưng đặt zone bằng 2. Chương trình in ra những dòng nào?

  • A. Một dòng fee: 30000 (Xom Vui), vì chỉ nhãn khớp mới chạy.
  • B. Hai dòng: fee: 30000 (Xom Vui) rồi fee: 35000 (other).
  • C. Ba dòng, vì switch luôn chạy từ nhãn đầu tiên xuống.
  • D. Lỗi biên dịch, vì thiếu break ở các nhãn.

Đáp án: B. Máy nhảy vào case 2, in phí 30000, rồi chạy xuyên vào default và in 35000. case 1 nằm phía trên điểm vào nên không chạy. A là mô hình của dạng mũi tên, không phải của dạng cổ điển. C sai vì máy bắt đầu từ nhãn khớp chứ không từ nhãn đầu. D sai vì thiếu break hoàn toàn hợp lệ, chỉ cho kết quả thừa. [Đã chạy, JDK 17.0.15.]

Concept Check 3. Dùng đoạn Dangling ở trên: member bằng true, bigOrder bằng false. Điều gì xảy ra?

  • A. Không in gì, vì else thuộc if (member) mà member là true.
  • B. In big member order, vì cả hai điều kiện đều được xét.
  • C. Lỗi biên dịch, vì hai if lồng nhau bắt buộc phải có ngoặc nhọn.
  • D. In who gets this else?, vì else đi với if (bigOrder) gần nhất mà bigOrder sai.

Đáp án: D. else luôn đi với if gần nhất phía trên. A là điều cách thụt dòng gợi ý, và là lý do lỗi này đánh lừa người đọc. B sai vì bigOrder là false nên dòng đó không chạy. C sai vì không có ngoặc nhọn vẫn biên dịch được, chỉ là dễ nhầm. [Đã chạy, JDK 17.0.15.]

Làm cùng mình (Guided Practice)

Bạn sẽ dựng dần class TierDesk 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 TierDesk.java rồi chạy bằng java TierDesk.java.

Bước 1: giảm giá cho thành viên.

java
public class TierDesk {
    public static void main(String[] args) {
        boolean member = true;
        int discountPercent = 0;
        // TODO 1: nếu member là true thì đặt discountPercent bằng 10, ngược lại bằng 0
        System.out.println("step 1: " + discountPercent);
    }
}

Chạy xong bạn sẽ thấy: step 1: 10. Gợi ý: viết đủ ngoặc nhọn cho cả if và else.

Bước 2: xếp hạng bằng chuỗi else if. Thêm ngay dưới dòng in của bước 1:

java
        int points = 79;
        String tier = "NONE";
        // TODO 2: dùng if, else if, else để đặt tier: từ 80 điểm là GOLD, từ 65 là SILVER, từ 50 là BRONZE
        System.out.println("step 2: " + tier);

Chạy xong bạn sẽ thấy: step 2: SILVER.

Vẽ ra giấy: vẽ ba cổng nối tiếp lớn hơn hoặc bằng 80, lớn hơn hoặc bằng 65, lớn hơn hoặc bằng 50, và đánh dấu đường đi của 79 điểm. Đối chiếu: 79 sai ở cổng đầu, đúng ở cổng hai và dừng, cổng ba không bao giờ được thử.

Bước 3: sửa thứ tự bị sai.

java
        int vip = 95;
        String vipTier;
        if (vip >= 50) {
            vipTier = "BRONZE";
        } else if (vip >= 80) {
            vipTier = "GOLD";
        } else {
            vipTier = "NONE";
        }
        // TODO 3: sắp lại thứ tự các nhánh ở trên để khách 95 điểm ra GOLD
        System.out.println("step 3: " + vipTier);

Chạy xong bạn sẽ thấy: step 3: GOLD (trước khi sửa là step 3: BRONZE). Gợi ý: điều kiện chặt nhất lên đầu.

Bước 4: chặn chạy xuyên.

java
        int zone = 2;
        int fee = 0;
        switch (zone) {
            case 1:
                fee = 20_000;
            case 2:
                fee = 30_000;
            default:
                fee = 35_000;
        }
        // TODO 4: thêm break vào đúng chỗ để zone 2 ra fee bằng 30000
        System.out.println("step 4: " + fee);

Chạy xong bạn sẽ thấy: step 4: 30000 (trước khi sửa là 35000). Vẽ ra giấy: vẽ ba nhãn và đánh dấu đường chạy của zone bằng 2, trước và sau khi thêm break. Đối chiếu: trước khi sửa, máy nhảy vào case 2 rồi trượt xuống default, nên giá trị cuối là 35000.

Bước 5: viết lại bằng switch biểu thức. Bạn tự quyết cách viết.

java
        int other = 9;
        int fee2 = 0;
        // TODO 5: gán lại fee2 bằng một switch biểu thức theo other: vùng 1 là 20000, vùng 2 là 30000, còn lại 35000
        System.out.println("step 5: " + fee2);

Chạy xong bạn sẽ thấy: step 5: 35000. Gợi ý: dùng ->, và nhánh default là bắt buộc với int. 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 đủ if, else if và hai dạng switch, và tự dự đoán 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/04-cau-lenh-dieu-kien 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 điều kiện. 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
if (points >= 50) đứng trước else if (points >= 80) với 79 điểmKhông báo lỗi, kết quả là BRONZEĐiều kiện lỏng đứng đầu nên chặn các nhánh sauXếp điều kiện từ chặt đến lỏng
if (points = 80)error: incompatible types: int cannot be converted to booleanDấu = là phép gán, không phải phép so sánhViết ==, hoặc >= nếu muốn từ 80 trở lên
typed == "yes" với typed tạo bằng new String("yes")Không báo lỗi, kết quả false dù equals cho true== so tờ phiếu, không so nội dung chuỗiDùng typed.equals("yes")
case 1: không có break rồi case 2:Không báo lỗi, đầu ra cho cả A và BNhãn chỉ là điểm vào nên máy chạy xuyênThêm break hoặc dùng dạng ->

Ba trong bốn lỗi không có thông báo nào, và đó là điều làm chúng nguy hiểm. Hình dưới đây gom hiểu lầm lớn nhất 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 Java xem hết mọi điều kiện rồi chọn hạng cao nhất khách đủ điểm, nên đoán GOLD. Hàng dưới màu lục là mô hình đúng: Java dừng ở điều kiện đúng đầu tiên tính từ trên xuống, nên điều kiện lớn hơn hoặc bằng 50 đứng đầu cho BRONZE và không có thông báo nào. 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 điều kiện 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ẽ đường đi. Dữ liệu vào là chuỗi Tier ở phần Ví dụ (giữ points làm biến), thử lần lượt với points bằng 80, 65 và 49. Điền bảng gồm các cột: điểm, các điều kiện được thử theo thứ tự, hạng. Kết quả mong đợi: bảng ba dòng, hạng lần lượt là GOLD, SILVER, NONE. Tiêu chí nghiệm thu: khớp với kết quả chạy thật, và bạn chỉ ra được điều kiện biên của 80 và 65 nằm ở nhánh nào. Gợi ý: dấu >= tính cả điểm đúng bằng mốc.

Bài 2. Gắn ngoặc nhọn cho else. Dữ liệu vào: file Dangling.java ở phần Khái niệm 1. Thêm ngoặc nhọn để else thuộc if (member), rồi chạy hai lần với member bằng true và false. Kết quả mong đợi: với true không in gì, với false in not a member. Tiêu chí nghiệm thu: hai lần chạy đúng như trên, và bạn nói được ngoặc nhọn nào buộc else vào if ngoài. Gợi ý: if (bigOrder) phải nằm trọn trong một cặp ngoặc.

Bài 3. Từ dạng cũ sang dạng mũi tên. Dữ liệu vào: file Fallthrough.java. Viết lại bằng switch biểu thức để zone bằng 1 cho fee là 20000 và in một dòng. Kết quả mong đợi: zone 1 fee 20000. Tiêu chí nghiệm thu: không còn break nào, và đúng một dòng được in. Gợi ý: mẫu của ZoneFee ở phần Khái niệm 4.

Mức Vận dụng

Bài 4. Phí giao hàng của tiệm. Viết chương trình Shipping tính fee theo quy tắc: vùng 1 là 20000, vùng 2 và 3 là 30000, vùng khác là 35000 (dùng switch biểu thức). Nếu cân nặng quá 2 kg thì cộng 5000 cho mỗi kg đã bắt đầu vượt 2 kg. Thành viên được giảm 10000 khi phí vượt 40000. Cân nặng không dương thì in weight must be positive và dừng. Dữ liệu vào: zone bằng 9, weight bằng 6.0, member bằng true. Kết quả mong đợi: fee = 45000. Tiêu chí nghiệm thu: khớp với kết quả mong đợi, và bạn tự thử thêm ba bộ dữ liệu: vùng 1 với 1.5 kg không thành viên cho 20000, vùng 1 với 4.2 kg không thành viên cho 35000, vùng 9 với 6.0 kg không thành viên cho 55000. Gợi ý: số kg vượt là Math.ceil(weight - 2), và kiểm tra cân nặng nên làm trước hết.

Mức Thử thách (tuỳ chọn)

Bài 5. Switch trên chuỗi và null. Viết Command nhận một String command rồi dùng switch biểu thức: "start" ra starting, "stop" ra stopping, giá trị khác ra unknown command kèm chính giá trị đó. Chương trình phải xử lý được command bằng null mà không ném lỗi, in no command. Kết quả mong đợi với null: no command; với "Stop": unknown command Stop. Tiêu chí nghiệm thu: cả hai kết quả đúng, và bạn giải thích được vì sao switch trên chuỗi null ném NullPointerException. Gợi ý: kiểm tra null bằng if trước switch.

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 1). Máy thử các điều kiện của chuỗi else if theo thứ tự nào và dừng khi nào? Vì sao phải xếp từ chặt tới lỏng?

Câu 2 (dự đoán kết quả code, mục tiêu 2). Đoạn sau in ra gì?

java
boolean a = true;
boolean b = false;
if (a)
    if (b)
        System.out.println("one");
else
    System.out.println("two");

Câu 3 (dự đoán trạng thái sau nhiều bước, mục tiêu 3). Với int x = 2;, đoạn switch cổ điển sau in ra những dòng nào?

java
switch (x) {
    case 1: System.out.println("A");
    case 2: System.out.println("B");
    case 3: System.out.println("C"); break;
    case 4: System.out.println("D");
}

Câu 4 (tìm lỗi, mục tiêu 4). Đoạn String label = switch (n) { case 1 -> "one"; case 2 -> "two"; }; với n kiểu int không biên dịch được. Nêu lý do và sửa.

Câu 5 (giải thích cho bạn chưa học, mục tiêu 1). Bạn của bạn hỏi: "Khách 95 điểm sao lại bị xếp hạng thấp nhất?" Hãy giải thích bằng hình ảnh cổng soát vé, rồi nói cách sửa.

Điều cần nhớ

  • Chuỗi if, else if, else thử điều kiện từ trên xuống và dừng ở nhánh đúng đầu tiên, nên xếp từ chặt đến lỏng (mục tiêu 1).
  • Luôn viết ngoặc nhọn vì else đi với if gần nhất, và dùng == hoặc >= trong điều kiện chứ không dùng = (mục tiêu 2).
  • Trong switch cổ điển nhãn chỉ là điểm vào, thiếu break thì máy chạy xuyên các nhãn dưới (mục tiêu 3).
  • switch biểu thức dùng ->, trả về giá trị, chỉ chạy một nhánh, dùng yield trong khối, và bắt buộc bao phủ mọi đầu vào (mục tiêu 4).

Cheat sheet một trang.

Việc cần làmCú phápGhi chú
Chọn một trong haiif (c) { ... } else { ... }Luôn viết ngoặc nhọn
Chọn theo khoảng giá trịif (...) ... else if (...) ... else ...Xếp từ chặt đến lỏng
Chọn theo giá trị cố địnhswitch (x) { case 1 -> ...; default -> ...; }Dạng mũi tên, không cần break
Gộp nhiều nhãncase 2, 3 -> ...Dùng dấu phẩy
Khối nhiều dòng có giá trịdefault -> { ...; yield v; }Kết thúc bằng yield
So sánh chuỗis.equals("yes")Không dùng ==
So sánh bằngx == 80Một dấu bằng là phép gán
Switch trên chuỗi có thể nullkiểm tra null trướcGiá trị null làm switch ném lỗi

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: khoảng giá trị của một số thì chuỗi if, một giá trị cố định thì switch biểu thức, nhiều giá trị chung kết quả thì gộp nhãn bằng dấu phẩy, và giá trị có thể null thì kiểm tra trước.

Câu hỏi để dành cho ngày mai: quầy thu ngân muốn tính tổng tiền của 50 sản phẩm, mỗi sản phẩm có một giá khác nhau. Nếu chỉ dùng những gì bạn có đến giờ, bạn phải viết bao nhiêu dòng cộng? Hãy nghĩ trước khi sang bài 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 switch biểu thức trên enum. Với enum đã liệt kê đủ hằng, switch biểu thức không cần default vì trình biên dịch biết hết các giá trị. Nếu sau này ai đó thêm một hằng vào enum mà quên cập nhật switch, mã sẽ không biên dịch được, thay vì chạy sai âm thầm. Chương trình Deeper [Đã chạy, JDK 17.0.15]:

text
SAT weekend
NullPointerException: Cannot invoke "String.hashCode()" because "<local4>" is null

Dòng thứ hai cho thấy switch trên một chuỗi null ném NullPointerException ngay ở dòng switch (phần <local4> là số thứ tự biến, có thể khác ở chương trình của bạn).

So với lựa chọn khác. Bốn lỗi biên dịch của switch biểu thức, mỗi lỗi đã chạy thật [Đã chạy, JDK 17.0.15]: trộn -> và : cho different case kinds used in the switch; thiếu default trên int cho the switch expression does not cover all possible input values; hai nhãn trùng cho duplicate case label; khối { } không có yield cho switch rule completes without providing a value.

Hiểu lầm nâng cao. Nhiều người nghĩ default luôn cứu được switch. Với giá trị chọn là null, ngoại lệ được ném ra trước khi default được xét, như dòng thứ hai ở trên. Dạng cổ điển có một cách dùng chạy xuyên có chủ đích: xếp các nhãn trống liền nhau để gộp nhiều giá trị vào một nhánh, nhưng dạng case 2, 3 -> làm việc đó rõ ràng hơn.

Giới hạn của bài. Bài này chưa nói về switch với mẫu kiểu (pattern matching) của các bản Java mới hơn, về toán tử điều kiện ? :, và về chuẩn bị dữ liệu thử cho từng nhánh. 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 là Vòng lặp: cách chương trình lặp lại một việc nhiều lần thay vì viết lại nhiều dòng. Bạn sẽ dùng nó để cộng giá của 50 sản phẩm chỉ với vài dòng.

Mini-dự án nối tiếp: ở bài sau, quầy thu ngân sẽ duyệt một danh sách đơn hàng và áp dụng hạng thành viên cho từng đơn. Hãy giữ lại file 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
if (points >= 80) {
    tier = "GOLD";
} else if (points >= 65) { // thứ tự từ chặt đến lỏng
    tier = "SILVER";
} else if (points >= 50) {
    tier = "BRONZE";
}
int fee2 = switch (other) {
    case 1 -> 20_000;
    case 2 -> 30_000;
    default -> 35_000;
};
Dòng codeMáy làm gìLý do chọn cách này
if (member) { discountPercent = 10; } else { discountPercent = 0; }Chọn một trong hai giá trị cho discountPercentCả hai nhánh đều gán nên không bao giờ để trống
if (points >= 80) rồi else if (points >= 65)Thử từ chặt đến lỏng, 79 dừng ở nhánh haiChặt nhất đứng đầu để không bị nuốt
Ở bước 3, đặt vip >= 80 lên đầuKhách 95 gặp điều kiện chặt nhất trướcĐây là cách sửa duy nhất: đổi thứ tự, không đổi điều kiện
Ở bước 4, thêm break; sau fee = 20_000; và sau fee = 30_000;Máy dừng sau dòng của nhãn khớpNhãn cuối default không cần break vì đã ở cuối switch
int fee2 = switch (other) { ... }Chọn giá trị nhánh theo otherDạng mũi tên, bắt buộc có default cho int

Lời giải Bài 1. [Đã chạy] Bảng:

ĐiểmĐiều kiện được thửHạng
80>= 80 đúngGOLD
65>= 80 sai, >= 65 đúngSILVER
49>= 80 sai, >= 65 sai, >= 50 sai, vào elseNONE

Dấu >= tính cả điểm bằng mốc, nên 80 vào nhánh GOLD và 65 vào nhánh SILVER.

Lời giải Bài 2. [Đã chạy] Mẫu sau: if (member) { if (bigOrder) { ... } } else { ... }. Cặp ngoặc quanh if (bigOrder) đóng nó lại, nên else bên ngoài buộc vào if (member). Với true không in gì, với false in not a member.

Lời giải Bài 3. [Đã chạy] Dùng đúng mẫu của ZoneFee: int fee = switch (zone) { case 1 -> 20_000; case 2 -> 30_000; default -> 35_000; }; rồi in "zone " + zone + " fee " + fee. Không còn break, và chỉ nhánh khớp chạy, nên với zone bằng 1 chỉ có một dòng zone 1 fee 20000.

Lời giải Bài 4. [Đã chạy, fee = 45000] Các dòng then chốt:

java
int fee = switch (zone) { case 1 -> 20_000; case 2, 3 -> 30_000; default -> 35_000; };
if (weight > 2) {
    fee += (int) Math.ceil(weight - 2) * 5_000; // mỗi kg đã bắt đầu vượt 2 kg tính 5000
}
if (member && fee > 40_000) {
    fee -= 10_000; // thành viên giảm 10000 khi phí vượt 40000
}
Dòng codeMáy làm gìLý do chọn cách này
switch biểu thức cho feeVới zone 9 ra 35000Nhiều vùng cố định nên switch gọn hơn chuỗi if
Math.ceil(weight - 2)6.0 trừ 2 được 4.0, làm tròn lên 4Mỗi kg đã bắt đầu vượt đều tính, nên làm tròn lên
fee += ... * 5_00035000 cộng 4 lần 5000 ra 55000Dấu += cộng thêm vào phí cơ bản
member && fee > 40_000Thành viên và 55000 lớn hơn 40000 nên trừ 10000 ra 45000Hai điều kiện ghép bằng &&, như bài toán tử

Đầu chương trình có if (weight <= 0) in weight must be positive rồi dừng, để cân nặng sai không đi tiếp vào phép tính.

Gợi ý lời giải Bài 5. [Đã chạy] Cấu trúc then chốt: if (command == null) gán reply bằng no command, còn nhánh else gán reply bằng một switch biểu thức có ba nhãn "start", "stop" và default. switch trên chuỗi gọi hashCode() của giá trị chọn nên null ném lỗi, do đó phải chặn trước bằng if. Chữ "Stop" có chữ hoa nên không khớp "stop" và rơi vào default.

Đáp án Tự kiểm tra.

  1. Máy thử các điều kiện từ trên xuống, và dừng ở điều kiện đúng đầu tiên rồi bỏ qua mọi nhánh còn lại. Điều kiện lỏng đứng đầu sẽ nuốt các nhánh phía sau, nên phải xếp từ chặt đến lỏng. Kiểm tra mục tiêu 1.
  2. In ra two [Đã chạy]. else đi với if (b) gần nhất, a đúng nên vào if (b), mà b sai nên nhánh else chạy. Kiểm tra mục tiêu 2.
  3. In ra B rồi C [Đã chạy]. Máy nhảy vào case 2, chạy xuyên vào case 3, và dừng ở break, nên A và D không in. Kiểm tra mục tiêu 3.
  4. switch biểu thức phải bao phủ mọi đầu vào, mà int còn nhiều giá trị khác 1 và 2 nên thiếu nhánh. Sửa: thêm default -> "other"; [Đã chạy, thông báo the switch expression does not cover all possible input values]. Kiểm tra mục tiêu 4.
  5. Cổng soát vé đầu tiên mở cho mọi khách từ 50 điểm, nên người 95 điểm qua cổng đó và không bao giờ tới các cổng cao hơn. Sửa bằng cách đặt cổng 80 điểm lên đầu, rồi 65, rồi 50. Kiểm tra mục tiêu 1.