Command Palette

Search for a command to run...

[Java Basics] Câu lệnh điều kiện trong Java: if, else if, else và switch

Mọi chương trình từ đầu series tới giờ đều chạy một mạch từ trên xuống dưới. Câu lệnh điều kiện là chỗ điều đó không còn đúng nữa: chương trình nhìn vào một giá trị rồi rẽ sang nhánh này thay vì nhánh kia. Java cho bạn hai công cụ cho việc này, ifswitch, cộng thêm ternary operator khi quyết định gọn đủ để nằm trong một expression.

Cả hai đều không phức tạp, và cả hai đều mang những cái bẫy sống sót được tới tận code chạy thật — một else gắn nhầm vào if khác, một break bị quên khiến khách gold âm thầm nhận mức giảm giá của silver, một switch dạng mũi tên không chịu compile vì chưa phủ hết trường hợp. Mọi chương trình, mọi thông báo của compiler và mọi stack trace bên dưới đều được compile và chạy thật trên OpenJDK 21.0.6.

Câu lệnh điều kiện trong Java: if, else if, else và switch

Bắt đầu từ if, vì switch chỉ là một phiên bản chuyên biệt của cùng ý tưởng đó.

if, else và chuỗi else if

Dạng nhỏ nhất là một điều kiện và một block. Điều kiện nằm trong ngoặc tròn, thân nằm trong ngoặc nhọn:

int age = 20;

if (age >= 18) {
    System.out.println("adult");
}

Thêm else cho nhánh còn lại, và else if cho nhánh thứ ba, thứ tư, thứ năm:

int score = 72;

if (score >= 90) {
    System.out.println("grade A");
} else if (score >= 80) {
    System.out.println("grade B");
} else if (score >= 70) {
    System.out.println("grade C");
} else {
    System.out.println("grade D or worse");
}

Java không có keyword elseif. else if đúng nghĩa đen là một else mà thân của nó lại là một if khác; cái chuỗi bạn thấy ở trên là một đống if lồng nhau mà quy ước cho phép viết trên cùng một mức thụt lề.

Chuỗi dừng lại ở điều kiện đúng đầu tiên

Việc lồng nhau đó dẫn tới một hệ quả đáng chứng minh chứ không nên chỉ nói suông: các điều kiện được đánh giá từ trên xuống dưới, cái đầu tiên cho true sẽ chạy nhánh của nó, và mọi điều kiện bên dưới nó không hề được đánh giá. Đây là phiên bản in ra một dòng mỗi khi một điều kiện thật sự bị hỏi tới:

public class Chain {
    static boolean check(String label, boolean result) {
        System.out.println("  evaluating " + label);
        return result;
    }

    public static void main(String[] args) {
        int score = 72;
        System.out.println("score = " + score);

        if (check("score >= 90", score >= 90)) {
            System.out.println("grade A");
        } else if (check("score >= 80", score >= 80)) {
            System.out.println("grade B");
        } else if (check("score >= 70", score >= 70)) {
            System.out.println("grade C");
        } else if (check("score >= 60", score >= 60)) {
            System.out.println("grade D");
        } else {
            System.out.println("grade F");
        }
    }
}
score = 72
  evaluating score >= 90
  evaluating score >= 80
  evaluating score >= 70
grade C

Ba điều kiện được đánh giá chứ không phải bốn. score >= 60 chưa từng bị hỏi tới, và else cũng vậy.

Chuỗi else if được đánh giá từ trên xuống dưới và dừng ở điều kiện đúng đầu tiên

Chuyện này quan trọng hơn là chuyện nhanh chậm. Nếu một điều kiện gọi method có ghi log, thay đổi state hay chạm vào database, đặt nó ở cuối chuỗi nghĩa là nó có thể không bao giờ chạy. Điều kiện trong một chuỗi if nên không có side effect, chính vì lý do đó.

Nhiều if rời rạc không phải là một chuỗi

Bỏ các keyword else đi thì bạn không còn chuỗi nữa — bạn có bốn câu lệnh độc lập, và cả bốn đều được đánh giá:

int score = 95;

if (score >= 90) System.out.println("grade A");
if (score >= 80) System.out.println("grade B");
if (score >= 70) System.out.println("grade C");
if (score >= 60) System.out.println("grade D");
grade A
grade B
grade C
grade D

Bốn dòng thay vì một. Khi các nhánh vốn phải loại trừ nhau thì chúng cần else.

Thứ tự các điều kiện là một phần của logic

Vì nhánh đúng đầu tiên thắng, một chuỗi có vùng giá trị chồng lấn mà viết sai thứ tự sẽ co lại thành đúng nhánh đầu tiên:

int score = 95;

if (score >= 60) {
    System.out.println("grade D");
} else if (score >= 70) {
    System.out.println("grade C");
} else if (score >= 80) {
    System.out.println("grade B");
} else if (score >= 90) {
    System.out.println("grade A");
}
grade D

