Conditional Selection #
Conditional selection is the mechanism for directing a program’s execution flow based on certain conditions. Java provides several constructs for this, and the right choice affects code readability and safety. What’s interesting: Java keeps evolving its conditional selection constructs — switch, which started as a mere statement, can now be an expression (Java 14), and can be used for type pattern matching (Java 21). This article covers all the constructs from classic if-else to modern switch, including the anti-patterns that often cause bugs like unintentional fall-through and overly nested conditions.
if and if-else #
if executes a block of code only when the condition is true. The condition must be a boolean-typed expression — there’s no implicit conversion from int or object to boolean like in C or Python.
int stock = 5;
// Single if
if (stock == 0) {
System.out.println("Out of stock");
}
// if-else — two branches
if (stock > 0) {
System.out.println("Available: " + stock);
} else {
System.out.println("Out of stock");
}
// if-else if-else — many branches
int score = 78;
if (score >= 90) {
System.out.println("A");
} else if (score >= 80) {
System.out.println("B");
} else if (score >= 70) {
System.out.println("C");
} else if (score >= 60) {
System.out.println("D");
} else {
System.out.println("E");
}
flowchart TD
A("[Start]") --> B{score >= 90?}
B -- Yes --> C[Print A]
B -- No --> D{score >= 80?}
D -- Yes --> E[Print B]
D -- No --> F{score >= 70?}
F -- Yes --> G[Print C]
F -- No --> H{score >= 60?}
H -- Yes --> I[Print D]
H -- No --> J[Print E]
C & E & G & I & J --> K("[End]")Trap: if Without Curly Braces #
Java allows if without curly braces when there’s only one statement, but this is a classic source of bugs:
// ANTI-PATTERN: if without curly braces
if (debug)
System.out.println("entered here");
System.out.println("this ALWAYS runs"); // ✗ not part of the if!
// Famous bug: the Apple SSL/TLS "goto fail" happened because of this pattern
if (condition)
goto fail;
goto fail; // always executes!
// CORRECT: always use curly braces
if (debug) {
System.out.println("entered here");
}
Guard Clauses — Reduce Nesting #
Code with too much nesting (deeply nested) is hard to read. The guard clause pattern flips the conditions: handle special cases early and return, so the main logic stays at the top level.
// ANTI-PATTERN: deep nesting — "arrow code"
public String processPayment(Order order) {
if (order != null) {
if (order.isValid()) {
if (order.getBalance() >= order.getTotal()) {
if (!order.isAlreadyPaid()) {
// the main logic sits inside 4 levels of nesting
pay(order);
return "success";
} else {
return "already paid";
}
} else {
return "insufficient balance";
}
} else {
return "invalid order";
}
} else {
return "null order";
}
}
// CORRECT: guard clauses — handle special cases early, return immediately
public String processPayment(Order order) {
if (order == null) return "null order";
if (!order.isValid()) return "invalid order";
if (order.isAlreadyPaid()) return "already paid";
if (order.getBalance() < order.getTotal()) return "insufficient balance";
// the main logic at the top level, easy to read
pay(order);
return "success";
}
switch Statement (Classic) #
The switch statement matches a value of an expression against a series of cases. It’s more concise than long if-else if chains when comparing one variable against many fixed values.
int day = 3;
switch (day) {
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
case 3:
System.out.println("Wednesday");
break;
case 4:
System.out.println("Thursday");
break;
case 5:
System.out.println("Friday");
break;
default:
System.out.println("Weekend");
}
Types Supported by Classic switch #
| Type | Supported |
|---|---|
byte, short, int, char | ✓ |
Byte, Short, Integer, Character (wrappers) | ✓ |
String | ✓ (since Java 7) |
enum | ✓ |
long, float, double, boolean | ✗ |
Fall-Through: Feature or Bug? #
Without break, execution “falls through” to the next case (fall-through). It’s a feature that’s often unintentional:
// ANTI-PATTERN: forgotten break — unintentional fall-through
int code = 2;
switch (code) {
case 1:
System.out.println("one");
case 2:
System.out.println("two"); // printed
case 3:
System.out.println("three"); // ALSO printed because break was forgotten!
default:
System.out.println("other"); // ALSO printed!
}
// Output: two, three, other — though only "two" was expected
// INTENTIONAL fall-through — use comments to make it clear
switch (day) {
case 1: // fall-through
case 2: // fall-through
case 3: // fall-through
case 4: // fall-through
case 5:
System.out.println("Workday");
break;
case 6: // fall-through
case 7:
System.out.println("Weekend");
break;
}
switch Expression (Java 14+) #
Java 14 introduced switch as an expression that produces a value, with the -> syntax that eliminates both the fall-through and break problems at once. This is the recommended modern way.
// switch expression — produces a value directly
int day = 3;
String dayName = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> "Wednesday";
case 4 -> "Thursday";
case 5 -> "Friday";
case 6 -> "Saturday";
case 7 -> "Sunday";
default -> throw new IllegalArgumentException("Invalid day: " + day);
};
// Multiple values in a single case
String dayType = switch (day) {
case 1, 2, 3, 4, 5 -> "Workday";
case 6, 7 -> "Weekend";
default -> "Invalid";
};
// Use yield for more complex code blocks
String category = switch (score) {
case 1, 2 -> "low";
case 3 -> {
System.out.println("middle value processed");
yield "medium"; // yield replaces return inside a switch expression
}
default -> "high";
};
Comparing switch Statement vs switch Expression #
| Aspect | switch Statement | switch Expression |
|---|---|---|
| Produces a value | ✗ | ✓ |
| Fall-through default | Yes (dangerous) | None |
break required | Yes | No |
| Complex code blocks | Directly | Use yield |
| Java version | All | Java 14+ |
| Exhaustiveness | Not checked | Must be complete (enum/sealed) |
flowchart TD
A{"Need to\nproduce a value?"} -- Yes --> B["switch expression\ncase X -> value"]
A -- No --> C{"Many cases share logic\nwith intentional fall-through?"}
C -- Yes --> D["switch statement\nwith fall-through comments"]
C -- No --> E{"How many\nbranches?"}
E -- "2-3" --> F["if-else"]
E -- "4+" --> G["switch expression\nor switch statement"]
style B color:#fff,stroke:#16a34a,stroke-width:2px
style F color:#fff,stroke:#3b82f6,stroke-width:2px
style G color:#fff,stroke:#16a34a,stroke-width:2pxswitch with String and Enum #
switch works well with String (since Java 7) and enum, and both are safer than int:
// switch with String
String command = "start";
switch (command) {
case "start" -> System.out.println("Starting...");
case "stop" -> System.out.println("Stopping...");
case "pause" -> System.out.println("Pausing...");
default -> System.out.println("Unknown command: " + command);
}
// switch with enum — the most type-safe
enum OrderStatus { PENDING, PROCESSING, SHIPPED, COMPLETED, CANCELLED }
OrderStatus status = OrderStatus.SHIPPED;
String message = switch (status) {
case PENDING -> "Order awaiting confirmation";
case PROCESSING -> "Order being processed";
case SHIPPED -> "Order in transit";
case COMPLETED -> "Order delivered";
case CANCELLED -> "Order cancelled";
// No default needed! The compiler ensures all enums are handled
};
Usingswitchwithenumwithoutdefaultis a type safety advantage: if you add a new value to the enum but forget to add it to theswitch, the compiler immediately gives an error. WithintorString, adding a new value silently falls intodefaultand might go undetected.
Pattern Matching switch (Java 21) #
Java 21 takes switch to the next level: it can match object types while also binding — replacing long chains of if (x instanceof Type t).
// Without pattern matching — verbose and repetitive
static String describe(Object obj) {
if (obj instanceof Integer i) {
return "Integer: " + i;
} else if (obj instanceof String s) {
return "String of length " + s.length();
} else if (obj instanceof Double d) {
return "Double: " + d;
} else if (obj == null) {
return "null";
} else {
return "Other type: " + obj.getClass().getSimpleName();
}
}
// With pattern matching switch (Java 21) — concise and safe
static String describe(Object obj) {
return switch (obj) {
case Integer i -> "Integer: " + i;
case String s -> "String of length " + s.length();
case Double d -> "Double: " + d;
case null -> "null";
default -> "Other type: " + obj.getClass().getSimpleName();
};
}
Guarded Patterns — Extra Conditions in a Case #
static String classify(Object obj) {
return switch (obj) {
case Integer i when i < 0 -> "Negative integer: " + i;
case Integer i when i == 0 -> "Zero";
case Integer i -> "Positive integer: " + i;
case String s when s.isEmpty() -> "Empty string";
case String s -> "String: " + s;
case null -> "null";
default -> "Other type";
};
}
// Example with sealed classes — no default needed
sealed interface Shape permits Circle, Square, Triangle {}
record Circle(double r) implements Shape {}
record Square(double s) implements Shape {}
record Triangle(double base, double height) implements Shape {}
static double calculateArea(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Square(double side) -> side * side;
case Triangle(double base, double height) -> 0.5 * base * height;
// No default needed — the sealed class is exhaustive
};
}
Choosing the Right Construct #
Every conditional selection construct has its ideal context:
| Situation | Best Choice |
|---|---|
| 1-2 simple conditions | if-else |
| Many nested conditions | Guard clauses + if |
| One variable vs many fixed values | switch expression (Java 14+) |
| Enum or sealed class | switch expression without default |
| Default value from a condition | Ternary or switch expression |
| Matching types and values | Pattern matching switch (Java 21) |
| Intentional fall-through | switch statement + explicit comments |
Summary #
- Always use curly braces on
ifeven for a single statement; missing braces are a classic source of hard-to-find bugs.- Guard clauses reduce nesting — flip the conditions and
returnearly for special cases; the main logic stays at the top level and is easy to read.switchexpression (Java 14+) is the modern default — the->syntax eliminates fall-through andbreak; it produces a value directly, making it more functional.- Fall-through in switch statements is a bug source — always add
break, or add a// fall-throughcomment if it’s intentional to share logic between cases.switchwith enum withoutdefault— the compiler ensures all enum values are handled; adding a new enum value is immediately caught as a compile error.- Pattern matching
switch(Java 21) — replaces chains ofinstanceof+ cast; supports guarded patterns withwhenfor extra conditions.yieldinswitchexpressions — useyield valueto return a value from a code block inside aswitchexpression, notreturn.switchdoesn’t supportlong,double,float,boolean— only small integers,String, andenum; useif-elsefor other types.