Java Foundations
21/39
Bài 21 / 39~14 phútĐiều kiện & Vòng lặpLộ trình · chặng 02/21Miễn phí… lượt xem

switch — từ statement cổ điển đến expression hiện đại

Khám phá switch trong Java: phân biệt switch statement cổ điển có fall-through và switch expression hiện đại từ Java 14, cùng cơ chế bytecode tableswitch.

TL;DR: Lệnh switch là cấu trúc tra cứu giá trị hiệu năng cao. Phiên bản cổ điển (statement) tiềm ẩn lỗi nguy hiểm do cơ chế fall-through khi quên từ khóa break. Từ Java 14, switch expression với cú pháp mũi tên -> loại bỏ hoàn toàn fall-through, hỗ trợ trả về giá trị trực tiếp hoặc qua từ khóa yield, đồng thời kiểm tra tính đầy đủ (exhaustive). Ở tầng bytecode, JVM tối ưu hóa các hằng số liên tục thành tableswitch với độ phức tạp $O(1)$ và các hằng số phân tán thành lookupswitch với độ phức tạp $O(\log n)$.

Khi bạn có 5–10 nhánh so sánh cùng một biến với các hằng số, chuỗi if/else if dài lê thê. switch được sinh ra cho đúng pattern đó. Nhưng switch cổ điển có cái bẫy khó quên — fall-through — đã gây không ít lỗi production nổi tiếng.

Bài này giải thích switch từ phiên bản cổ điển (có fall-through), đến switch expression (Java 14+) an toàn hơn và trả được giá trị (rất phổ biến khi kết hợp với Enum type-safe), đến pattern matching (Java 21). Kèm cơ chế bytecode giải thích vì sao switch có thể nhanh hơn chuỗi if.

1. Analogy máy bán nước tự động mô tả cấu trúc switch như thế nào?

Bạn bỏ đồng xu, nhấn mã: A1 cho Coca, A2 cho Pepsi, B1 cho nước lọc, mặc định báo "hết hàng". Máy có bảng tra cứu từ mã đến hành động, không phải loạt "nếu A1 thì..., nếu A2 thì..., nếu B1 thì...".

Đời thườngswitch
Mã A1, A2, B1...case "A1":, case "A2":
Không mã nào khớp → "hết hàng"default:
Máy tra bảng nhanh, không đọc từng mãJVM có thể dùng tableswitch / lookupswitch
💡 Cách nhớ

switch là bảng tra cứu — compiler biết trước tập giá trị hằng để so, nên có thể sinh instruction nhảy trực tiếp theo index thay vì so sánh tuần tự.

2. Cú pháp switch statement cổ điển hoạt động ra sao và tiềm ẩn bẫy gì?

Java
int day = 3;
String name;

switch (day) {
    case 1:
        name = "Mon";
        break;
    case 2:
        name = "Tue";
        break;
    case 3:
        name = "Wed";
        break;
    case 4:
        name = "Thu";
        break;
    case 5:
        name = "Fri";
        break;
    case 6:
    case 7:
        name = "Weekend";
        break;
    default:
        name = "Invalid";
}

Điểm đặc biệt:

  • Biểu thức sau switch (...) phải là kiểu byte, short, int, char, String (Java 7+), hoặc enum. Không nhận long, double, float, boolean.
  • case phải là hằng số compile-time. Không được case someVar.
  • Thiếu break → fall-through sang case tiếp theo. Đôi khi hữu ích (case 6, 7 cùng xử lý "Weekend"), nhưng đa số là bug.

2.1 Fall-through — bẫy kinh điển

Java
int status = 1;
switch (status) {
    case 1:
        System.out.println("active");
        // QUEN break
    case 2:
        System.out.println("pending");
        break;
    case 3:
        System.out.println("closed");
        break;
}

Output: active rồi pending. Quên break ở case 1 → thực thi tiếp tục "rơi xuống" case 2.

Điểm dễ trượt: case 2: không phải một nhánh riêng được "chọn" — nó chỉ là một nhãn đánh dấu vị trí trong dòng chảy. JVM nhảy tới nhãn khớp rồi chạy thẳng xuống cho tới khi gặp break, kể cả khi phải băng qua nhãn của case khác:

Dòng chảy trong switch cổ điển với status bằng 1, thiếu break nên thân của case 2 vẫn chạy, đối lập với đường thoát ngay của cú pháp mũi tên

Java lấy nguyên semantics này từ C — C giữ nó vì đôi khi tiện cho state machine / jump table. Trong Java 90% trường hợp là bug, vì vậy Java 14 giới thiệu cú pháp mới không có fall-through mặc định.

Thử ngẫmmột service xử lý order status dùng switch cổ điển, case ACTIVE thiếu break nên log in luôn cả dòng của PENDING. Sửa bằng thêm break, hay đổi hẳn sang switch expression — khác nhau ở điều gì?

