algoz
Chapters/01 Java basics/Conditions and switch

Conditions and switch

Choose the right branching tool for every decision, from simple guards to pattern matching.

You'll gain
Choose the clearest branch every timefrom if-else chains to switch expressions
Practice
Sheet coming sooncoding problems for this note
Summary

Conditional statements evaluate boolean expressions and match discrete values to execute exactly one chosen branch.

The idea in real life

Think of a train track switch at a railway junction. As an oncoming train reaches the junction, the track switch shifts the rails so the train rolls down one specific path while leaving all other tracks closed. Conditional statements work like that track switch in your code: they inspect an incoming condition or value, direct execution down the selected branch, and bypass every alternate path.

Why it exists

  1. Algorithms must adapt their behaviour to user input and changing state rather than running the exact same fixed list of statements every time.
  2. Multi-branch conditionals let you handle numerical ranges, boundaries, and fallback default cases in a clear top-to-bottom sequence.
  3. Matching one value against many fixed keys with switch reads more clearly than a long chain of equality checks.
  4. Modern switch expressions turn branching into value-producing expressions while guaranteeing exhaustiveness at compile time.

How it works

if, else if and else

Evaluate boolean conditions in sequence until the first true branch runs.

The if statement evaluates a boolean condition. If the condition is true, the statements inside its block execute. If the condition is false, Java skips that block and checks the next else if condition, continuing down the chain until it finds a condition that evaluates to true.

If none of the conditions evaluate to true, the optional else block executes as a fallback. A pair of curly braces defines a block of statements that run together when the condition is met.

The following program evaluates an exam score and assigns a letter grade based on numeric ranges:

Main.java
public class Main {
    public static void main(String[] args) {
        int score = 85;
        char grade;

        if (score >= 90) {
            grade = 'A';
        } else if (score >= 80) {
            grade = 'B';
        } else if (score >= 70) {
            grade = 'C';
        } else if (score >= 60) {
            grade = 'D';
        } else {
            grade = 'F';
        }

        System.out.println("Score: " + score);
        System.out.println("Grade: " + grade);
    }
}
Output
Score: 85
Grade: B

In this program:

  • score holds the student's test score, set to 85.
  • grade stores the assigned letter grade character.
  • The condition score >= 90 evaluates to false, so Java skips the first block.
  • The condition score >= 80 evaluates to true, so Java assigns 'B' to grade and immediately skips the remaining else if and else branches.

The ternary operator

Produce one of two values using a single conditional expression.

The ternary operator (? :) is a compact expression that selects between two values based on a boolean condition. Its syntax takes three operands:

text
condition ? expressionIfTrue : expressionIfFalse

First, Java evaluates condition. If it is true, the operator evaluates and yields expressionIfTrue. If it is false, the operator evaluates and yields expressionIfFalse.

Unlike an if statement, the ternary construct is an expression, meaning it computes and returns a concrete value. You can assign that value directly to a variable or pass it directly into a method call.

The ternary operator works best for short, two-way decisions such as computing an absolute value or finding the maximum of two numbers:

Main.java
public class Main {
    public static void main(String[] args) {
        int first = 14;
        int second = 25;

        // Choose the larger number in a single concise line
        int max = (first > second) ? first : second;

        // Determine whether a score passed
        int score = 72;
        String result = (score >= 60) ? "Pass" : "Fail";

        System.out.println("Max: " + max);
        System.out.println("Result: " + result);
    }
}
Output
Max: 25
Result: Pass

In this program:

  • first and second hold the two integer values being compared.
  • max receives 25 because first > second evaluates to false, selecting second.
  • score holds the test result 72, and result receives "Pass" because score >= 60 evaluates to true.

Avoid chaining or nesting ternary operators. Writing expressions like a ? b : c ? d : e hurts readability, obscures operator precedence, and makes logic errors difficult to spot during code reviews.

Classic switch

Match a single discrete value against constant case labels.

A classic switch statement evaluates a single selector expression and compares its value against constant case labels. When a matching label is found, execution jumps directly to that branch and runs its statements until it encounters a break statement or reaches the end of the switch block.

