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, if và switch, 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.
![]()
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.

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
connected là false, 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.

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 mà == 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

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 selector | Từ phiên bản | Ghi chú |
|---|---|---|
byte, short, char, int | Java 1.0 | bộ ban đầu |
Byte, Short, Character, Integer | Java 5 | được unbox tự động |
enum | Java 5 | label là tên trần của constant |
String | Java 7 | so bằng .equals() chứ không phải == |
| mọi reference type khi dùng pattern | Java 21 | xem phần pattern matching |
Còn những cái này thì không. long, float, double và boolean 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
double và boolean 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

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)
Và 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 if | switch | |
|---|---|---|
| Kiểm tra cái gì | mọi boolean expression | một giá trị so với các hằng số |
| Khoảng giá trị và điều kiện ghép | được | không, chỉ so bằng |
| Type của selector | mọi thứ cho ra boolean | họ int, String, enum, pattern |
| Số nhánh vẫn còn dễ đọc | hai hoặc ba | nhiề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ánh | mỗ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, float
và boolean. 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, while và do-while, cùng cách chọn giữa chúng.