Conditionals are how a program chooses. Java gives you three tools — if, the ternary,
and switch — and switch changed substantially in recent versions, in a way
that removed the language's most notorious source of accidental bugs.
if, else if, else
int score = 85;
if (score >= 90) {
System.out.println("A");
} else if (score >= 80) {
System.out.println("B");
} else if (score >= 70) {
System.out.println("C");
} else {
System.out.println("F");
}
The branches are tested top to bottom and the first match wins — which is why the
order above works with only one comparison per branch. Reverse the order and every score would match
>= 70 first.
The condition must be a boolean. Unlike C or JavaScript, Java has no truthiness: an
int, a String or an object in an if is a compile error, not a
shortcut. This removes the classic if (x = 5) assignment typo entirely.
Always use braces, even for one line. This is not pedantry — it is the shape of one of the most expensive bugs ever shipped:
boolean valid = false;
// The second line is NOT part of the if. It always runs.
if (valid)
System.out.println("ok");
System.out.println("this always prints");
Guard clauses beat nesting
When you find yourself three levels deep, invert the conditions and return early. The logic is identical and the reader never has to hold a stack of conditions in their head:
record Order(String id, boolean paid, int quantity) { }
// Nested — the real work is buried at the deepest indentation
String processNested(Order order) {
if (order != null) {
if (order.paid()) {
if (order.quantity() > 0) {
return "shipping " + order.id();
} else {
return "nothing to ship";
}
} else {
return "unpaid";
}
} else {
return "no order";
}
}
// Guard clauses — each failure handled and dismissed, the happy path last and flat
String processFlat(Order order) {
if (order == null) return "no order";
if (!order.paid()) return "unpaid";
if (order.quantity() <= 0) return "nothing to ship";
return "shipping " + order.id();
}
The ternary
int age = 20;
String status = age >= 18 ? "adult" : "minor";
// An if-statement that only assigns one variable is a ternary waiting to happen
String label;
if (age >= 18) { label = "adult"; } else { label = "minor"; }
Use it when you are producing a value. Do not nest it — a nested ternary is an if
chain written to be hard to read.
switch: the old form and its trap
The classic syntax uses case labels with colons, and it falls through: once
a case matches, execution continues into the following cases until it hits a break.
int day = 3;
switch (day) {
case 1:
System.out.println("Monday");
break; // forget this...
case 2:
System.out.println("Tuesday");
break;
case 3:
System.out.println("Wednesday");
break;
default:
System.out.println("Other");
}
A missing break is not an error. The compiler accepts it, and the program runs several
branches instead of one. Decades of Java bugs trace back to exactly that.
The arrow form, which fixes it
Since Java 14 you can write -> instead of :. Only the matching branch
runs; there is no fall-through and no break to forget:
int day = 3;
switch (day) {
case 1 -> System.out.println("Monday");
case 2 -> System.out.println("Tuesday");
case 3 -> System.out.println("Wednesday");
default -> System.out.println("Other");
}
Use the arrow form for all new code. The colon form is worth recognising because you will read it in older codebases, but there is no reason to write it.
Switch as an expression
The bigger improvement: a switch can now produce a value rather than perform
statements. Combine several labels with a comma, and use yield when a branch needs a
block:
int day = 6;
String type = switch (day) {
case 1, 2, 3, 4, 5 -> "weekday";
case 6, 7 -> "weekend";
default -> "invalid";
};
int daysInMonth = switch (2) {
case 1, 3, 5, 7, 8, 10, 12 -> 31;
case 4, 6, 9, 11 -> 30;
case 2 -> {
boolean leapYear = true;
yield leapYear ? 29 : 28; // yield returns a value from a block
}
default -> throw new IllegalArgumentException("bad month");
};
A switch expression must be exhaustive — every possible input has to be covered,
or it will not compile. That is a genuine safety gain: with an enum, adding a new
constant turns every switch that forgot to handle it into a compile error rather than a silent
default.
enum Status { PENDING, SHIPPED, DELIVERED }
String describe(Status status) {
return switch (status) { // no default needed — all three are covered
case PENDING -> "waiting";
case SHIPPED -> "on its way";
case DELIVERED -> "arrived";
};
}
Switching on types
Since Java 21, a case can match a type and bind a variable in one step, which removes
a whole ladder of instanceof and casts:
String describe(Object value) {
return switch (value) {
case Integer i when i > 100 -> "big number: " + i;
case Integer i -> "number: " + i;
case String s -> "text of length " + s.length();
case null -> "nothing";
default -> "something else";
};
}
Note when for an extra condition, and that case null is now allowed —
previously a switch on a null reference always threw. This gets considerably more
powerful with sealed types, which Sealed Classes covers.
A real one
From the console bank app this site uses for examples — an arrow switch dispatching a menu choice:
int choice = console.readChoice("Choose 1-6: ", 1, 6, 6);
// A switch over ints. Java 14+ arrow labels: no fall-through, no `break` to forget.
try {
switch (choice) {
case 1 -> viewAccounts(user);
case 2 -> deposit(user);
case 3 -> withdraw(user);
case 4 -> transfer(user);
case 5 -> history(user);
case 6 -> {
console.success("Signed out.");
return;
}
default -> console.error("That is not an option.");
}
} catch (BankException e) {
// Any rule the services enforce surfaces here as a message, and the loop continues.
// One handler for the whole menu beats a try/catch inside each case.
console.error(e.getMessage());
}
Note the braces on case 6: a branch that needs more than one statement gets a block.
And note the try wrapping the whole switch rather than each case — the point
Exception Handling makes about catching in one place.
Which to use
- Two or three branches on different conditions —
if. - Assigning one variable from a condition — the ternary.
- Many branches on one value — a
switchexpression with arrows. - Anything on an enum — a
switchexpression, so the compiler catches the constant you add next year.
Next
Conditions run a block once. Loops is next — running one many times, and the two mistakes everybody makes at least once.