Java — Từ Zero đến Senior/Điều kiện & Vòng lặp/switch — từ statement cổ điển đến expression hiện đại
2/7
~20 phútĐiều kiện & Vòng lặp

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

Phân biệt switch statement (có fall-through, break) và switch expression (Java 14+, an toàn hơn, trả giá trị). Hiểu cơ chế tableswitch / lookupswitch ở bytecode và pattern matching Java 21.

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ị, đế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

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
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ớ

switchbả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. switch statement cổ điển

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 breakfall-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

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.

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.

3. switch expression (Java 14+) — an toàn hơn, trả giá trị

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

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ộpcase 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 { }yield:

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 returnreturn thoát khỏi method, yield chỉ thoát khỏi switch.

3.2 Exhaustive check với enum

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

4. Cơ chế bên dưới — tableswitch vs lookupswitch

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

  • tableswitch — dùng khi các casesố 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).

// "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:

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. Pattern matching cho switch (Java 21)

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

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.

6. Khi nào dùng switch, khi nào dùng if?

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. Pitfall tổng hợp

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

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.

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

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.

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. 📚 Deep Dive Oracle

ℹ️ 📚 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. Tóm tắt

  • switch = bảng tra cứu từ giá trị đến hành động. JVM có tableswitch / lookupswitch — thường nhanh hơn chuỗi if/else if dài.
  • Statement cổ điển (case L:) có fall-through — quên break là bug. Chỉ dùng với code legacy.
  • switch expression (case L ->, Java 14+) — không fall-through, trả giá trị, exhaustive check với enum. Default cho code mới.
  • yield trong case block trả giá trị cho switch expression; khác return (thoát method).
  • Biểu thức switch hỗ trợ: byte/short/int/char/String/enum. Không long/float/double/boolean.
  • Java 21 thêm pattern matching — case theo kiểu + when guard + case null.
  • Nếu biểu thức có range / điều kiện phức → dùng if/else if. Nếu chỉ so hằng số → switch gọn hơn.

10. Tự kiểm tra

Tự kiểm tra
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");
}

In B rồi C.

x = 2 khớp case 2 → in B. Không có break ở case 2 → fall-through sang case 3 → in C. Case 3 có break → thoát switch. Case default không chạy vì đã thoát. Chính vì fall-through kiểu này dễ gây bug nên Java 14 giới thiệu case L -> ... không fall-through.

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.
String s = switch (x) {
    case 1 -> "A";
    case 2 -> "B";
    case 3 -> "C";
    default -> "D";
};
  • An toàn hơn: không fall-through, không cần break → không có class bug kiểu "quên break".
  • Gán trực tiếp: switch trở thành expression trả giá trị, gán vào s trong 1 dòng. Bản statement phải khai báo biến s trước rồi gán trong từng case — dài hơn và dễ quên gán.
  • Exhaustive check: với enum/sealed, compiler báo lỗi nếu thiếu case, buộc bạn update khi thêm value mới.
Q3
Vì sao không switch được trên double hay long?

JLS §14.11 giới hạn selector của switch ở byte/short/int/char (và các kiểu tham chiếu tương đương như String, enum, Integer...).

  • long bị loại vì bytecode tableswitch/lookupswitch nhận int 32-bit, không hỗ trợ 64-bit trực tiếp. Mở rộng chỉ cho 1 kiểu không đáng.
  • float/double bị loại vì so sánh float có những edge case (NaN, -0.0 vs +0.0) khiến equality không transitive → không dùng được cho bảng hash/tree ổn định.

Với các kiểu đó, dùng if/else if.

Q4
Khi nào compiler sinh tableswitch, khi nào sinh lookupswitch?
Compiler phân tích mật độ các case value. Nếu các case là số nguyên gần liên tiếp (vd 1–10 hoặc 100–120), sinh tableswitch — bảng dense, nhảy theo offset O(1). Nếu các case thưa (vd 1, 50, 1000), tableswitch tốn memory vô ích → sinh lookupswitch — bảng sorted key, tra bằng binary search O(log n). Ngưỡng chuyển đổi do javac quyết định dựa trên ratio số case / range — bạn không điều khiển trực tiếp, nhưng hiểu để biết vì sao switch thường nhanh hơn if/else if chuỗi dài.
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";
};
Không. Switch expression yêu cầu exhaustive — phủ hết các giá trị có thể có. Enum Color có 3 value (RED, GREEN, BLUE) nhưng bạn chỉ cover 2 — compiler báo "the switch expression does not cover all possible input values". Fix: thêm case BLUE -> "#0000FF"; hoặc default -> .... Đây là tính năng mạnh — khi thêm YELLOW vào enum, compiler bắt lỗi ngay tất cả switch không update; refactor an toàn.

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