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

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

Hiểu cấu trúc if/else trong Java: cơ chế rẽ nhánh bytecode, short-circuit an toàn, giải quyết lỗi dangling-else, toán tử ba ngôi và kỹ thuật guard clauses.

TL;DR: Cấu trúc if, else if, else điều khiển luồng thực thi dựa trên biểu thức kiểu boolean. Tại tầng bytecode, JVM chuyển đổi điều kiện thành các lệnh nhảy có điều kiện (ifeq, if_icmpne). Kết hợp toán tử logic && và || với cơ chế short-circuit giúp phòng tránh lỗi NullPointerException. Luôn sử dụng cặp dấu ngoặc nhọn {} cho mọi khối lệnh để tránh lỗi dangling-else và bug nổi tiếng "goto fail", đồng thời áp dụng mẫu hình Guard Clauses để làm phẳng các cấu trúc if lồng nhau phức tạp.

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ử logic short-circuit, 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 mô tả cấu trúc rẽ nhánh như thế nào?

Đoạn code tuần tự giống con đường thẳng — bạn đi từ đầu đến cuối. if là ngã ba: tùy điều kiện mà rẽ nhánh này hay nhánh kia. else if là ngã tư nối tiếp — rẽ sai nhánh đầu thì thử nhánh tiếp theo. else là đườ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 của if, else if và else hoạt động ra sao?

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

Java
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:

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

Java
// 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. Trình ảo hóa JVM thực thi if/else ở tầng bytecode như thế nào?

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ụ:

Java
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ự.

Thử ngẫmstack trace chỉ ghi số dòng, không ghi "lỗi ở nhánh else". Biết if thực ra là một cú nhảy có điều kiện tới L_ELSE, bạn đọc dòng lỗi khác đi thế nào khi cả hai nhánh cùng gọi một hàm bị crash?

4. Cơ chế short-circuit bảo vệ điều kiện if an toàn ra sao?

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

&& và || là 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:

Java
// 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à `|`

& 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:

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

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

Thử ngẫmnếu bạn đảo thứ tự hai vế trong user != null && user.getAge() >= 18 thành chiều ngược lại, kết quả Boolean cuối cùng có đổi không, và điều gì thật sự đổi?

5. Toán tử ba ngôi thay thế if/else trong trường hợp nào?

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:

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

Java
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:
    Java
    // Anti-pattern
    String s = a ? (b ? "ab" : "a") : (c ? "c" : "none");
    
  • Có side effect trong nhánh (gọi method thay đổi state):
    Java
    // Kho debug
    x = cond ? doOne() : doTwo();
    
  • Block có 2+ statement — ternary chỉ cho expression, không cho statement.
⚠️ Autoboxing bẫy với ternary
Java
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. Lỗi dangling-else xảy ra khi nào và cách khắc phục là gì?

Java
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 { }:

Java
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 { }.

Thử ngẫmđang review một if lồng nhau không dấu ngoặc, thụt lề gợi ý else thuộc if ngoài cùng — bạn tin theo mắt hay theo quy tắc "gắn với if gần nhất"?

7. Kết hợp if với Pattern Matching instanceof hoạt động ra sao?

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

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

Java
// 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 &&:

Java
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. Tại sao cấu trúc if lồng nhau sâu lại là dấu hiệu cần refactor?

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

Java
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. Các bẫy pitfall thường gặp với cấu trúc if/else là gì?

❌ 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 ==.

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

Java
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ờ.

Java
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. Tài liệu Deep Dive Oracle quy định gì về rẽ nhánh có điều kiện?

📚 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. Điểm cốt lõi cần nhớ về cấu trúc if/else là gì?

  • Biểu thức boolean: Java bắt buộc điều kiện trong if phải là boolean, không chấp nhận số nguyên như C/C++.
  • Luôn dùng {}: Bao bọc khối lệnh bằng ngoặc nhọn để ngăn ngừa lỗi dangling-else và bug "goto fail".
  • Short-circuit: Tận dụng && và || để kiểm tra điều kiện an toàn, chống ngoại lệ NullPointerException.
  • Guard Clauses: Dùng return sớm để làm phẳng các khối if lồng nhau sâu hơn 3 tầng.

12. Tự kiểm tra kiến thức về cấu trúc rẽ nhánh if/else như thế nào?

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 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 20/32 của chặng

Thảo luận & góp ý

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

Bài tiếp theo

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