Java Foundations/if / else / else if — rẽ nhánh theo điều kiện
20/39
Bài 20 / 39~18 phútĐiều kiện & Vòng lặpMiễn phí lượt xem

if / else / else if — rẽ nhánh theo điều kiện

Hiểu if/else trong Java từ cú pháp đến cơ chế short-circuit, dangling-else, ternary operator, và vì sao nested if sâu là dấu hiệu cần refactor.

Chương trình từ đầu Module 1 đến giờ đều chạy tuần tự — dòng 1 xong tới dòng 2. Nhưng thực tế phần mềm luôn phải quyết định: nếu user chưa đăng nhập thì redirect login; nếu BMI < 18.5 thì phân loại "Gầy"; nếu số âm thì báo lỗi.

Bài này giải thích cơ chế rẽ nhánh trong Java: cú pháp if/else/else if, cách JVM thực thi, toán tử ba ngôi, và những bẫy tinh vi mà hầu hết tutorial bỏ qua.

1. Analogy — ngã ba đường

Đoạn code tuần tự giống con đường thẳng — bạn đi từ đầu đến cuối. ifngã ba: tùy điều kiện mà rẽ nhánh này hay nhánh kia. else ifngã tư nối tiếp — rẽ sai nhánh đầu thì thử nhánh tiếp theo. elseđường chốt cuối — mọi nhánh khác đều đổ về đây.

Đời thườngif / else if / else
Ngã ba, chọn rẽ trái nếu có biển "Bệnh viện"if (hasHospitalSign)
Ngã tư tiếp theo, chọn rẽ phải nếu có biển "Trường học"else if (hasSchoolSign)
Không có biển nào — đi thẳng về nhàelse
💡 Cách nhớ

if kiểm tra điều kiện đúng/sai. Đúng → chạy block của if. Sai → bỏ qua, thử nhánh else if kế tiếp. Không nhánh nào đúng → rơi vào else. JVM chỉ chạy đúng 1 nhánh trong chuỗi.

2. Cú pháp cơ bản

int score = 75;

if (score >= 90) {
    System.out.println("Excellent");
} else if (score >= 80) {
    System.out.println("Good");
} else if (score >= 65) {
    System.out.println("Fair");
} else if (score >= 50) {
    System.out.println("Pass");
} else {
    System.out.println("Fail");
}

Với score = 75, JVM xét lần lượt từ trên xuống rồi dừng ở điều kiện khớp đầu tiên. Hai ngưỡng phía dưới không phải "xét rồi trượt" — chúng không được chạm tới lần nào:

Chuỗi else if xét bốn ngưỡng điểm từ trên xuống, nhánh khớp đầu tiên chạy rồi nhảy thẳng ra sau khối, các điều kiện phía dưới không được xét

Hệ quả thực dụng: thứ tự các điều kiện là một phần của logic. Đảo score >= 50 lên đầu thì mọi ngưỡng cao hơn phía dưới thành code chết — không compile error, không warning, chỉ là điểm 95 cũng ra "Pass".

Điều kiện bên trong if (...) phải là boolean. Đây là điểm khác C/C++ — Java không cho if (5) hay if (ptr).

int x = 5;
if (x) { ... }        // COMPILE ERROR — x khong phai boolean
if (x != 0) { ... }   // OK — bieu thuc ra boolean

2.1 Khối lệnh (block) — nên dùng { } kể cả với 1 dòng

Java cho phép bỏ { } nếu body chỉ có 1 statement:

if (x > 0)
    System.out.println("Positive");

Không làm thế. Lịch sử đã chứng kiến Apple "goto fail" bug (2014) — một dòng goto fail; thừa nằm "ngoài" if do không có { }, làm sập toàn bộ SSL validation của macOS/iOS trong nhiều tháng.

// Bug phong cach "goto fail"
if (isLoggedIn)
    logInfo("login ok");
    updateLastSeen();  // dong nay LUON chay, du chua dang nhap!

Thụt lề lừa mắt, nhưng updateLastSeen() không nằm trong if — nó là statement độc lập sau if.

Luôn viết { } dù body chỉ 1 dòng. Không tốn thêm gì, phòng bug về sau.

3. Cơ chế bên dưới — JVM làm gì với if?

Java không có thực sự "nhánh if" ở cấp bytecode. Compiler biên dịch if thành instruction nhảy có điều kiện — kiểm tra boolean rồi goto tới địa chỉ khác.

Ví dụ:

int x = 10;
if (x > 5) {
    System.out.println("big");
} else {
    System.out.println("small");
}

