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. 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ường | if / 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");
}
Đ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 — && và || không đánh giá hết
if (obj != null && obj.isValid()) {
// an toan — neu obj == null, obj.isValid() KHONG duoc goi
}
&& và || là short-circuit operator:
A && B— nếuA=false, không đánh giáB, kết quảfalse.A || B— nếuA=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à `|`
& 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 && và || 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:
- JLS §14.9 — The if Statement — cú pháp, quy tắc dangling-else.
- JLS §15.25 — Conditional Operator ? : — kiểu của biểu thức ternary, rules autoboxing/numeric promotion.
- JLS §15.24 — Conditional-Or / §15.23 — Conditional-And — quy tắc short-circuit.
- JEP 394 — Pattern Matching for instanceof — final trong Java 16.
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/elserẽ nhánh theo điều kiệnboolean. Java chỉ nhận boolean, không nhậnint != 0như C.- Luôn dùng
{ }kể cả cho body 1 dòng — phòng "goto fail" class of bugs. &&và||là 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
iflà instruction nhảy có điều kiện — compiler đảo ngược điều kiện để tối ưu fall-through. - Nested
if3+ tầng → refactor thành guard clause. - Java 16+:
instanceof T sbind biến luôn, không cần cast rời.
12. Tự kiểm tra
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");
▸
int x = 5;
if (x > 0)
System.out.println("a");
System.out.println("b");
System.out.println("c");In a, b, c — cả 3 dòng.
if không có { } nên body chỉ là 1 statement: dòng System.out.println("a"). Hai dòng "b" và "c" nằm ngoài if, thuộc luồng chính, nên luôn chạy bất kể x dương hay âm. Thụt lề lừa mắt — đây chính là class of bug "goto fail" của Apple. Bài học: luôn viết { }.
Q2Vì sao if (user != null && user.getAge() >= 18) an toàn, còn đảo ngược lại thì không?▸
if (user != null && user.getAge() >= 18) an toàn, còn đảo ngược lại thì không?&& là short-circuit: nếu vế trái false, vế phải không được đánh giá. Khi user == null, user != null trả false ngay → JVM bỏ qua user.getAge() → không NPE. Nếu đảo thành user.getAge() >= 18 && user != null, khi user == null, JVM gọi user.getAge() trước — ném NullPointerException trước cả khi kiểm tra null.Q3Viế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.▸
int x. Sau đó giải thích tại sao cách này không an toàn với Integer.MIN_VALUE.int abs = (x >= 0) ? x : -x;
Vấn đề: Integer.MIN_VALUE = -2147483648. Negate nó → +2147483648, nhưng int max là 2147483647 → integer overflow → kết quả lại là -2147483648 (âm). Math.abs(Integer.MIN_VALUE) cũng gặp vấn đề này, có ghi chú trong Javadoc. Giải pháp đúng: dùng Math.absExact(x) (Java 15+) để ném exception thay vì silently overflow, hoặc dùng long.
Q4Khi nào ternary ? : là lựa chọn tốt hơn if/else, và khi nào ngược lại?▸
? : là lựa chọn tốt hơn if/else, và khi nào ngược lại?- Ternary tốt hơn khi chọn 1 trong 2 giá trị đơn giản để gán / return. Ngắn hơn, ít biến local, expression dùng được trong context cần expression (vd init field, argument method).
- if/else tốt hơn khi mỗi nhánh có nhiều statement, có side effect, hoặc có 3+ điều kiện. Debugger cũng đặt breakpoint dễ hơn từng nhánh.
- Rule: nếu bạn định đặt ternary trong ternary (3 tầng), dừng lại. Chuyển sang
if/else ifhoặcswitchcho dễ đọc.
Q5Đoạn sau compile không? Vì sao?int x = 10;
if (x) {
System.out.println("non-zero");
}
▸
int x = 10;
if (x) {
System.out.println("non-zero");
}if (...) phải là boolean (JLS §14.9), khác với C/C++ nhận mọi số với semantics != 0. Phải viết tường minh: if (x != 0). Rule này loại cả class bug kiểu if (x = 5) — gán trả int không phải boolean, compiler từ chối. Với if (flag = true) thì lại lọt — đây là một trong vài case nên tránh gán trong điều kiện.Bài tiếp theo: switch — từ statement cổ điển đến expression hiện đại