95 >= 60 là true, nên không có gì phía dưới được hỏi tới nữa. Compiler không có ý kiến gì ở đây — về nguyên tắc các nhánh bên dưới vẫn tới được, chỉ là không tới được với bất kỳ giá trị nào đã lọt vào nhánh đầu. Với các khoảng chồng lấn, hãy xếp từ điều kiện hẹp nhất tới rộng nhất.

Ngoặc nhọn là tuỳ chọn, và đó là chỗ sinh bug

Java cho phép bỏ ngoặc nhọn khi thân nhánh chỉ có một câu lệnh:

if (age >= 18)
    System.out.println("adult");

Hợp lệ, và cũng là nguồn của cả một họ bug, bởi compiler xác định "thân nhánh" theo ngữ pháp còn người đọc xác định theo thụt lề. Khi hai bên bất đồng thì compiler thắng:

public class Braces {
    static void openConnection() { System.out.println("openConnection()"); }
    static void sendPayload()    { System.out.println("sendPayload()"); }

    public static void main(String[] args) {
        boolean connected = false;

        if (connected)
            openConnection();
            sendPayload();

        System.out.println("main finished");
    }
}
sendPayload()
main finished

connectedfalse, vậy mà payload vẫn được gửi. Cái if sở hữu đúng một câu lệnh — openConnection(); — còn sendPayload(); chỉ đơn giản là câu lệnh kế tiếp trong main. javac chạy trần không nói gì, và javac -Xlint:all cũng vậy.

Hình dạng của lỗi goto fail

Bug "goto fail" của Apple năm 2014 trong phần kiểm tra SSL certificate có đúng hình dạng này: một dòng bị lặp lại nằm dưới một if không ngoặc nhọn, thụt lề như thể nó thuộc về nhánh đó, nhưng thật ra chạy vô điều kiện. Hậu quả là các bước kiểm tra còn lại bị bỏ qua và chữ ký không hợp lệ vẫn được báo là hợp lệ.

Java bắt được một nửa của mẫu đó. Chép nguyên xi sang Java thì dòng return bị lặp làm cho code phía sau thành code chết:

static int verify() {
    int err;
    if ((err = hash("serverRandom")) != 0)
        return err;
    if ((err = hash("signedParams")) != 0)
        return err;
        return err;
    err = rawVerify();
    return err;
}
GotoFailA.java:19: error: unreachable statement
        err = rawVerify();
        ^
1 error

Quy tắc unreachable của Java từ chối compile một câu lệnh không bao giờ chạy được. Đó là một tấm lưới an toàn thật sự — và nó chỉ bật lên khi dòng thừa làm cho thứ khác không tới được. Đổi câu lệnh bị lặp thành một phép gán thì cùng kiểu thụt lề sai đó compile êm ru:

public class GotoFailB {
    static boolean isAdmin(String user) { return user.equals("root"); }

    static boolean allowed(String user, String action) {
        boolean granted = false;
        if (isAdmin(user))
            granted = true;
        if (action.equals("read"))
            granted = true;
            granted = true;
        return granted;
    }

    public static void main(String[] args) {
        System.out.println("root  / read   -> " + allowed("root", "read"));
        System.out.println("guest / read   -> " + allowed("guest", "read"));
        System.out.println("guest / delete -> " + allowed("guest", "delete"));
    }
}
root  / read   -> true
guest / read   -> true
guest / delete -> true

Mọi thứ đều được cho phép. javac -Xlint:all không báo gì cả, vì ở đây không có code chết và cũng không sai type — chỉ có một câu lệnh nằm không đúng chỗ mà thụt lề của nó nói.

⚠️ Hãy đặt ngoặc nhọn cho mọi nhánh, kể cả nhánh một dòng. Nó tốn hai ký tự và loại bỏ trọn vẹn loại bug ở trên, cộng thêm loại bug ở phần ngay sau đây.

Dangling else: một else không ngoặc nhọn thuộc về if nào?

Lồng hai if không ngoặc nhọn rồi thêm một else vào thì có hai cách đọc. Quy tắc của Java: một else gắn với if gần nhất phía trước mà chưa có else — bất kể bạn thụt lề thế nào.

public class Dangling {
    static void noBraces(int x, int y) {
        System.out.println("noBraces(x=" + x + ", y=" + y + ")");
        if (x > 10)
            if (y > 10)
                System.out.println("   both are big");
        else
            System.out.println("   x is not big");
    }

    static void withBraces(int x, int y) {
        System.out.println("withBraces(x=" + x + ", y=" + y + ")");
        if (x > 10) {
            if (y > 10)
                System.out.println("   both are big");
        } else {
            System.out.println("   x is not big");
        }
    }

    public static void main(String[] args) {
        noBraces(5, 20);
        withBraces(5, 20);
        noBraces(20, 5);
        withBraces(20, 5);
    }
}
noBraces(x=5, y=20)
withBraces(x=5, y=20)
   x is not big