Xem bytecode với javap -c:

bipush 10
istore_1            // x = 10
iload_1
iconst_5
if_icmple  L_ELSE   // neu x <= 5 thi nhay toi L_ELSE
// nhanh if:
ldc "big"
invokevirtual println
goto L_END          // nhay qua nhanh else
L_ELSE:
ldc "small"
invokevirtual println
L_END:
return

Chú ý: compiler đảo ngược điều kiện. Bạn viết if (x > 5), bytecode là if (x <= 5) goto else. Vì CPU nhánh xử lý "nhảy khi sai, fall-through khi đúng" rẻ hơn 1 lệnh so với "nhảy khi đúng".

📚 Tại sao đáng biết?

Hiểu if compile thành goto có điều kiện giúp bạn hiểu tại sao branch prediction ảnh hưởng performance, tại sao switch với nhiều case có thể nhanh hơn chuỗi if/else if dài (tableswitch / lookupswitch — xem bài sau), và vì sao loop unrolling lại là technique tối ưu thật sự.

4. Short-circuit — &&|| không đánh giá hết

if (obj != null && obj.isValid()) {
    // an toan — neu obj == null, obj.isValid() KHONG duoc goi
}

&&||short-circuit operator:

  • A && B — nếu A = false, không đánh giá B, kết quả false.
  • A || B — nếu A = true, không đánh giá B, kết quả true.

Đây là cơ chế giúp kiểm tra null an toàn, tránh NullPointerException:

// An toan
if (user != null && user.getAge() >= 18) { ... }

// Nguy hiem — neu user == null, user.getAge() nem NPE
if (user.getAge() >= 18 && user != null) { ... }
⚠️ Đừng nhầm với `&` và `|`

&| (không short-circuit) là bitwise AND/OR với số nguyên, và là logical AND/OR đánh giá cả 2 vế với boolean. Ít khi muốn dùng cho điều kiện:

if (a != null & a.doSomething()) { ... }  // SAI — a.doSomething() van chay khi a null

Dùng &&|| cho mọi điều kiện, dù compiler cho phép & với boolean.

5. Toán tử ba ngôi ? : — if/else rút gọn cho biểu thức

Khi bạn cần chọn 1 trong 2 giá trị để gán hoặc return, ternary ngắn gọn hơn nhiều:

// Cach dai
String status;
if (score >= 50) {
    status = "Pass";
} else {
    status = "Fail";
}

// Cach ngan
String status = (score >= 50) ? "Pass" : "Fail";

Cú pháp: <dieu kien> ? <gia tri neu dung> : <gia tri neu sai>. Cả 2 giá trị phải cùng kiểu (hoặc có quan hệ kế thừa / autoboxing tương thích).

5.1 Khi nào dùng, khi nào tránh?

Dùng khi chọn 1 trong 2 giá trị đơn giản:

int absX = (x >= 0) ? x : -x;
String label = (isVip) ? "VIP" : "Guest";
return (list == null) ? "[]" : list.toString();

Tránh khi:

  • Nested ternary 3+ tầng — đọc không ra:
    // Anti-pattern
    String s = a ? (b ? "ab" : "a") : (c ? "c" : "none");
    
  • Có side effect trong nhánh (gọi method thay đổi state):
    // Kho debug
    x = cond ? doOne() : doTwo();
    
  • Block có 2+ statement — ternary chỉ cho expression, không cho statement.
⚠️ Autoboxing bẫy với ternary
Integer i = null;
int x = (flag) ? i : 0;  // NEM NullPointerException neu flag = true

Java thấy nhánh kia là int, nên unbox Integer i về int — nếu i = null, NPE ngay. Dễ sót vì compile không cảnh báo.

6. Dangling-else — else gắn với if nào?

if (a > 0)
    if (b > 0)
        System.out.println("both positive");
    else
        System.out.println("???");

else gắn với if gần nhất — tức if (b > 0). Thụt lề đánh lừa mắt, nhưng không đổi ngữ nghĩa.

Nếu muốn else gắn với if (a > 0), bắt buộc { }:

if (a > 0) {
    if (b > 0) {
        System.out.println("both positive");
    }
} else {
    System.out.println("a <= 0");
}

Lại thêm một lý do luôn dùng { }.

7. Pattern match với instanceof (Java 16+)

Trước Java 16, kiểm tra kiểu phải cast riêng:

// Style cu (truoc Java 16)
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

Từ Java 16 (JEP 394), bạn có thể bind biến ngay trong instanceof:

