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 #

TypeSupported
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 #

Aspectswitch Statementswitch Expression
Produces a value
Fall-through defaultYes (dangerous)None
break requiredYesNo
Complex code blocksDirectlyUse yield
Java versionAllJava 14+
ExhaustivenessNot checkedMust 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:2px

switch 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
};
Using switch with enum without default is a type safety advantage: if you add a new value to the enum but forget to add it to the switch, the compiler immediately gives an error. With int or String, adding a new value silently falls into default and 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:

SituationBest Choice
1-2 simple conditionsif-else
Many nested conditionsGuard clauses + if
One variable vs many fixed valuesswitch expression (Java 14+)
Enum or sealed classswitch expression without default
Default value from a conditionTernary or switch expression
Matching types and valuesPattern matching switch (Java 21)
Intentional fall-throughswitch statement + explicit comments

Summary #

  • Always use curly braces on if even for a single statement; missing braces are a classic source of hard-to-find bugs.
  • Guard clauses reduce nesting — flip the conditions and return early for special cases; the main logic stays at the top level and is easy to read.
  • switch expression (Java 14+) is the modern default — the -> syntax eliminates fall-through and break; 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-through comment if it’s intentional to share logic between cases.
  • switch with enum without default — 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 of instanceof + cast; supports guarded patterns with when for extra conditions.
  • yield in switch expressions — use yield value to return a value from a code block inside a switch expression, not return.
  • switch doesn’t support long, double, float, boolean — only small integers, String, and enum; use if-else for other types.

← Previous: Operators   Next: Loops →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact