Module 3 · Control Flow & Program Structure ⏱ 19 min

Switch Statements, Switch Expressions, and Enums

By the end of this lesson you will be able to:
  • Predict the output of a classic switch, including deliberate and accidental fall-through
  • Use modern switch expressions with -> and yield to return values safely
  • Define an enum and write an exhaustive switch over its constants

Branching on a single value against a list of constants is tedious with if / else if. You end up repeating the variable name, cluttering the logic, and inviting off-by-one errors in the comparisons. Java's switch exists for exactly this situation: it matches one value against a set of case labels and jumps straight to the match.

The classic switch has been in Java since the beginning, but it carries a sharp edge: fall-through. When a matching case has no break, execution keeps rolling into the next case, and the next, until something stops it. That can be deliberate — stacking several cases in front of one shared block — but more often it is a bug that ships to production.

The classic switch statement

A switch statement compares the value in parentheses against each case label for equality. When a match is found, execution starts at that label and continues downward. A break statement exits the switch entirely; without one, the program falls through into the next case and keeps running. A default label catches anything that did not match an explicit case.

The values in case labels must be compile-time constants: literal numbers, characters, or enum constants. You cannot use a variable as a case label, even if that variable never changes. This restriction lets the compiler build an efficient jump table, but it also means switch is only for fixed, known-ahead-of-time values.

flowchart TD
  SW["switch (day)"] --> C1["case 1:"]
  C1 --> P1["print(Mon)"]
  P1 --> B1["break"]
  B1 --> E["exit switch"]
  SW --> C2["case 2:"]
  C2 --> P2["print(Tue)"]
  P2 --> C3["case 3:"]
  C3 --> P3["print(Wed)"]
  P3 --> B3["break"]
  B3 --> E
  style P2 fill:#b45309,color:#fff
  style C3 fill:#b45309,color:#fff
A break exits the switch; a missing break falls through into the next case and keeps executing.
Main.java — classic switch with deliberate fall-through and a default. Real, compiling Java.
public class Main {
    public static void main(String[] args) {
        int day = 2;
        switch (day) {
            case 1:
                System.out.println("Monday");
                break;
            case 2:
            case 3:
                System.out.println("Midweek");
                break;
            case 6:
            case 7:
                System.out.println("Weekend");
                break;
            default:
                System.out.println("Other");
        }
    }
}

Switch expressions: -> and yield

Modern Java (14+) adds a safer form: the switch expression. Instead of printing inside each case, the switch returns a value that you assign to a variable. The arrow syntax -> means exactly one result per case, and it never falls through. There is no need for break, because each arrow case is self-contained.

If a case needs multiple statements before producing its value, use the yield keyword instead of an arrow. The switch expression must be exhaustive: every possible value must map to a case, or the compiler rejects it. A default clause satisfies that requirement when the value is not an enum.

Switch expressions remove an entire category of bug. If you have ever debugged a missing break at 2 a.m., you already know why this matters.

flowchart LR
  subgraph ST["Switch statement"]
    S1["case 1:"] --> P1["print(A)"]
    P1 --> B1["break needed"]
  end
  subgraph EX["Switch expression"]
    S2["case 1 -> "A""] --> V1["returns value"]
    S3["case 2 -> "B""] --> V2["returns value"]
  end
  style ST fill:#b45309,color:#fff
  style EX fill:#166534,color:#fff
A switch statement runs statements and needs breaks. A switch expression returns a value and never falls through.
Main.java — switch expression with arrow syntax and yield. Real, compiling Java.
public class Main {
    public static void main(String[] args) {
        int score = 85;
        String grade = switch (score / 10) {
            case 10, 9 -> "A";
            case 8 -> "B";
            case 7 -> "C";
            case 6 -> "D";
            default -> {
                System.out.println("Below threshold");
                yield "F";
            }
        };
        System.out.println("Grade: " + grade);
    }
}

Enums: named constants with type safety

An enum is a type with a fixed set of named values. Define it once, and the compiler treats it as a real type — not just a number with a label. Day.MONDAY is a Day; it is not an int and cannot be confused with one.

Enums shine inside switches. Because the compiler knows every possible value, it can check that your switch is exhaustive: every enum constant must have a case, or a default, or the code will not compile. That turns a whole class of runtime mistakes into build-time errors. Enum switches also read more clearly than integer switches: case MONDAY says what it means in plain English.

flowchart TD
  E["enum Day { MON, TUE, WED }"] --> S["switch (d)"]
  S --> C1["case MON -> ..."]
  S --> C2["case TUE -> ..."]
  S --> C3["case WED -> ..."]
  C3 --> OK["Compiler verifies:<br/>all cases covered"]
  style E fill:#c2410c,color:#fff
  style OK fill:#166534,color:#fff
An enum defines a closed set of values; a switch over it can be checked for exhaustiveness at compile time.
Exercise

Classic switch with a missing break. Predict the exact output (watch the fall-through).

public class Main {
    public static void main(String[] args) {
        int level = 2;
        switch (level) {
            case 1:
                System.out.println("Low");
            case 2:
                System.out.println("Medium");
            case 3:
                System.out.println("High");
                break;
            default:
                System.out.println("Unknown");
        }
    }
}
Exercise

In a switch expression using arrow syntax (->), what happens if you omit a break?

Exercise

Switch expression with yield. Predict the exact output (two lines).

public class Main {
    public static void main(String[] args) {
        int n = 4;
        String result = switch (n) {
            case 1, 2, 3 -> "small";
            case 4, 5 -> "medium";
            default -> {
                System.out.println("big");
                yield "large";
            }
        };
        System.out.println(result);
    }
}
Exercise

Why is a switch over an enum safer than a switch over an int?

Exercise

Enum switch puzzle. Predict the exact output (two lines) — work through the enum and the switch expression carefully.

public class Main {
    enum Status { PENDING, ACTIVE, CLOSED }

    public static void main(String[] args) {
        Status s = Status.ACTIVE;
        String label = switch (s) {
            case PENDING -> "wait";
            case ACTIVE -> "go";
            case CLOSED -> "done";
        };
        System.out.println(label);
        System.out.println(s == Status.ACTIVE);
    }
}

Recap

  • A classic switch matches a value against case labels. Without break, execution falls through into the next case.
  • A switch expression with -> returns a value and never falls through. Use yield inside a block to return a value.
  • Switch expressions must be exhaustive; the compiler rejects incomplete ones.
  • An enum defines a closed set of named constants. Switching over an enum lets the compiler verify that every constant is covered.
  • Prefer switch expressions over classic switches when you are computing a value; they remove an entire category of fall-through bugs.

Next you will learn loop patterns and invariants — the mental models that make loops predictable instead of mysterious.

Checkpoint quiz

What does a classic switch do when a matching case has no break?

In a switch expression, what is the purpose of yield?

Go deeper — technical resources