The break statement terminates execution of the switch body and jumps to the first statement following the closing curly brace. If no case matches, execution jumps to the optional default label.

Java permits the selector expression to be one of the following types:

  • Primitive integer types: byte, short, char, and int.
  • String literals and variables.
  • Enumerated types (enums are introduced in chapter 02).

Primitive floating-point types (float, double), 64-bit integers (long), and boolean cannot be used as switch selectors.

Every case label must be a compile-time constant, such as a literal value or a final variable initialized with a constant expression. You cannot use variable names or range checks such as case x > 5:.

The following program maps an integer day code to the corresponding day name:

Main.java
public class Main {
    public static void main(String[] args) {
        int dayCode = 3;
        String dayName;

        switch (dayCode) {
            case 1:
                dayName = "Monday";
                break;
            case 2:
                dayName = "Tuesday";
                break;
            case 3:
                dayName = "Wednesday";
                break;
            case 4:
                dayName = "Thursday";
                break;
            case 5:
                dayName = "Friday";
                break;
            default:
                dayName = "Weekend";
                break;
        }

        System.out.println("Day 3: " + dayName);
    }
}
Output
Day 3: Wednesday

In this program:

  • dayCode holds the integer day identifier, set to 3.
  • dayName stores the resulting text description.
  • Execution matches case 3:, assigns "Wednesday" to dayName, and break exits the switch.

Fall-through

Execution slides into following cases when a break statement is omitted.

In a classic switch statement, execution does not stop automatically at the end of a case block. If you omit the break statement, execution continues directly into the subsequent case branches, running their statements even though their labels do not match the selector. This behaviour is called fall-through.

Accidental fall-through is a well-known source of bugs in classic switch statements. However, fall-through can also be used intentionally to share a single block of logic across multiple case labels.

The following program demonstrates both an unintended cascade and an intentional case grouping:

Main.java
public class Main {
    public static void main(String[] args) {
        int stage = 1;
        System.out.println("Starting pipeline at stage " + stage + ":");

        // Unintended fall-through: missing break statements
        switch (stage) {
            case 1:
                System.out.println("Stage 1 complete");
            case 2:
                System.out.println("Stage 2 complete");
            case 3:
                System.out.println("Stage 3 complete");
                break;
            default:
                System.out.println("Invalid stage");
        }

        // Intentional fall-through: grouping case labels
        int dayOfWeek = 6;
        String dayType;
        switch (dayOfWeek) {
            case 1:
            case 2:
            case 3:
            case 4:
            case 5:
                dayType = "Weekday";
                break;
            case 6:
            case 7:
                dayType = "Weekend";
                break;
            default:
                dayType = "Unknown";
                break;
        }

        System.out.println("Day 6 type: " + dayType);
    }
}
Output
Starting pipeline at stage 1:
Stage 1 complete
Stage 2 complete
Stage 3 complete
Day 6 type: Weekend

In this program:

  • In the first switch, stage is 1. Because case 1: and case 2: lack break statements, execution falls through and prints all three completion messages before reaching the break on stage 3.
  • In the second switch, cases 1 through 5 are stacked together so any weekday executes the exact same assignment before breaking.

Going deeper

Switch expressions

Return a value directly from a switch with arrow labels.

Standardized in Java 14 (JEP 361) and fully supported on JDK 17, switch expressions transform the switch construct into a value-producing expression.

Switch expressions introduce arrow labels (->). An arrow label executes only the expression or block to its right. There is no fall-through between arrow cases, so you never write break.

You can also list multiple comma-separated constants on a single case line, eliminating the need to stack multiple empty labels.

When a branch requires multiple statements, you enclose them in a block { ... } and use the yield keyword to return the value for that branch.

The following program uses a switch expression to classify days and compute values:

Main.java
public class Main {
    public static void main(String[] args) {
        int day = 6;

        // Arrow syntax with multiple comma-separated labels
        String dayType = switch (day) {
            case 1, 2, 3, 4, 5 -> "Weekday";
            case 6, 7 -> "Weekend";
            default -> "Unknown";
        };

        // Block body with yield for multi-statement branches
        int code = 2;
        int processedValue = switch (code) {
            case 1 -> 100;
            case 2 -> {
                int factor = 15;
                int base = 20;
                yield factor * base;
            }
            default -> 0;
        };

        System.out.println("Day type: " + dayType);
        System.out.println("Processed value: " + processedValue);
    }
}
Output
Day type: Weekend
Processed value: 300

In this program:

  • dayType receives "Weekend" directly from the switch expression. Notice the semicolon ; at the end of the assignment statement.
  • In the second switch, case 2 runs a block that declares local variables factor and base, and returns their product 300 using yield.

Exhaustiveness

Switch expressions must cover every conceivable value of the selector type.

A switch expression must be exhaustive: it must provide a value for every possible input value of the selector type. If any valid input could slip through without producing a value, the compiler rejects the program.

Classic switch statements do not enforce exhaustiveness because a statement does not return a value; unhandled values simply fall out of the switch body. A switch expression, by contrast, must always yield a result to its enclosing statement or variable assignment.

Because types such as int, char, and String have vast or infinite domains of possible values, you must provide a default branch when switching over them:

text
Main.java:5: error: the switch expression does not cover all possible input values
        String dayName = switch (day) {
                         ^
1 error

The only exception occurs when switching over an enum type (introduced in chapter 02), where the compiler can verify that every declared enum constant is covered by an explicit case label.

Early returns

Flatten deeply nested conditions by returning immediately when a branch matches.

Deeply nested if-else blocks create complex indentation structures often described as the pyramid of doom. You can flatten these structures using early returns (also known as guard clauses).

A guard clause checks boundary conditions, special edge cases, or invalid inputs at the start of a method and exits immediately with a return statement. Once those checks pass, the remaining logic runs at the base indentation level without nested else blocks.

The following program demonstrates how early returns keep branching flat and readable:

Main.java
public class Main {
    // Returns the shipping fee based on distance and membership status.
    static int calculateShippingFee(int distanceKm, boolean isMember) {
        if (distanceKm <= 0) {
            return 0;
        }
        if (isMember) {
            return 0;
        }
        if (distanceKm > 100) {
            return 25;
        }
        return 10;
    }

    public static void main(String[] args) {
        System.out.println("Local member fee: " + calculateShippingFee(15, true));
        System.out.println("Long distance non-member: " + calculateShippingFee(120, false));
        System.out.println("Short distance non-member: " + calculateShippingFee(30, false));
    }
}
Output
Local member fee: 0
Long distance non-member: 25
Short distance non-member: 10

In this program:

  • Static methods group code within the class (methods are covered in note 8).
  • calculateShippingFee inspects the inputs distanceKm and isMember.
  • Non-positive distances and active memberships exit immediately with a fee of 0, keeping the remaining distance logic un-nested.

Choosing between if, ternary and switch

Match the construct to the shape of the decision you need to make.

Selecting the clearest branching construct depends on whether you are testing ranges, combining boolean conditions, or matching discrete keys:

ConstructBest used forEvaluatesHandles ranges?Produces a value?
if / else ifComplex conditions, ranges, and state checksAny boolean expressionYes (score >= 80)No (statement)
Ternary (? :)Simple binary choices assigned in a single lineOne boolean conditionYesYes (expression)
Switch expressionMatching a single variable against discrete keysPrimitive integers, strings, enumsNo (exact keys only)Yes (expression)
Classic switchLegacy code or intentional statement fall-throughPrimitive integers, strings, enumsNo (exact keys only)No (statement)

Follow these guidelines:

  • Use the ternary operator when selecting between two simple values to assign or return.
  • Use if / else if / else when checking numeric ranges, floating-point numbers, or multiple boolean expressions connected with logical AND (&&) or OR (||).
  • Use switch expressions when matching a single discrete variable against three or more known constant values or categories.

Under the hood

Pattern matching for switch

Inspect types and test guard conditions directly inside switch cases.

In Java 21 and newer (finalized in JEP 441), switch statements and expressions support pattern matching. Instead of testing only constant primitive keys or strings, a switch can match an object's runtime type, bind it to a local pattern variable, and apply boolean guards.

Before Java 21, passing null to a switch on a String throws a NullPointerException before any case is checked. In Java 21 or newer, you can handle null safely using an explicit case null -> branch.

You can also attach a boolean guard to any pattern using the when keyword. The case branch executes only if the type matches and the guard condition evaluates to true.

The following program runs on Java 21 or newer to inspect input values:

Main.java
public class Main {
    // Returns a description of an object based on its type and value.
    static String describeInput(Object input) {
        return switch (input) {
            case null -> "Empty reference";
            case String text when text.isEmpty() -> "Blank text value";
            case String text -> "Text of length " + text.length() + ": " + text;
            default -> "Unsupported value";
        };
    }

    public static void main(String[] args) {
        System.out.println(describeInput("Algoz"));
        System.out.println(describeInput(""));
        System.out.println(describeInput(null));
    }
}
Output
Text of length 5: Algoz
Blank text value
Empty reference

In this program:

  • describeInput accepts an Object reference (objects and class hierarchies are covered in chapter 02).
  • case null -> catches null references safely without throwing an exception.
  • case String text when text.isEmpty() checks that input is a String and tests its content with a boolean guard before the more general case String text branch.

How switch compiles

The JVM selects between indexed jumps and binary search tables.

When the Java compiler translates a switch construct into bytecode, it chooses between two dedicated instructions based on the density of the case values: tableswitch and lookupswitch.

The tableswitch instruction is used when case values form a dense, contiguous sequence of integers (such as 1, 2, and 3). The JVM creates an indexed jump table in bytecode. At run time, the selector value is used as an index into that table, so the JVM jumps straight to the right case.

The lookupswitch instruction is used when case values are sparse, with large numerical gaps between keys (such as 10, 500, and 10000). Allocating an indexed table for sparse keys would waste bytecode space on empty slots. Instead, the compiler stores an ordered list of key-and-offset pairs. At run time, the JVM searches the sorted keys for the selector; because they are sorted, it can use a fast search such as binary search (chapter 18).

Disassembling compiled switch methods with javap -c reveals how the compiler chooses between these two bytecode instructions:

text
Compiled from "SwitchDemo.java"
public class SwitchDemo {
  public static int denseSwitch(int);
    Code:
       0: iload_0
       1: tableswitch   { // 1 to 3
                     1: 28
                     2: 31
                     3: 34
               default: 37
          }
      28: bipush        10
      30: ireturn
      31: bipush        20
      33: ireturn
      34: bipush        30
      36: ireturn
      37: iconst_0
      38: ireturn

  public static int sparseSwitch(int);
    Code:
       0: iload_0
       1: lookupswitch  { // 3
                    10: 36
                   500: 39
                 10000: 43
               default: 46
          }
      36: bipush        100
      38: ireturn
      39: sipush        5000
      42: ireturn
      43: ldc           #7                  // int 99999
      45: ireturn
      46: iconst_0
      47: ireturn
}

When switching on a String, the compiler evaluates hashCode() first using a lookup table on integer hash codes, and then verifies the match using equals() to prevent hash collision errors.

Dangling else

An ambiguous else clause always binds to the closest unclosed if statement.

The dangling else problem is a classic grammatical ambiguity in programming languages when if statements are nested without curly braces. When an inner if is followed by an else, does the else belong to the outer condition or the inner one?

Under the Java Language Specification (JLS §14.9.1), an else clause always binds to the nearest preceding if statement that does not already have an else.

The compiler completely ignores visual indentation. If you indent an else to align with the outer if without using braces, the compiler still attaches the else to the inner if:

Main.java
public class Main {
    public static void main(String[] args) {
        int x = 15;
        int y = 5;

        // Misleading indentation: the else attaches to the inner if
        if (x > 10)
            if (y > 10)
                System.out.println("Both greater than 10");
        else
            System.out.println("Misleading output: x is not greater than 10");
    }
}
Output
Misleading output: x is not greater than 10

In this program:

  • x is 15, so the outer condition x > 10 evaluates to true.
  • Execution enters the inner statement if (y > 10). Because y is 5, that condition is false.
  • The compiler bound the else to if (y > 10), so it runs the else branch and prints that x is not greater than 10, even though x is 15.

Always enclosing every conditional body in explicit curly braces {} eliminates this ambiguity completely.

In Java

The following program combines conditional branching, switch expressions, early returns, and the ternary operator to process student grades and determine academic standing:

Main.java
public class Main {
    // Returns the letter grade corresponding to a numeric score.
    static char getLetterGrade(int score) {
        if (score >= 90) {
            return 'A';
        } else if (score >= 80) {
            return 'B';
        } else if (score >= 70) {
            return 'C';
        } else if (score >= 60) {
            return 'D';
        } else {
            return 'F';
        }
    }

    // Returns a performance description based on the letter grade.
    static String describeGrade(char grade) {
        return switch (grade) {
            case 'A' -> "Excellent work";
            case 'B', 'C' -> "Satisfactory progress";
            case 'D', 'F' -> {
                String warning = "Academic review required";
                yield warning;
            }
            default -> "Invalid grade";
        };
    }

    // Returns a status string indicating whether the student passed.
    static String getStatus(int score) {
        return (score >= 60) ? "Passed" : "Failed";
    }

    public static void main(String[] args) {
        int studentScore = 82;
        char grade = getLetterGrade(studentScore);
        String description = describeGrade(grade);
        String status = getStatus(studentScore);

        System.out.println("Score: " + studentScore);
        System.out.println("Grade: " + grade);
        System.out.println("Status: " + status);
        System.out.println("Remarks: " + description);
    }
}
Output
Score: 82
Grade: B
Status: Passed
Remarks: Satisfactory progress

In this program:

  • getLetterGrade uses an if-else ladder to classify a continuous numeric score into a letter grade character.
  • describeGrade uses a switch expression with multi-label cases and a yield block to return a descriptive remark.
  • getStatus uses the ternary operator to return "Passed" or "Failed" based on the score threshold.
  • main coordinates the pipeline and prints the final academic evaluation.

Traps

  1. Omitting break in classic switch statements

    In classic switch statements, omitting break causes execution to fall through into subsequent cases, running unrelated code unintentionally. Use modern switch expressions with arrow syntax -> to eliminate fall-through entirely.

    ✕ WRONGcase 1: doAction();
    ✓ RIGHTcase 1: doAction(); break;
  2. Using assignment = instead of equality == in a condition

    For numeric types, if (count = 5) triggers a compiler error because an integer cannot convert to a boolean. With boolean variables, however, if (isValid = false) assigns false to isValid and evaluates to false, compiling cleanly but introducing a silent logic bug. Write if (!isValid) instead.

    ✕ WRONGif (isValid = false)
    ✓ RIGHTif (isValid == false)
  3. Comparing strings with == instead of equals

    The == operator tests whether two variables point to the same object. It does not compare text content. Comparing strings with == causes unpredictable failures. String comparison and the string pool are covered in note 10, strings.

    ✕ WRONGif (command == "start")
    ✓ RIGHTif (command.equals("start"))
  4. Dangling else binding to the wrong if statement

    Without curly braces, an else keyword binds to the nearest preceding if statement regardless of your indentation. Always enclose every if and else body in explicit curly braces {} to avoid inverted logic bugs.

    ✕ WRONGif (ready) if (hasAccess) run(); else reject();
    ✓ RIGHTif (ready) { if (hasAccess) run(); } else { reject(); }
  5. Omitting default in a switch expression

    Switch expressions must be exhaustive because they evaluate to a value. For open-ended types such as int or String, omitting the default branch produces a compile error: the switch expression does not cover all possible input values.

    ✕ WRONGint fee = switch (tier) { case 1 -> 5; };
    ✓ RIGHTint fee = switch (tier) { case 1 -> 5; default -> 0; };
  6. Nesting ternary operators and destroying readability

    Chaining multiple ? : operators obscures operator precedence and creates code that is difficult to trace. Use standard if-else blocks or a switch expression whenever selecting among three or more outcomes.

    ✕ WRONGint score = a > b ? c > d ? 10 : 20 : 30;
    ✓ RIGHTint score; if (a > b) { score = (c > d) ? 10 : 20; } else { score = 30; }
AlgozSet in Geist & Geist MonoJava track