noBraces(x=20, y=5)
   x is not big
withBraces(x=20, y=5)

Hai method cho kết quả ngược nhau. Với x = 5, bản không ngoặc nhọn không in gì trong khi bản có ngoặc nhọn in ra thông báo; với x = 20, y = 5 thì ngược lại.

Dangling else gắn với if gần nhất chưa có else, không phải if bạn thụt lề cùng

Trong noBraces, cái else thuộc về if (y > 10), nên toàn bộ khối đó là một câu lệnh duy nhất nằm dưới sự canh gác của if (x > 10). Khi x bằng 5 thì cái chốt đó là false và không nhánh nào chạy cả. Thụt lề nói khác đi, và thụt lề mới là thứ sai.

Ngoặc nhọn làm cho việc gắn kết trở nên rõ ràng, đó là lý do mọi style guide nghiêm túc đều bắt buộc dùng nó ở đây.

Điều kiện bắt buộc là boolean, không gì khác

Trong C, if (x = 5) compile được: phép gán cho ra 5, và mọi giá trị khác 0 đều được coi là đúng. Java không có khái niệm truthy. Điều kiện của if bắt buộc là boolean hoặc Boolean, nên cái lỗi gõ nhầm kinh điển trở thành lỗi compile chứ không phải một bug âm thầm:

int x = 0;
if (x = 5) {
    System.out.println("x is five");
}
NotBoolean.java:4: error: incompatible types: int cannot be converted to boolean
        if (x = 5) {
              ^
1 error

Điều đó áp dụng cho mọi giá trị không phải boolean, kể cả literal và reference:

if (1) { }
TruthyInt.java:3: error: incompatible types: int cannot be converted to boolean
        if (1) {
            ^
1 error
String name = "An";
if (name) { }
TruthyRef.java:4: error: incompatible types: String cannot be converted to boolean
        if (name) {
            ^
1 error

Không có chuyện if (list) nghĩa là "không rỗng", cũng không có if (str) nghĩa là "khác null". Hãy viết đúng phép so sánh bạn muốn: if (name != null), if (!name.isEmpty()), if (count != 0).

Phép gán duy nhất mà Java vẫn chấp nhận

Quy tắc về type đóng được trường hợp int, nhưng không đóng được trường hợp boolean. done = true là một assignment expression có type boolean, nên nó là một điều kiện hợp lệ:

boolean done = false;
System.out.println("before: done = " + done);
if (done = true) {
    System.out.println("this branch always runs");
}
System.out.println("after:  done = " + done);
before: done = false
this branch always runs
after:  done = true

Nhánh luôn chạy và variable bị ghi đè ngay lúc đi vào. javac -Xlint:all không nói gì về chuyện này. Đây là chỗ duy nhất Java không bảo vệ bạn, nên hãy đọc kỹ điều kiện if trên các variable boolean, và ưu tiên if (done) thay vì if (done == true) — dạng ngắn không có dấu = nào để mà đánh rơi.

So sánh giá trị bên trong điều kiện

Điều kiện thường là các phép so sánh, và Java có hai loại. Với primitive, == so sánh giá trị và đó chính là thứ bạn muốn. Với object — bao gồm String== so sánh reference, tức là hỏi "hai cái này có phải cùng một object không", mà đó gần như không bao giờ là câu hỏi bạn đang đặt ra.

Scanner sc = new Scanner(System.in);
System.out.print("Type yes: ");
String answer = sc.nextLine();

if (answer == "yes") {
    System.out.println("== says they match");
} else {
    System.out.println("== says they do NOT match");
}

if (answer.equals("yes")) {
    System.out.println(".equals() says they match");
} else {
    System.out.println(".equals() says they do NOT match");
}
printf 'yes\n' | java StringEq
Type yes: == says they do NOT match
.equals() says they match

Người dùng gõ đúng yes== vẫn nói không, bởi một chuỗi được tạo lúc runtime là một object khác với literal nằm trong source. Hãy dùng .equals() để so nội dung.

Bản thân .equals() cũng có một điểm chết: nó là instance method, nên gọi nó trên null là ném exception.

String input = null;
System.out.println("\"yes\".equals(input) -> " + "yes".equals(input));
System.out.println("Objects.equals(input, \"yes\") -> " + Objects.equals(input, "yes"));
System.out.println("Objects.equals(null, null)   -> " + Objects.equals(null, null));
System.out.println(input.equals("yes"));
"yes".equals(input) -> false
Objects.equals(input, "yes") -> false
Objects.equals(null, null)   -> true
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "input" is null
	at Compare.main(Compare.java:9)

Từ đó rút ra hai cách viết an toàn với null. Đặt literal ở bên trái — "yes".equals(input) — thì cái nhận lệnh gọi không bao giờ null được. Hoặc gọi java.util.Objects.equals(a, b), nó xử lý null ở cả hai phía và trả về true khi cả hai đều null. String còn nhiều thứ sâu hơn thế; bài về String trong series này nói kỹ về identity, string pool và .equals().

Ternary operator như một if/else gọn

if là một câu lệnh: nó chạy code chứ không sinh ra giá trị. Ternary operator ?: là dạng expression của cùng quyết định đó, nên nó nằm được ở vế phải của một phép gán:

int a = 7, b = 12;

int max;
if (a > b) {
    max = a;
} else {
    max = b;
}

int max2 = (a > b) ? a : b;

int n = 9;
System.out.println(n + " is " + (n % 2 == 0 ? "even" : "odd"));
if/else  max = 12
ternary  max = 12
9 is odd

Dùng nó khi cả hai nhánh cùng sinh ra một giá trị cho cùng một variable, và đặc biệt là bên trong phép nối chuỗi hay trong argument của method, những chỗ mà một câu lệnh if không chen vào được. Bản thân operator này — độ ưu tiên, cái bẫy autoboxing — thuộc về bài viết về operator trong series này.

Chỗ nó hết giúp ích là khi lồng nhau. Một chuỗi ternary chính là một chuỗi else if viết mà không có chữ:

int score = 72;
String grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F";
nested ternary grade = C

Đoạn đó đúng và cũng khó đọc. Hai tầng thường là giới hạn; quá mức đó thì một chuỗi if hoặc một switch expression dễ đọc hơn.

switch: dạng dấu hai chấm kinh điển

switch đem một giá trị so với một danh sách hằng số. Ở dạng kinh điển, mỗi hằng số là một label case kết thúc bằng dấu hai chấm, còn default hứng mọi trường hợp còn lại:

static void describe(int day) {
    System.out.print("day " + day + " -> ");
    switch (day) {
        case 1:
            System.out.println("Monday");
            break;
        case 2:
            System.out.println("Tuesday");
            break;
        case 3:
            System.out.println("Wednesday");
            break;
        default:
            System.out.println("not a weekday I know");
    }
}
day 1 -> Monday
day 3 -> Wednesday
day 9 -> not a weekday I know

Từ quan trọng ở đây là label. Một case không phải là một block. Nó là một chỗ để nhảy tới.

Fall-through là mặc định, không phải lỗi của compiler

Một khi control đi vào ở label khớp, nó cứ chạy tiếp xuống dưới qua mọi thứ bên dưới — kể cả các label case khác — cho tới khi gặp break (hoặc return, hoặc hết switch) chặn lại. Bỏ các break khỏi chương trình ở trên là thấy khác biệt ngay:

static void noBreak(int day) {
    System.out.print("day " + day + " -> ");
    switch (day) {
        case 1: System.out.print("Mon ");
        case 2: System.out.print("Tue ");
        case 3: System.out.print("Wed ");
        default: System.out.print("other");
    }
    System.out.println();
}
day 1 -> Mon Tue Wed other
day 3 -> Wed other
day 9 -> other

day 1 in ra bốn thứ. Trả break về chỗ cũ thì cùng những lời gọi đó mỗi cái chỉ in một thứ:

day 1 -> Mon 
day 3 -> Wed 
day 9 -> other

Control đi vào switch ở case khớp rồi rơi xuống tiếp cho tới khi break chặn lại

javac chạy trần compile bản hỏng mà không nói một lời. javac -Xlint:fallthrough thì có cảnh báo:

FallThrough.java:7: warning: [fallthrough] possible fall-through into case
            case 2:
            ^
FallThrough.java:9: warning: [fallthrough] possible fall-through into case
            case 3:
            ^
FallThrough.java:11: warning: [fallthrough] possible fall-through into case
            default:
            ^
3 warnings

Hãy bật cái lint đó lên. Bug mà nó bắt được thuộc loại lọt được ra production, bởi nó chỉ lộ ra với những input rơi vào một label nằm sớm:

static double discount(String tier, double price) {
    double rate = 0;
    switch (tier) {
        case "gold":
            rate = 0.20;
        case "silver":
            rate = 0.10;
            break;
        case "bronze":
            rate = 0.05;
            break;
        default:
            rate = 0;
    }
    return price * (1 - rate);
}
gold   1000 -> 900.0
silver 1000 -> 900.0
bronze 1000 -> 950.0

Khách gold trả đúng bằng khách silver. Thiếu một break, và hạng khách quan trọng nhất lại chính là hạng bị sai.

Trường hợp duy nhất mà fall-through là điều bạn muốn

Các label xếp chồng nhau không có câu lệnh nào ở giữa sẽ dùng chung một thân, và đó là cách sạch sẽ để diễn đạt "bất kỳ cái nào trong số này":

static int daysIn(int month, int year) {
    int days;
    switch (month) {
        case 1: case 3: case 5: case 7: case 8: case 10: case 12:
            days = 31;
            break;
        case 4: case 6: case 9: case 11:
            days = 30;
            break;
        case 2:
            days = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 ? 29 : 28;
            break;
        default:
            throw new IllegalArgumentException("month out of range: " + month);
    }
    return days;
}
2026-01 -> 31
2026-04 -> 30
2026-02 -> 28
2028-02 -> 29
Exception in thread "main" java.lang.IllegalArgumentException: month out of range: 13
	at GroupedCases.daysIn(GroupedCases.java:15)
	at GroupedCases.main(GroupedCases.java:25)

Đây là gom nhóm chứ không phải rơi vào thân của case khác, và -Xlint:fallthrough không cảnh báo về nó. Mọi trường hợp khác — một case chạy câu lệnh của nó rồi cố ý chạy tiếp sang case kế — xứng đáng có một comment nói rõ, vì nếu không thì mọi người đọc đều sẽ cho rằng bạn quên break.

default, và nó được đặt ở đâu

default là tuỳ chọn. Bỏ nó khỏi một switch statement thì một giá trị không khớp đơn giản là không làm gì cả.

Nó cũng không bắt buộc phải nằm cuối. Compiler coi nó như một label nữa, nên nó đặt ở đâu cũng được — và nếu nó không có break thì control rơi từ nó sang label đứng ngay sau:

static void middleNoBreak(int n) {
    System.out.print("middle(" + n + ") -> ");
    switch (n) {
        case 1:
            System.out.print("one ");
            break;
        default:
            System.out.print("other ");
        case 2:
            System.out.print("two ");
            break;
    }
    System.out.println();
}
middle(1) -> one 
middle(2) -> two 
middle(7) -> other two

middle(7) khớp default, in other, rồi rơi tiếp vào case 2. Điều đó hợp lệ và gần như luôn là một sai sót. Hãy đặt default ở cuối và cho nó một break hoặc một throw.

switch nhận được những type nào?

Selector — cái expression nằm trong switch (...) — bị giới hạn. Những cái này chạy được:

Type của selectorTừ phiên bảnGhi chú
byte, short, char, intJava 1.0bộ ban đầu
Byte, Short, Character, IntegerJava 5được unbox tự động
enumJava 5label là tên trần của constant
StringJava 7so bằng .equals() chứ không phải ==
mọi reference type khi dùng patternJava 21xem phần pattern matching

Còn những cái này thì không. long, float, doubleboolean bị từ chối thẳng:

long id = 3L;
switch (id) {
    case 1L:
        System.out.println("one");
        break;
    default:
        System.out.println("other");
}
SwitchLong.java:4: error: selector type long is not allowed
        switch (id) {
               ^
1 error

doubleboolean cho cùng thông báo đó với tên type của chúng. Với long, hoặc ép kiểu khi khoảng giá trị cho phép, hoặc dùng một chuỗi if. Với boolean thì if/else vốn đã chính là cái switch rồi.

Còn một quy tắc nữa: label của case phải là hằng số biết được lúc compile. Một static final int thì được, một local variable thường thì không:

int limit = 10;
switch (n) {
    case limit: System.out.println("matched the local"); break;
    default:    System.out.println("no match");
}
CaseConst.java:9: error: constant expression required
            case limit: System.out.println("matched the local"); break;
                 ^
1 error

Một local final int limit = 10; là hằng số và compile được. Label trùng nhau cũng bị từ chối: error: duplicate case label.

Switch trên String có một cái bẫy lúc chạy. Dạng kinh điển gọi method trên selector, nên null ném exception trước khi bất kỳ label nào được xét tới:

String cmd = null;
switch (cmd) {
    case "list": System.out.println("listing"); break;
    default:     System.out.println("unknown command");
}
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.hashCode()" because "<local2>" is null
	at SwitchNull.main(SwitchNull.java:4)

Chữ String.hashCode() trong thông báo đó chính là phần dịch ngầm lộ ra: một switch trên String được compile thành một lần tra hash rồi tới một lần kiểm tra equals. default ở đây không hứng được null — hãy chặn trước khi vào switch, hoặc dùng dạng của Java 21 mô tả bên dưới, nơi null có thể là một label viết ra hẳn hoi.

switch hiện đại: label mũi tên, switch expression và yield

Java 14 chốt lại một cú pháp thứ hai. Thay dấu hai chấm bằng -> thì label sở hữu đúng một câu lệnh, một expression hoặc một block — và control không bao giờ rơi sang label kế tiếp:

enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }

static void classify(Day d) {
    System.out.print(d + " -> ");
    switch (d) {
        case MON, TUE, WED, THU, FRI -> System.out.println("work day");
        case SAT, SUN                -> System.out.println("weekend");
    }
}
MON -> work day
SAT -> weekend

Hai thứ thay đổi cùng lúc. Không có break vì chẳng có gì để mà thoát ra, và một label duy nhất liệt kê được nhiều constant ngăn nhau bằng dấu phẩy, thay thế cho mẹo xếp chồng label.

Hai dạng không được trộn trong cùng một switch:

MixForms.java:6: error: different case kinds used in the switch
            case 2: System.out.println("two"); break;
            ^
1 error

Thay đổi lớn hơn là switch có thể là một expression — nó sinh ra một giá trị, nên gán được:

static int daysIn(int month, int year) {
    return switch (month) {
        case 1, 3, 5, 7, 8, 10, 12 -> 31;
        case 4, 6, 9, 11 -> 30;
        case 2 -> (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 ? 29 : 28;
        default -> throw new IllegalArgumentException("month out of range: " + month);
    };
}
daysIn(2, 2026) = 28
daysIn(2, 2028) = 29

Để ý dấu chấm phẩy sau ngoặc nhọn đóng: cả cái switch là một expression nằm trong câu lệnh return. Một throw được phép làm nhánh vì nó kết thúc đột ngột thay vì sinh ra giá trị.

Khi một nhánh cần nhiều hơn một dòng thì dùng block — và lúc đó yield cung cấp giá trị mà nhánh đó sinh ra:

static String bucket(int n) {
    return switch (n) {
        case 0 -> "zero";
        case 1, 2, 3 -> "small";
        default -> {
            String size = n > 100 ? "huge" : "medium";
            System.out.println("   (computing bucket for " + n + ")");
            yield size + " (" + n + ")";
        }
    };
}
bucket(0)   = zero
bucket(2)   = small
   (computing bucket for 50)
bucket(50)  = medium (50)
   (computing bucket for 500)
bucket(500) = huge (500)

yield không phải return. Dùng return bên trong một switch expression là lỗi compile, vì nó định thoát khỏi cả method từ bên trong một expression:

BadYield.java:5: error: attempt to return out of a switch expression
            default -> { return "other"; }
                         ^
1 error

Dạng dấu hai chấm có break, dạng mũi tên, và switch expression trả về giá trị bằng yield

switch expression phải sinh ra giá trị ở mọi nhánh

Một switch statement được phép bỏ qua những input nó không xử lý. Một switch expression thì không: nó phải sinh ra giá trị với bất cứ thứ gì đi vào, nên compiler đòi các nhánh phải phủ hết. Trên một enum, điều đó nghĩa là đủ mọi constant, hoặc một default:

enum Status { NEW, PAID, SHIPPED }

static String label(Status s) {
    return switch (s) {
        case NEW -> "waiting for payment";
        case PAID -> "ready to pack";
    };
}
EnumMissing.java:5: error: the switch expression does not cover all possible input values
        return switch (s) {
               ^
1 error

Thêm case SHIPPED -> "on the way"; vào là compile được:

waiting for payment
ready to pack
on the way

Đây là tính năng chứ không phải chướng ngại. Liệt kê đủ mọi constant và bỏ default đi nghĩa là ngày nào đó ai thêm constant thứ tư vào enum, compiler sẽ chỉ thẳng vào từng switch cần sửa. Một nhánh default sẽ dập tắt cảnh báo đó vĩnh viễn.

Quy tắc phủ hết này không áp dụng cho switch statement dạng mũi tên — giá trị không khớp thì đơn giản là không làm gì:

enum Day { MON, SAT, SUN }

Day d = Day.SUN;
switch (d) {
    case MON -> System.out.println("work day");
}
System.out.println("nothing matched, and that is fine for a statement");
nothing matched, and that is fine for a statement

Pattern matching cho switch, chính thức từ Java 21

Java 21 chốt chính thức pattern matching cho switch (JEP 441), sau các bản preview từ Java 17 tới 20. Một label case giờ có thể là một type pattern vừa kiểm tra type vừa bind ra một variable, có thể thu hẹp thêm bằng một guard when, và null trở thành một label bạn viết ra được:

static String describe(Object o) {
    return switch (o) {
        case null                   -> "nothing at all";
        case Integer i when i > 100 -> "a big int: " + i;
        case Integer i              -> "an int: " + i;
        case String s               -> "a String of length " + s.length();
        case Double d               -> "a double: " + d;
        default                     -> "a " + o.getClass().getSimpleName();
    };
}
an int: 7
a big int: 500
a String of length 5
a double: 3.5
nothing at all
a LocalDate

Có ba quy tắc đáng nhớ. Thứ tự quan trọng, vì pattern khớp đầu tiên thắng — nhánh có guard Integer i when i > 100 bắt buộc phải nằm trước nhánh Integer i trơn, nếu không nó không bao giờ khớp được. Không có case null thì một pattern switch ném exception chứ không rơi xuống default:

Exception in thread "main" java.lang.NullPointerException
	at java.base/java.util.Objects.requireNonNull(Objects.java:233)
	at PatternNull.describe(PatternNull.java:3)
	at PatternNull.main(PatternNull.java:11)

case null, default -> là một label ghép hợp lệ khi bạn muốn xử lý cả hai như nhau.

Trên JDK cũ hơn thì cả cấu trúc này bị từ chối, kèm theo tên phiên bản trong thông báo:

PatternPreview.java:4: error: patterns in switch statements are not supported in -source 20
            case Integer i -> "int " + i;
                 ^
  (use -source 21 or higher to enable patterns in switch statements)
1 error

Java 21 cũng nới một quy tắc nhỏ hơn: label case của enum giờ được viết đủ tên, nên case Status.PAID: compile được. Dưới --release 17 thì đúng dòng đó bị từ chối với error: an enum switch case label must be the unqualified name of an enumeration constant.

Chuỗi if hay switch: cái nào dễ đọc hơn?

Chuỗi ifswitch
Kiểm tra cái gìmọi boolean expressionmột giá trị so với các hằng số
Khoảng giá trị và điều kiện ghépđượckhông, chỉ so bằng
Type của selectormọi thứ cho ra booleanhọ int, String, enum, pattern
Số nhánh vẫn còn dễ đọchai hoặc banhiều
Thiếu một trường hợpâm thầm không làm gìdạng expression là lỗi compile
Chi phí rẽ nhánhmỗi nhánh một phép so, theo thứ tựmột cú nhảy với tập label dày

Khác biệt về rẽ nhánh nhìn thấy được trong bytecode chứ không phải trên đồng hồ bấm giờ. Một chuỗi if bốn nhánh compile thành bốn phép so tuần tự; đúng bốn trường hợp đó trong một switch compile thành một tableswitch duy nhất, một bảng nhảy đánh chỉ số theo giá trị:

javap -c Dispatch
  static int withIfChain(int);
       0: iload_0
       1: iconst_1
       2: if_icmpne     8
       5: bipush        10
       7: ireturn
       8: iload_0
       9: iconst_2
      10: if_icmpne     16
      ...

  static int withSwitch(int);
       0: iload_0
       1: tableswitch   { // 1 to 4
                     1: 32
                     2: 35
                     3: 38
                     4: 41
               default: 44
          }

Với các label thưa — 1, 500, 9000 — javac sinh ra lookupswitch, một bảng đã sắp xếp để JVM tìm kiếm. Với số nhánh ít ỏi mà phần lớn chương trình có thì khác biệt này không đáng kể; lý do để chọn switch là nó nói rõ "một giá trị, nhiều hằng số" theo cách mà chuỗi if không nói được.

Thực tế: dùng switch cho một giá trị đem so với một tập hằng số cố định, nhất là enum hay một chuỗi lệnh; dùng if cho khoảng giá trị, cho điều kiện ghép, và cho mọi thứ dính tới nhiều hơn một variable. Một bộ điều phối menu là ví dụ rõ nhất cho switch expression:

import java.util.Scanner;

public class Menu {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("Command (list / add / quit): ");
        String cmd = sc.nextLine().trim().toLowerCase();

        String reply = switch (cmd) {
            case "list", "ls" -> "showing every item";
            case "add", "new" -> "creating an item";
            case "quit", "exit" -> "goodbye";
            default -> "unknown command: " + cmd;
        };
        System.out.println(reply);
    }
}
printf 'new\n' | java Menu
Command (list / add / quit): creating an item

Các lỗi thường gặp, kèm thông báo lỗi thật

Quên break ở dạng dấu hai chấm. Đã thấy ở trên: hạng gold âm thầm nhận mức giảm giá của silver. Hãy compile với -Xlint:fallthrough, hoặc dùng label mũi tên, nơi cái sai này không diễn đạt ra được.

Viết = trong khi định viết ==. Trên int đây là lỗi compile, incompatible types: int cannot be converted to boolean. Trên boolean thì nó compile được và nhánh luôn chạy. Hãy viết if (done), đừng bao giờ viết if (done == true).

So String bằng ==. Không lỗi, không cảnh báo, và nó có vẻ chạy đúng trong các test mà cả hai chuỗi đều là literal dùng chung một chỗ trong string pool. Nó hỏng ngay khi một trong hai đến từ input, từ file hay từ một lời gọi mạng. Hãy dùng .equals(), "literal".equals(x) hoặc Objects.equals(a, b).

Code không tới được sau một return trong nhánh. Java từ chối luôn:

static String grade(int score) {
    if (score >= 60) {
        return "pass";
        System.out.println("logging a pass");
    }
    return "fail";
}
Unreachable.java:5: error: unreachable statement
            System.out.println("logging a pass");
            ^
1 error

Lỗi anh em của nó bắt đúng cái sai ngược lại — một method mà return duy nhất nằm trong một if, nên đường đi khi điều kiện false sẽ rơi ra khỏi method:

MissingReturn.java:6: error: missing return statement
    }
    ^
1 error

Một dấu chấm phẩy lạc sau điều kiện. if (x > 10); là một if mà thân của nó là câu lệnh rỗng, còn cái block đứng sau chỉ là một block bình thường luôn luôn chạy:

int x = 3;
if (x > 10);
{
    System.out.println("x is greater than 10");
}
System.out.println("x = " + x);
x is greater than 10
x = 3

javac chạy trần im lặng. -Xlint:all thì bắt được:

EmptyIf.java:4: warning: [empty] empty statement after if
        if (x > 10);
                   ^
1 warning

FAQ

Java có keyword elseif không?

Không. Nó là hai chữ, else if, và cũng không phải một cấu trúc đặc biệt — cái else chỉ đơn giản sở hữu một câu lệnh if khác. Đó cũng là lý do chuỗi này không có giới hạn cố định và các điều kiện được đánh giá đúng theo thứ tự.

Vì sao switch của tôi chạy nhiều case cùng lúc?

Một label case đánh dấu một điểm vào chứ không phải một block. Một khi control nhảy tới label khớp, nó cứ chạy tiếp qua các label bên dưới cho tới khi gặp break, return hoặc ngoặc nhọn đóng. Hãy thêm break còn thiếu, hoặc chuyển switch sang label mũi tên vốn không thể rơi xuống.

switch có chạy được với long hay double không?

Không. javac báo error: selector type long is not allowed, và tương tự với double, floatboolean. Các selector được phép là byte, short, char, int cùng các wrapper của chúng, String, enum, và — từ Java 21 — mọi reference type khi các label là pattern.

yield và return trong switch khác nhau thế nào?

yield cung cấp giá trị của switch expression bao quanh nó từ bên trong một nhánh dạng block; sau đó chương trình chạy tiếp ngay sau switch. return thoát khỏi cả method, và đó là lý do dùng nó trong một switch expression là lỗi compile: attempt to return out of a switch expression.

Có cần default trong mọi switch không?

Trong switch statement thì không — giá trị không khớp sẽ không làm gì. Trong switch expression thì các nhánh phải phủ hết mọi input, nên bạn cần một default, hoặc với enum thì mỗi constant một nhánh. Nên chọn cách liệt kê đủ constant: khi ấy compiler sẽ báo cho bạn mọi switch cần sửa mỗi khi có constant mới được thêm.

Có nên luôn đặt ngoặc nhọn cho if không?

Có. Dạng không ngoặc nhọn là hợp lệ và nó chính là hình dạng đứng sau cả bug câu lệnh lạc chỗ lẫn bug dangling else. Hai ký tự cho mỗi nhánh đổi lấy một thân nhánh đúng như thụt lề của nó nói.

Kết luận

if kiểm tra mọi boolean expression, từ trên xuống dưới, nhánh đúng đầu tiên thắng, và mọi thứ bên dưới nhánh đó không hề được đánh giá. switch đem một giá trị so với các hằng số, và ở dạng kinh điển thì một case khớp là một điểm vào cứ chạy tiếp cho tới khi gặp break. Dạng mũi tên xoá bỏ hoàn toàn chuyện rơi xuống, dạng expression sinh ra giá trị và buộc bạn xử lý mọi input, còn pattern của Java 21 mở rộng cùng cú pháp đó sang type. Ngoặc nhọn cho mọi nhánh, .equals() cho object, và -Xlint:all trên compiler là đủ che phần lớn những gì còn lại.

Một câu lệnh điều kiện chạy một nhánh đúng một lần. Bài tiếp theo nói về cách còn lại khiến chương trình thôi chạy một mạch: vòng lặp — for, whiledo-while, cùng cách chọn giữa chúng.

Bài viết liên quan

[Java Basics] Phạm vi biến trong Java: biến cục bộ, field và static

Scope của variable trong Java: local variable, parameter, instance field và static field, block scope, shadowing, definite assignment và lifetime, cùng mọi lỗi cannot find symbol chạy thật trên JDK 21.

[Java Basics] Tính đóng gói trong Java: access modifier, getter và setter

Tính đóng gói (encapsulation) trong Java qua code chạy thật: bốn access modifier demo trên hai package thật, quy tắc protected mà phần lớn tài liệu nói sai, getter làm lộ mutable field, defensive copy, immutable class và record, cùng mọi error của javac chạy thật trên JDK 21.

[Java Basics] Tính kế thừa trong Java: extends và super

extends hoạt động thế nào, subclass kế thừa gì và không kế thừa gì, vì sao constructor không bao giờ được kế thừa, super(...) xâu chuỗi constructor lên tới java.lang.Object rồi chạy ngược xuống ra sao, field hiding khác overriding chỗ nào, protected qua package, final class, vấn đề fragile base class, và khi nào composition là lựa chọn đúng.

[Java Basics] Ép kiểu dữ liệu trong Java: implicit và explicit

Ép kiểu dữ liệu trong Java: widening và narrowing, tại sao (byte) 300 in ra 44, tại sao byte cộng byte không compile được, cast ẩn bên trong += và vì sao cast không phải là parsing.