if, else if, else — and Why Order Matters
An if statement runs a block of code only when a boolean condition is true. Java is strict here in a way C is not: the condition must be an actual boolean. You cannot write if (count) meaning "if count is non-zero" — that is a compile error, and it is a small mercy, because it prevents a whole family of bugs.
Chaining with else if creates a ladder, and the crucial property of a ladder is that Java stops at the first condition that is true and ignores every one below it. That is what makes the grading example below correct with such simple conditions: by the time control reaches score >= 80, we already know the score is under 90, so there is no need to write score >= 80 && score < 90.
The flip side is that order is part of the logic, not a matter of taste. Write the same ladder starting from the lowest boundary and every score of 95 will be reported as a pass at the 40 mark and nothing further will be checked. When a grading or pricing ladder gives wrong answers, the first thing to check is whether the conditions run from most specific to least specific.
A final else with no condition catches everything the ladder missed. Include one whenever "none of the above" is a real possibility, even if it only prints a message — silently doing nothing when the data is unexpected is how bugs hide.
int score = 85;
if (score >= 90) {
System.out.println("Grade: A");
} else if (score >= 80) { // reached only when score < 90
System.out.println("Grade: B");
} else if (score >= 70) {
System.out.println("Grade: C");
} else if (score >= 40) {
System.out.println("Grade: D");
} else {
System.out.println("Grade: F");
}
// Grade: B
// WRONG ORDER — every passing score reports "D"
// if (score >= 40) { grade = "D"; }
// else if (score >= 90) { grade = "A"; } // never reached
// Nesting: check the outer condition before the inner one makes sense
int attendance = 82;
if (attendance >= 75) {
if (score >= 40) {
System.out.println("Eligible for the exam and passing");
} else {
System.out.println("Eligible for the exam but needs to improve");
}
} else {
System.out.println("Not eligible — attendance below 75%");
} - Prefer an early
returnor a guard clause over deep nesting. Three levels of nestedifare hard to follow; checking the failure cases first and returning from each turns the same logic into a flat list of rules.
Four Mistakes That Compile Perfectly
The dangerous conditional bugs are the ones the compiler accepts. Each of the four below produces a program that runs and gives wrong answers, which is much harder to find than a program that refuses to build.
A stray semicolon. if (score > 90); ends the if statement immediately with an empty body. The block that follows is then an ordinary block of code that always runs, regardless of the condition. Java allows it, and the indentation makes it invisible to the eye.
Assignment instead of comparison. With numbers, if (x = 5) is caught at compile time because an int is not a boolean. With booleans it compiles: if (isActive = true) assigns true to isActive and then tests it, so the branch always runs and your variable has been quietly overwritten. This is a good reason to write if (isActive) rather than if (isActive == true) in the first place.
Missing braces. Java allows a single statement after if without braces. Add a second line later, indent it to match, and it runs unconditionally. Always use braces, even for one line — it costs two characters and removes the whole class of bug.
Comparing the wrong way. == on Strings compares identity rather than contents, as covered earlier, and == on doubles fails on values that look equal because of binary rounding. Compare strings with .equals(), and compare doubles by checking whether the difference is smaller than a tiny tolerance.
int score = 50;
// 1. Stray semicolon — the block always runs
if (score > 90);
{
System.out.println("Distinction"); // prints even for 50
}
// 2. Assignment inside a boolean condition
boolean isActive = false;
if (isActive = true) { // assigns, then tests -> always true
System.out.println("Active");
}
System.out.println(isActive); // true — the variable was changed!
// Write it this way instead
if (isActive) { /* ... */ }
if (!isActive) { /* ... */ }
// 3. Missing braces
// if (score >= 40)
// System.out.println("Pass");
// System.out.println("Certificate issued"); // ALWAYS runs
// 4. Comparing the wrong way
String status = new String("PASS");
// if (status == "PASS") // false — compares references
if (status.equals("PASS")) { } // correct
double total = 0.1 + 0.2;
// if (total == 0.3) // false!
if (Math.abs(total - 0.3) < 1e-9) { // correct: compare within a tolerance
System.out.println("Close enough");
} - When an
ifbehaves as though the condition is being ignored, print the condition itself:System.out.println("cond = " + (score > 90));. Seeing the actual boolean value takes ten seconds and settles the question.
The Classic switch Statement
When you are comparing one variable against a list of fixed values, a ladder of else if repeats the variable name on every line. switch says it once. It works on int, short, byte, char, their wrapper classes, String (since Java 7) and enum types. It does not work on long, double, float or boolean, and it cannot express a range such as score >= 90 — those need an if ladder.
The behaviour that defines the classic form is fall-through. Once a case matches, execution continues straight down through every following case until it meets a break or reaches the end of the switch. Forgetting a break is the most common switch bug: enter one case and the program executes three.
Fall-through is not purely a hazard — it is also how you group several values that share an action, by stacking case labels with no code between them. The example below uses that deliberately. But when you fall through after running code, always leave a comment saying so, or the next reader will assume it is a bug and add the break back.
default handles anything not listed. Convention puts it last, and although it is optional, leaving it out means unexpected values pass through the switch silently.
String day = "Saturday";
switch (day) {
case "Monday":
case "Tuesday":
case "Wednesday":
case "Thursday":
case "Friday":
System.out.println("Weekday — classes as usual");
break; // without this, it falls into Saturday
case "Saturday":
case "Sunday":
System.out.println("Weekend — no classes");
break;
default:
System.out.println("Not a day name: " + day);
}
// Fall-through as a BUG
int menuChoice = 1;
switch (menuChoice) {
case 1:
System.out.println("Deposit");
// no break!
case 2:
System.out.println("Withdraw"); // this runs too
break;
}
// Deposit
// Withdraw - A classic
switchon aStringthat happens to benullthrows aNullPointerException—nulldoes not fall todefault. Check for null before the switch, or use the modern form described next.
Modern switch: Arrows and Expressions
Java 14 added an arrow form of switch that removes the fall-through problem entirely. Write case X -> something; and only that branch runs; there is no break to forget. Several values can share one arm by listing them separated by commas, which replaces the stacked-label trick.
The bigger change is that switch can now be an expression — it produces a value you assign to a variable. Previously you had to declare the variable above the switch, assign inside each case, and hope you had not missed one. As an expression the compiler checks for you: a switch expression must cover every possible input, so on a String or an int a default arm is required, and omitting it is a compile error rather than a runtime surprise. Note the semicolon after the closing brace — it is an expression, so the statement has to end.
When an arm needs more than one line, use a block and finish it with yield to supply the value. return will not work there, because you are returning a value from the switch, not from the enclosing method.
Java 21 extended this further with pattern matching: a case label can test the type of an object and bind it to a variable in one step, which removes long chains of instanceof plus casting. Use it only if your project is on Java 21 or newer.
String day = "Saturday";
// Arrow form — no fall-through, no break
switch (day) {
case "Saturday", "Sunday" -> System.out.println("Weekend");
default -> System.out.println("Weekday");
}
// switch as an EXPRESSION (Java 14+)
String type = switch (day) {
case "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday" -> "Weekday";
case "Saturday", "Sunday" -> "Weekend";
default -> "Unknown";
}; // <- semicolon: it is an expression
System.out.println(type); // Weekend
// Multi-line arm: use a block and yield
int month = 8;
int days = switch (month) {
case 1, 3, 5, 7, 8, 10, 12 -> 31;
case 4, 6, 9, 11 -> 30;
case 2 -> {
int year = 2026;
boolean leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
yield leap ? 29 : 28;
}
default -> throw new IllegalArgumentException("Bad month: " + month);
};
System.out.println(days); // 31
// Pattern matching in switch (Java 21+)
static String describe(Object o) {
return switch (o) {
case Integer i -> "a number: " + i;
case String s -> "text of length " + s.length();
default -> "something else";
};
} - You cannot mix arrow arms and colon arms in the same switch — it is one style or the other. In new code, prefer the arrow form everywhere.
Choosing Between if and switch
Both do the same job, so the choice is about which one makes the intent obvious to a reader. The short version: use switch when you are matching one value against a set of fixed alternatives, and if when you are testing ranges or combining several different conditions.
- Matching a menu choice, a status code, a command name or an enum value —
switchreads far better - Testing a range such as
marks >= 90, or comparing two different variables — onlyifcan do it - Combining conditions with
&&and||— onlyifcan do it - Assigning a value based on the alternatives — a switch expression is the cleanest option, and the compiler checks you covered every case
- Two alternatives and a short result — the ternary
? :beats both - More than about seven branches of either kind — consider whether the data belongs in a
Mapor in anenuminstead of in code
- There is a performance argument that
switchon integers compiles into a jump table and is faster than a longifladder. It is true, and it is irrelevant at the sizes you will write. Choose whichever is easier to read.