3. Cú pháp switch expression từ Java 14 có ưu điểm gì vượt trội?

Cú pháp mới (case L ->) không còn fall-through, và switch trở thành expression trả giá trị:

Java
int day = 3;

String name = switch (day) {
    case 1 -> "Mon";
    case 2 -> "Tue";
    case 3 -> "Wed";
    case 4 -> "Thu";
    case 5 -> "Fri";
    case 6, 7 -> "Weekend";
    default -> "Invalid";
};

Lợi ích:

  • Không fall-through — mỗi case L -> là 1 block độc lập, không cần break.
  • Trả giá trị — gán trực tiếp vào biến, không cần khai báo biến trước rồi gán trong từng case.
  • Case gộp — case 6, 7 -> ... ngắn gọn.
  • Exhaustive check với enum / sealed — compiler báo lỗi nếu thiếu case.

3.1 Block body với yield

Khi case cần nhiều statement trước khi "trả giá trị", dùng { } và yield:

Java
String desc = switch (day) {
    case 1, 2, 3, 4, 5 -> "Weekday";
    case 6, 7 -> {
        log("Weekend hit");
        yield "Weekend";
    }
    default -> throw new IllegalArgumentException("Day " + day);
};

yield = "trả giá trị ra ngoài switch". Khác return — return thoát khỏi method, yield chỉ thoát khỏi switch.

3.2 Exhaustive check với enum

Java
enum Status { ACTIVE, PENDING, CLOSED }

String display = switch (status) {
    case ACTIVE -> "Hoat dong";
    case PENDING -> "Cho";
    case CLOSED -> "Da dong";
    // khong can default — compiler da biet 3 case phu het enum
};

Nếu enum thêm value mới (BANNED), code trên compile error — buộc bạn update. Đây là power của type system: refactor an toàn, không sót case.

💡 Rule chọn giữa 2 style
  • Code mới → luôn dùng switch expression (case L ->). An toàn hơn, ngắn hơn.
  • Code cũ có sẵn → giữ nguyên statement nếu không gây vấn đề; migration khi refactor.
  • Cần fall-through thật sự (hiếm) → statement cổ điển mới có. Nhưng nên refactor thành case A, B -> nếu có thể.

Thử ngẫmenum Status thêm case BANNED mà switch expression cũ không viết default — compiler chặn bạn ngay. Nếu bạn từng thêm default -> null cho gọn, cái chặn đó còn tác dụng không?

4. Cơ chế tableswitch và lookupswitch ở tầng bytecode hoạt động ra sao?

JVM có 2 instruction đặc biệt cho switch:

  • tableswitch — dùng khi các case là số nguyên liên tiếp / gần liên tiếp. JVM nhảy theo offset trong bảng. O(1).
  • lookupswitch — dùng khi các case thưa thớt (ví dụ 1, 100, 1000). JVM tra bảng sorted key. O(log n).

Compiler chọn tự động. Ví dụ switch (day) với case 1–7:

tableswitch {
  1: GOTO case1
  2: GOTO case2
  ...
  7: GOTO case7
  default: GOTO defaultLabel
}

tableswitch tiêu tốn memory (bảng dense) nhưng nhảy trực tiếp — nhanh hơn chuỗi if/else if O(n).

Java
// "switch 1000 case" van O(1) neu dung tableswitch
// 1000 if/else if la O(n) — kiem tra tu dau den cuoi
📚 Vì sao JDK core thường dùng switch chứ không if/else dài?

Bytecode dành riêng cho switch (tableswitch, lookupswitch) cho JIT tối ưu thêm (branch table, jump table đích thật). Class kiểu Character.UnicodeBlock hoặc java.util.TimeZone có switch rất dài — chọn switch không chỉ vì cú pháp mà cả vì performance.

4.1 switch với String — hash + equals

switch nhận String từ Java 7, nhưng String không phải số nguyên. Compiler biến đổi:

Java
switch (s) {
    case "Mon": ...
    case "Tue": ...
}

Thành 2 tầng:

  1. switch (s.hashCode()) — chọn case ứng viên.
  2. Bên trong mỗi case, s.equals("Mon") để xác minh (phòng hash collision).

Kết quả: O(1) trung bình, correctness đảm bảo qua equals. Bạn không cần tự tối ưu — compiler đã lo.

5. Tính năng Pattern Matching cho switch trong Java 21 hoạt động như thế nào?

Java 21 final hoá pattern matching trong switch (JEP 441), cho phép case theo kiểu + điều kiện:

Java
static String describe(Object obj) {
    return switch (obj) {
        case Integer i when i > 0 -> "positive int: " + i;
        case Integer i -> "non-positive int: " + i;
        case String s when s.isEmpty() -> "empty string";
        case String s -> "string of len " + s.length();
        case int[] arr -> "int array len " + arr.length;
        case null -> "null";
        default -> "unknown";
    };
}

Đặc điểm:

  • Pattern Type var bind biến (giống instanceof Type var).
  • when <condition> thêm guard — case khớp khi cả pattern và condition đúng.
  • case null cho phép match null tường minh (trước Java 21, switch ném NPE với null).
  • Exhaustive với sealed class — compiler đảm bảo phủ hết subtype.

Trước Java 21 bạn sẽ viết if/else if/else với instanceof; giờ switch lo luôn. Chi tiết ở module OOP nâng cao.

Thử ngẫmđoạn service cũ bọc switch trong try/catch để nuốt NullPointerException khi tham số null. Chuyển sang case null -> của Java 21, đoạn try/catch đó còn cần không?

6. Khi nào nên dùng switch và khi nào nên dùng chuỗi if else?

Tình huốngNên dùng
So biến với nhiều hằng số (>3 nhánh) cùng kiểuswitch
Điều kiện phức tạp (range, biểu thức boolean)if/else if
So sánh double, float, longif/else if (switch không hỗ trợ)
Cần trả giá trị, gán biếnswitch expression
Enum với tất cả caseswitch expression (exhaustive check)
Kiểm tra kiểu run-time (polymorphism)Java 21: switch pattern matching; trước đó: instanceof + if

7. Các bẫy pitfall thường gặp nhất khi sử dụng switch là gì?

❌ Nhầm 1: Quên break trong switch statement.

Java
case 1:
    x = 10;
// quen break — chay tiep case 2
case 2:
    x = 20;
    break;

✅ Dùng switch expression (case L ->) — không fall-through mặc định.

❌ Nhầm 2: Tưởng case nhận biến.

Java
int threshold = 10;
switch (x) {
    case threshold: ...  // COMPILE ERROR — case can constant
}

✅ case chỉ nhận hằng số compile-time: literal, final variable được biết trước lúc compile, enum value.

❌ Nhầm 3: Switch trên String null (Java < 21).

Java
String s = null;
switch (s) { ... }  // nem NullPointerException

✅ Kiểm tra null trước; hoặc dùng Java 21 có case null.

❌ Nhầm 4: Dùng switch expression thiếu default cho kiểu không phải enum/sealed.

Java
int kind = readKind();
String label = switch (kind) {
    case 1 -> "A";
    case 2 -> "B";
    // COMPILE ERROR — kind la int, khong exhaustive, can default
};

✅ Thêm default -> hoặc default -> throw new IllegalStateException(...).

8. Tài liệu Deep Dive Oracle quy định gì về cấu trúc switch?

📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: JEP 361 là bước ngoặt — switch từ statement-only thành expression, mở đường cho pattern matching. JVMS §3.10 giải thích chi tiết cách compiler chọn instruction nào cho density khác nhau — worth đọc khi bạn viết code hot-path.

9. Điểm cốt lõi cần nhớ về switch trong Java là gì?

  • switch hoạt động như bảng tra cứu giá trị, tối ưu bytecode bằng tableswitch (O(1)) hoặc lookupswitch (O(log n)).
  • Switch statement cổ điển dễ sinh lỗi vì cơ chế fall-through ngầm định khi thiếu lệnh break.
  • Switch expression (case L ->, Java 14+) là chuẩn mặc định cho code mới nhờ an toàn fall-through và exhaustive check.
  • Từ Java 21, pattern matching cho phép switch kiểm tra trực tiếp kiểu dữ liệu, guard condition when, và xử lý tường minh case null.

10. Tự kiểm tra kiến thức về cấu trúc switch như thế nào?

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Đoạn sau in gì?
    int x = 2;
    switch (x) {
        case 1:
            System.out.println("A");
        case 2:
            System.out.println("B");
        case 3:
            System.out.println("C");
            break;
        default:
            System.out.println("D");
    }
  2. Q2
    Viết lại đoạn trên bằng switch expression trả về String. Sau đó giải thích 2 ưu điểm so với bản statement.
  3. Q3
    Vì sao không switch được trên double hay long?
  4. Q4
    Khi nào compiler sinh tableswitch, khi nào sinh lookupswitch?
  5. Q5
    Đoạn sau compile không?
    enum Color { RED, GREEN, BLUE }
    Color c = Color.RED;
    String hex = switch (c) {
        case RED -> "#FF0000";
        case GREEN -> "#00FF00";
    };

Bài tiếp theo: while và do-while — vòng lặp có điều kiện

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 2/21 · Lập trình từ số 0 với Java — bài 21/32 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

while và do-while — Lặp khi chưa biết trước số lần