// Pattern matching (Java 16+)
if (obj instanceof String s) {
    // s duoc bind tu dong, scope trong block if
    System.out.println(s.length());
}

Thậm chí kết hợp với &&:

if (obj instanceof String s && s.length() > 0) {
    // s scope trong bieu thuc && ben phai va trong block if
    System.out.println(s.charAt(0));
}

Ở module OOP / Pattern Matching chúng ta sẽ đào sâu thêm; giờ đủ để dùng.

8. Nested if sâu — dấu hiệu cần refactor

// Anti-pattern — pyramid of doom
if (user != null) {
    if (user.isActive()) {
        if (user.getPlan() != null) {
            if (user.getPlan().isPremium()) {
                // lam gi do sau 4 tang nested
            }
        }
    }
}

Guard clause (early return) làm phẳng code, dễ đọc hơn:

void process(User user) {
    if (user == null)                      return;
    if (!user.isActive())                  return;
    if (user.getPlan() == null)            return;
    if (!user.getPlan().isPremium())       return;

    // toan bo logic chinh o day, khong bi thut le sau
}

Đảo điều kiện và return sớm mỗi khi không hợp lệ. "Happy path" nằm ở cuối, không bị thụt lề.

💡 Rule of thumb

Nếu nested if quá 3 tầng, dừng tay refactor ngay: guard clause, Optional, hoặc tách method nhỏ. Reader trả lương cho bạn là bạn kia (hoặc chính bạn 6 tháng sau).

9. Pitfall tổng hợp

Nhầm 1: if (x = 5) — gán thay vì so sánh. ✅ Java chặn trường hợp này với kiểu non-boolean (compile error), nhưng với boolean thì qua lọt: if (flag = true) compile ok, luôn đúng → bug khó thấy. Dùng ==: if (x == 5).

Nhầm 2: So sánh String bằng ==.

if (status == "ACTIVE") { ... }  // lam viec voi literal, vo voi String tu Scanner

✅ Luôn "ACTIVE".equals(status) — đặt literal bên trái để null-safe.

Nhầm 3: Quên { } rồi thêm dòng thứ 2.

if (retry)
    logInfo("retry");
    callApi();  // LUON chay du retry = false

✅ Luôn { }.

Nhầm 4: Ternary với kiểu khác nhau gây auto-conversion bất ngờ.

Object o = flag ? 1 : "text";  // ca 2 nhanh autobox / wrap thanh Object
int x = flag ? 1 : 1.5;        // x thanh double, loi compile neu kho gan

✅ Đảm bảo 2 nhánh cùng kiểu hoặc cast rõ ràng.

10. 📚 Deep Dive Oracle

📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: JLS quy định dangling-else gắn với if gần nhất để tránh ambiguity — hầu hết ngôn ngữ C-family đồng thuận. JEP 394 giới thiệu "pattern variable" — scope khá tinh tế, worth đọc khi bạn viết nhiều code ADT-style.

11. Tóm tắt

  • if/else if/else rẽ nhánh theo điều kiện boolean. Java chỉ nhận boolean, không nhận int != 0 như C.
  • Luôn dùng { } kể cả cho body 1 dòng — phòng "goto fail" class of bugs.
  • &&||short-circuit — dùng chúng để null-check an toàn.
  • Ternary ? : tốt cho chọn 1 trong 2 giá trị. Nested / có side effect thì nên trả về if/else.
  • Bytecode của if là instruction nhảy có điều kiện — compiler đảo ngược điều kiện để tối ưu fall-through.
  • Nested if 3+ tầng → refactor thành guard clause.
  • Java 16+: instanceof T s bind biến luôn, không cần cast rời.

12. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Đoạn sau in gì và vì sao?
    int x = 5;
    if (x > 0)
        System.out.println("a");
        System.out.println("b");
    System.out.println("c");
  2. Q2
    Vì sao if (user != null && user.getAge() >= 18) an toàn, còn đảo ngược lại thì không?
  3. Q3
    Viết ternary tính giá trị tuyệt đối của int x. Sau đó giải thích tại sao cách này không an toàn với Integer.MIN_VALUE.
  4. Q4
    Khi nào ternary ? : là lựa chọn tốt hơn if/else, và khi nào ngược lại?
  5. Q5
    Đoạn sau compile không? Vì sao?
    int x = 10;
    if (x) {
        System.out.println("non-zero");
    }

Bài tiếp theo: switch — từ statement cổ điển đến expression hiện đại

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.

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

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

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

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