> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Use Switch Expressions and Pattern Matching in Java
- URL: https://www.ggorantala.dev/how-to-use-switch-expressions-pattern-matching-java/
- Published: 2026-05-01T05:24:00.000Z
- Updated: 2026-10-31T06:31:28.000Z
- Description: Build a student-grade report in Java 21 with arrow switches, yield, guarded patterns and case null. Full code, 39 checks, and seven compile errors forced.
- Author: Gopi Gorantala
- Tags: how-to-tutorials, modern-java, core-java

## Here's the report we're building

This is what you'll have in about twenty minutes. One small grading program, five students' submissions of four different shapes, and a few awkward raw inputs thrown at it:

```log
AUTUMN runs 12 weeks
SPRING runs 12 weeks
SUMMER runs 8 weeks

Exam[score=91]                                       ->  91  Distinction. Offer the honours seminar.
Coursework[marks=[78, 84, 80]]                       ->  81  MERIT. 9 more marks for distinction.
Project[title=Parcel router, score=72, late=true]    ->  62  PASS. 18 more marks for merit.
Project[title=Library kiosk, score=72, late=false]   ->  72  PASS. 8 more marks for merit.
Absent[reason=illness]                               ->   0  Fail. Book a resit.

describe(88) = score 88 (B)
describe(140) = out of range: 140
describe( 67 ) = score 67 (D)
describe(abc) = no value
describe() = blank
describe(null) = no value
describe(3.5) = unsupported: Double

```

Every line of that comes out of a `switch`. No `if`/`else if` ladders, no `instanceof` casts, no `break`. If you've used `switch` since Java 8, the code will look wrong to you at first, and then it'll look better. This tutorial is a guide to Java switch expression and pattern matching features as they work in Java 21, built on a student grades app small enough to type in one sitting.

Two things are in play, and people blur them together. A **switch expression** (Java 14) is a `switch` that produces a value, with arrow labels and no fall-through. **Pattern matching for switch** (Java 21) lets the labels be types, with guards, instead of only constants. We'll use both, in that order, and each step has a command you can run to check it.

I built this on a plain JDK with `javac`. It compiled on the first try, which is rare enough that I went and broke it on purpose, seven different ways. Those compiler messages are the troubleshooting section, and by the end you'll want them.

## What you need to have installed

| Tool                     | Version I used                   | Check it with  |
| ------------------------ | -------------------------------- | -------------- |
| JDK                      | OpenJDK 21.0.12.1 (Ubuntu build) | java -version  |
| javac                    | 21.0.12.1                        | javac -version |
| IntelliJ IDEA or Eclipse | optional, see the setup section  | Help → About   |

Anything from JDK 21 up should compile this as-is; I only ran it on 21\. Pattern matching for switch is final in 21 ([JEP 441](https://openjdk.org/jeps/441?ref=ggorantala.dev)), so on JDK 17 it won't compile (I forced that error below). Switch expressions arrived in 14 ([JEP 361](https://openjdk.org/jeps/361?ref=ggorantala.dev)).

You should know basic Java, enums and what a `record` is. If records are new, read [How to Use Java Records](https://ggorantala.dev/how-to-use-java-records/?ref=ggorantala.dev) first. We'll also lean on a sealed interface, which [the sealed classes tutorial](https://ggorantala.dev/how-to-use-sealed-classes-interfaces-java/?ref=ggorantala.dev) covers properly. Here you only need to know it means "this interface has a fixed list of implementations the compiler can see".

Need a JDK? SDKMAN works: `sdk list java` and install a 21 build from the list. Or take any JDK 21 build you like.

## Lay out the grades project

The primary path here is the command line, because it has the fewest moving parts and the compiler messages are the point. IDE notes come after.

Create this tree:

```log
grades/
└── src/
    ├── main/java/com/example/grades/
    │   ├── Band.java
    │   ├── Grader.java
    │   ├── Main.java
    │   ├── Submission.java
    │   └── Term.java
    └── test/java/com/example/grades/
        ├── Checks.java
        └── LegacySwitchDemo.java

```

```bash
mkdir -p grades/src/main/java/com/example/grades grades/src/test/java/com/example/grades
cd grades

```

To compile and run later, from the `grades` folder:

```bash
mkdir -p out
javac -Xlint:all -d out $(find src -name '*.java')
java -cp out com.example.grades.Main

```

On Windows PowerShell, swap the `$(find ...)` part for `javac -Xlint:all -d out (Get-ChildItem -Recurse src -Filter *.java).FullName`. I didn't run that variant myself.

### In IntelliJ IDEA

*New Project → Java*, set the JDK to 21, build system *IntelliJ*, and untick the sample-code checkbox. Then open *File → Project Structure → Project* and set *Language level* to 21\. If the level is lower, IntelliJ underlines `case Exam e ->` in red even though the file is fine. Right-click `src/main/java`, *Mark Directory as → Sources Root*, and `src/test/java` as *Test Sources Root*. I wrote these menu paths from JetBrains' current help pages; I didn't click through the IDE on this run.

### In Eclipse

*File → New → Java Project*, pick a JRE 21, finish. Open *Project → Properties → Java Compiler* and set the compliance level to 21\. Add `src/main/java` and `src/test/java` as source folders under *Build Path → Configure Build Path → Source*. Same caveat: from the docs, not executed.

### With Maven or Gradle

Set the Java release to 21 (`<maven.compiler.release>21</maven.compiler.release>` in a `pom.xml`, or `java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }` in `build.gradle.kts`). The code has no dependencies. My environment couldn't reach Maven Central on this run, so I didn't build with either tool. That's why the checks are plain Java instead of JUnit; the last section covers converting them.

### Podman and Docker

Nothing here needs a container. If you want a throwaway JDK anyway: `podman run --rm -it -v "$PWD":/work -w /work docker.io/library/eclipse-temurin:21-jdk bash` and swap `podman` for `docker`. I didn't run that either, so check the tag exists when you do.

## Write the grader, one switch at a time

Two small files first, then the real work. `Grader.java` grows across five steps; the complete file is at the end of Step 5, and every step's code goes into it in order.

### Step 1: A switch that returns a value

Goal: map a term to its number of teaching weeks, with no `break`, no default, and no way to forget one.

`src/main/java/com/example/grades/Term.java`

```java
package com.example.grades;

public enum Term {
    AUTUMN, SPRING, SUMMER
}

```

`src/main/java/com/example/grades/Band.java`

```java
package com.example.grades;

public enum Band {
    DISTINCTION, MERIT, PASS, FAIL
}

```

Start `Grader.java` like this. The imports are for later steps; unused imports don't hurt, and you won't have to come back for them.

`src/main/java/com/example/grades/Grader.java`

```java
package com.example.grades;

import com.example.grades.Submission.Absent;
import com.example.grades.Submission.Coursework;
import com.example.grades.Submission.Exam;
import com.example.grades.Submission.Project;

public final class Grader {

    private Grader() { }

    public static int teachingWeeks(Term term) {
        return switch (term) {
            case AUTUMN, SPRING -> 12;
            case SUMMER -> 8;
        };
    }
}

```

Three things to notice. The whole `switch` sits after `return`, so it's an *expression*: it has a value, and the semicolon after the closing brace belongs to the `return`. `case AUTUMN, SPRING` is one label with two constants, which replaces the stacked `case A: case B:` lines. And there's no `default`. That's on purpose. When a switch expression covers an enum, the compiler checks that every constant is handled, and if you add `WINTER` later it refuses to compile until you decide what winter means. A `default` would turn that error into a silent wrong answer.

You can't run anything yet, since `Submission` doesn't exist. The checkpoint comes in Step 4.

### Step 2: Switch on numbers, and see what the arrow saves you

Goal: turn a score into a letter. A score of 100 has to land in the same bucket as 90 to 99.

Add this method to `Grader`, below `teachingWeeks`:

```java
    public static char letter(int score) {
        if (score < 0 || score > 100) {
            throw new IllegalArgumentException("score out of range: " + score);
        }
        return switch (score / 10) {
            case 10, 9 -> 'A';
            case 8 -> 'B';
            case 7 -> 'C';
            case 6 -> 'D';
            default -> 'F';
        };
    }

```

Dividing by ten turns 0 to 100 into 0 to 10, which makes constants usable as labels. `case 10, 9` is the grouping. Here `default` is required: the compiler can't prove that `score / 10` is only 0 to 10, so it demands a catch-all, and the guard above it makes sure bad input never reaches it silently.

Now the part I want you to see with your own eyes. This is the same logic in the old colon style, with one missing `break`:

`src/test/java/com/example/grades/LegacySwitchDemo.java`

```java
package com.example.grades;

public class LegacySwitchDemo {

    static char legacyLetter(int score) {
        char letter = '?';
        switch (score / 10) {
            case 10:
            case 9:
                letter = 'A';
                break;
            case 8:
                letter = 'B';
            case 7:
                letter = 'C';
                break;
            default:
                letter = 'F';
        }
        return letter;
    }

    public static void main(String[] args) {
        System.out.println("legacy 85 -> " + legacyLetter(85));
        System.out.println("arrow  85 -> " + Grader.letter(85));
    }
}

```

This demo needs `Grader` to compile, so run it after Step 6\. When you do, the output is:

```log
legacy 85 -> C
arrow  85 -> B

```

An 85 became a C. Nothing crashed and nothing warned, except `-Xlint:all`, which does flag it:

```log
src/test/java/com/example/grades/LegacySwitchDemo.java:14: warning: [fallthrough] possible fall-through into case
            case 7:
            ^
1 warning

```

That one-line difference is the argument for arrow labels. They don't fall through, so the bug can't be written.

### Step 3: Compute inside a case with `yield`

Goal: produce a one-line report per band, and in two of the bands do some arithmetic first.

Add these two methods:

```java
    public static Band band(int score) {
        return switch (letter(score)) {
            case 'A' -> Band.DISTINCTION;
            case 'B' -> Band.MERIT;
            case 'C', 'D' -> Band.PASS;
            default -> Band.FAIL;
        };
    }

```

```java
    public static String report(int score) {
        Band band = band(score);
        return switch (band) {
            case DISTINCTION -> "Distinction. Offer the honours seminar.";
            case MERIT, PASS -> {
                int target = band == Band.MERIT ? 90 : 80;
                int gap = target - score;
                String next = band == Band.MERIT ? "distinction" : "merit";
                yield band + ". " + gap + " more mark" + (gap == 1 ? "" : "s") + " for " + next + ".";
            }
            case FAIL -> "Fail. Book a resit.";
        };
    }

```

`band` is a switch on a `char`. The labels `'C', 'D'` group two letters into one band. `report` is the interesting one. The `MERIT, PASS` arm has a block, and inside a block a bare value isn't allowed; you hand it back with `yield`. Think of `yield` as `return` for the switch expression only. The enclosing method keeps going.

I deleted the `yield` on purpose later to see what the compiler says (`not a statement`). It's in the troubleshooting list.

### Step 4: Switch on types with `when` guards

Goal: score four different kinds of submission without a single `instanceof`.

`src/main/java/com/example/grades/Submission.java`

```java
package com.example.grades;

import java.util.List;

public sealed interface Submission {

    record Exam(int score) implements Submission { }

    record Coursework(List<Integer> marks) implements Submission {
        public Coursework {
            marks = List.copyOf(marks);
        }
    }

    record Project(String title, int score, boolean late) implements Submission { }

    record Absent(String reason) implements Submission { }
}

```

`Submission` is sealed: the compiler knows `Exam`, `Coursework`, `Project` and `Absent` are the only implementations, because they're declared in the same file. The `Coursework` constructor copies its list so a caller can't change the marks afterwards (one of the checks proves it).

Add this method to `Grader`:

```java
    public static int finalScore(Submission submission) {
        return switch (submission) {
            case Exam e -> e.score();
            case Coursework c when c.marks().isEmpty() -> 0;
            case Coursework c -> (int) Math.round(
                    c.marks().stream().mapToInt(Integer::intValue).average().orElse(0));
            case Project p when p.late() -> Math.max(0, p.score() - 10);
            case Project p -> p.score();
            case Absent a -> 0;
        };
    }

```

Read the labels top to bottom. `case Exam e` matches any `Exam` and binds it to `e`, already cast. `case Coursework c when c.marks().isEmpty()` matches a `Coursework` only if the guard is also true. Then comes the unguarded `case Coursework c`, which catches the rest. The order matters, and the compiler enforces it: put the general case above the guarded one and the guarded one is unreachable, which is a compile error (forced below).

There's still no `default`. `Submission` is sealed, all four are covered, so the switch is exhaustive. Add a fifth record to `Submission` and every switch like this one stops compiling until you handle it. I'd rather have that than a runtime surprise.

One more guard-related detail. In Java 21 you write `when`, not `&&`. The JDK 17 preview used `&&` for guards, so older articles show it; if you copy code from them, change it.

**Checkpoint.** You have everything `Main` needs except `describe`. Add Step 5, then run it.

### Step 5: Switch on `Object`, `case null`, and a safe `default`

Goal: take unclean input (an integer, a string from a form, a submission, `null`, something unexpected) and describe it without a chain of `if` tests.

Add the last method and its helper, then close the class:

```java
    public static String describe(Object raw) {
        return switch (raw) {
            case null -> "no value";
            case Integer i when i < 0 || i > 100 -> "out of range: " + i;
            case Integer i -> "score " + i + " (" + letter(i) + ")";
            case String s when s.isBlank() -> "blank";
            case String s -> describe(parse(s));
            case Submission sub -> "submission worth " + finalScore(sub);
            default -> "unsupported: " + raw.getClass().getSimpleName();
        };
    }

```

```java
    private static Object parse(String text) {
        try {
            return Integer.valueOf(text.trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }

```

Walk through the labels. `case null` is allowed in Java 21 and it's the thing that finally lets a `switch` accept `null` on purpose. Without it, a `null` selector throws a `NullPointerException`, same as always. `case Integer i when i < 0 || i > 100` sits above plain `case Integer i`, so the bad numbers get caught first. The `String` arm calls `describe` again on the parsed value; `parse` returns `null` for non-numbers, which loops back to `case null`. And `default` is required here because `Object` has too many subtypes to list.

If you'd rather see the complete `Grader.java` in one block, here it is:

`src/main/java/com/example/grades/Grader.java`

```java
package com.example.grades;

import com.example.grades.Submission.Absent;
import com.example.grades.Submission.Coursework;
import com.example.grades.Submission.Exam;
import com.example.grades.Submission.Project;

public final class Grader {

    private Grader() { }

    public static int teachingWeeks(Term term) {
        return switch (term) {
            case AUTUMN, SPRING -> 12;
            case SUMMER -> 8;
        };
    }

    public static char letter(int score) {
        if (score < 0 || score > 100) {
            throw new IllegalArgumentException("score out of range: " + score);
        }
        return switch (score / 10) {
            case 10, 9 -> 'A';
            case 8 -> 'B';
            case 7 -> 'C';
            case 6 -> 'D';
            default -> 'F';
        };
    }

    public static Band band(int score) {
        return switch (letter(score)) {
            case 'A' -> Band.DISTINCTION;
            case 'B' -> Band.MERIT;
            case 'C', 'D' -> Band.PASS;
            default -> Band.FAIL;
        };
    }

    public static String report(int score) {
        Band band = band(score);
        return switch (band) {
            case DISTINCTION -> "Distinction. Offer the honours seminar.";
            case MERIT, PASS -> {
                int target = band == Band.MERIT ? 90 : 80;
                int gap = target - score;
                String next = band == Band.MERIT ? "distinction" : "merit";
                yield band + ". " + gap + " more mark" + (gap == 1 ? "" : "s") + " for " + next + ".";
            }
            case FAIL -> "Fail. Book a resit.";
        };
    }

    public static int finalScore(Submission submission) {
        return switch (submission) {
            case Exam e -> e.score();
            case Coursework c when c.marks().isEmpty() -> 0;
            case Coursework c -> (int) Math.round(
                    c.marks().stream().mapToInt(Integer::intValue).average().orElse(0));
            case Project p when p.late() -> Math.max(0, p.score() - 10);
            case Project p -> p.score();
            case Absent a -> 0;
        };
    }

    public static String describe(Object raw) {
        return switch (raw) {
            case null -> "no value";
            case Integer i when i < 0 || i > 100 -> "out of range: " + i;
            case Integer i -> "score " + i + " (" + letter(i) + ")";
            case String s when s.isBlank() -> "blank";
            case String s -> describe(parse(s));
            case Submission sub -> "submission worth " + finalScore(sub);
            default -> "unsupported: " + raw.getClass().getSimpleName();
        };
    }

    private static Object parse(String text) {
        try {
            return Integer.valueOf(text.trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
}

```

### Step 6: A `Main` that exercises every arm

`src/main/java/com/example/grades/Main.java`

```java
package com.example.grades;

import com.example.grades.Submission.Absent;
import com.example.grades.Submission.Coursework;
import com.example.grades.Submission.Exam;
import com.example.grades.Submission.Project;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        for (Term term : Term.values()) {
            System.out.println(term + " runs " + Grader.teachingWeeks(term) + " weeks");
        }
        System.out.println();

        List<Submission> inbox = List.of(
                new Exam(91),
                new Coursework(List.of(78, 84, 80)),
                new Project("Parcel router", 72, true),
                new Project("Library kiosk", 72, false),
                new Absent("illness"));

        for (Submission s : inbox) {
            int score = Grader.finalScore(s);
            System.out.printf("%-52s -> %3d  %s%n", s, score, Grader.report(score));
        }
        System.out.println();

        for (Object raw : new Object[] {88, 140, " 67 ", "abc", "", null, 3.5}) {
            System.out.println("describe(" + raw + ") = " + Grader.describe(raw));
        }
    }
}

```

## Run it and read the output

```bash
mkdir -p out
javac -Xlint:all -d out $(find src -name '*.java')
java -cp out com.example.grades.Main

```

The compile prints one warning (the fall-through in the demo file, which is the point of that file) and nothing else. The run prints the report you saw at the top:

```log
AUTUMN runs 12 weeks
SPRING runs 12 weeks
SUMMER runs 8 weeks

Exam[score=91]                                       ->  91  Distinction. Offer the honours seminar.
Coursework[marks=[78, 84, 80]]                       ->  81  MERIT. 9 more marks for distinction.
Project[title=Parcel router, score=72, late=true]    ->  62  PASS. 18 more marks for merit.
Project[title=Library kiosk, score=72, late=false]   ->  72  PASS. 8 more marks for merit.
Absent[reason=illness]                               ->   0  Fail. Book a resit.

describe(88) = score 88 (B)
describe(140) = out of range: 140
describe( 67 ) = score 67 (D)
describe(abc) = no value
describe() = blank
describe(null) = no value
describe(3.5) = unsupported: Double

```

Worth reading line by line. The late project scored 72 but the guard knocked off ten, giving 62 and a PASS; the on-time project with the same 72 stays at 72\. The coursework average of 78, 84 and 80 is 80.67, rounded to 81\. `describe(" 67 ")` trims, parses and recurses to `score 67 (D)`. `describe("abc")` prints `no value`, because `parse` returned `null` and `case null` took it. And `3.5` falls through to `default`.

## Pin the behaviour with 39 checks

I couldn't pull JUnit from Maven Central in my environment, so these are plain Java: a `check` method that compares and counts. They run the same assertions you'd write in JUnit 5.

`src/test/java/com/example/grades/Checks.java`

```java
package com.example.grades;

import com.example.grades.Submission.Absent;
import com.example.grades.Submission.Coursework;
import com.example.grades.Submission.Exam;
import com.example.grades.Submission.Project;
import java.util.ArrayList;
import java.util.List;

public class Checks {

    private static int passed;
    private static int failed;

    private static void check(String name, Object expected, Object actual) {
        if (expected.equals(actual)) {
            passed++;
            System.out.println("ok    " + name);
        } else {
            failed++;
            System.out.println("FAIL  " + name + ": expected " + expected + " but got " + actual);
        }
    }

    private static void checkThrows(String name, Class<? extends Throwable> type, Runnable code) {
        try {
            code.run();
            failed++;
            System.out.println("FAIL  " + name + ": nothing thrown");
        } catch (Throwable t) {
            check(name, type, t.getClass());
        }
    }

    public static void main(String[] args) {
        check("autumn weeks", 12, Grader.teachingWeeks(Term.AUTUMN));
        check("spring weeks", 12, Grader.teachingWeeks(Term.SPRING));
        check("summer weeks", 8, Grader.teachingWeeks(Term.SUMMER));

        check("100 is A", 'A', Grader.letter(100));
        check("90 is A", 'A', Grader.letter(90));
        check("89 is B", 'B', Grader.letter(89));
        check("85 is B (no fall-through)", 'B', Grader.letter(85));
        check("79 is C", 'C', Grader.letter(79));
        check("60 is D", 'D', Grader.letter(60));
        check("59 is F", 'F', Grader.letter(59));
        check("0 is F", 'F', Grader.letter(0));
        checkThrows("101 rejected", IllegalArgumentException.class, () -> Grader.letter(101));
        checkThrows("-1 rejected", IllegalArgumentException.class, () -> Grader.letter(-1));

        check("band of 95", Band.DISTINCTION, Grader.band(95));
        check("band of 85", Band.MERIT, Grader.band(85));
        check("band of 65", Band.PASS, Grader.band(65));
        check("band of 12", Band.FAIL, Grader.band(12));

        check("report distinction", "Distinction. Offer the honours seminar.", Grader.report(91));
        check("report merit", "MERIT. 9 more marks for distinction.", Grader.report(81));
        check("report singular mark", "MERIT. 1 more mark for distinction.", Grader.report(89));
        check("report pass", "PASS. 8 more marks for merit.", Grader.report(72));
        check("report fail", "Fail. Book a resit.", Grader.report(40));

        check("exam score", 91, Grader.finalScore(new Exam(91)));
        check("coursework average rounds", 81, Grader.finalScore(new Coursework(List.of(78, 84, 80))));
        check("empty coursework", 0, Grader.finalScore(new Coursework(List.of())));
        check("late project loses 10", 62, Grader.finalScore(new Project("p", 72, true)));
        check("late project floors at 0", 0, Grader.finalScore(new Project("p", 4, true)));
        check("on-time project", 72, Grader.finalScore(new Project("p", 72, false)));
        check("absent", 0, Grader.finalScore(new Absent("illness")));

        List<Integer> marks = new ArrayList<>(List.of(50, 60));
        Coursework cw = new Coursework(marks);
        marks.add(100);
        check("coursework copies its list", 55, Grader.finalScore(cw));

        check("describe null", "no value", Grader.describe(null));
        check("describe int", "score 88 (B)", Grader.describe(88));
        check("describe big int", "out of range: 140", Grader.describe(140));
        check("describe negative", "out of range: -3", Grader.describe(-3));
        check("describe numeric string", "score 67 (D)", Grader.describe(" 67 "));
        check("describe bad string", "no value", Grader.describe("abc"));
        check("describe blank", "blank", Grader.describe("  "));
        check("describe submission", "submission worth 91", Grader.describe(new Exam(91)));
        check("describe other type", "unsupported: Double", Grader.describe(3.5));

        System.out.println();
        System.out.println(passed + " passed, " + failed + " failed");
        if (failed > 0) {
            System.exit(1);
        }
    }
}

```

```bash
java -cp out com.example.grades.Checks

```

```log
ok    autumn weeks
ok    spring weeks
ok    summer weeks
ok    100 is A
ok    90 is A
ok    89 is B
ok    85 is B (no fall-through)
ok    79 is C
ok    60 is D
ok    59 is F
ok    0 is F
ok    101 rejected
ok    -1 rejected
ok    band of 95
ok    band of 85
ok    band of 65
ok    band of 12
ok    report distinction
ok    report merit
ok    report singular mark
ok    report pass
ok    report fail
ok    exam score
ok    coursework average rounds
ok    empty coursework
ok    late project loses 10
ok    late project floors at 0
ok    on-time project
ok    absent
ok    coursework copies its list
ok    describe null
ok    describe int
ok    describe big int
ok    describe negative
ok    describe numeric string
ok    describe bad string
ok    describe blank
ok    describe submission
ok    describe other type

39 passed, 0 failed

```

To move these to JUnit 5, turn each `check(...)` into `assertEquals(expected, actual)` inside an `@Test` method, and each `checkThrows` into `assertThrows`. I haven't run that conversion here. A good next read is the series' JUnit tutorial once it's up: [How to Write Unit Tests with JUnit 5](https://ggorantala.dev/how-to-write-unit-tests-junit-5-java/?ref=ggorantala.dev).

## What javac does with a switch

Three things happen that are worth knowing, because they explain every error in the next section.

**Exhaustiveness.** A switch *expression* must produce a value for every possible input. For an enum the compiler lists the constants; for a sealed type it lists the permitted subtypes; for anything else (`int`, `char`, `String`, `Object`) you need a `default`. You can drop `default` for enums and sealed types, and the compiler becomes your reviewer.

**Dominance.** Pattern labels are tested top to bottom, and the compiler rejects a label that an earlier label already covers completely. That's why `Coursework c` has to follow `Coursework c when ...`.

**No fall-through in arrow form.** Each arrow arm is its own body: an expression, a block, or a `throw`. Under the hood, a switch on an enum is compiled against a synthetic lookup (that's the `Grader$1.class` file in `out/`), and a pattern switch is compiled into a dynamic call that picks the matching branch at runtime. You never write either; you only need to know the cost isn't a chain of `instanceof` tests at your call site.

Two neighbours worth a mention. `instanceof` got the same binding trick in Java 16, which is covered in [How to Use instanceof Pattern Matching](https://ggorantala.dev/how-to-use-instanceof-pattern-matching-java/?ref=ggorantala.dev). And `case Exam e` can be taken apart further, as `case Exam(int score)`, using record patterns, covered in [How to Use Record Patterns](https://ggorantala.dev/how-to-use-record-patterns-java/?ref=ggorantala.dev). I kept this tutorial to type patterns so each idea stands on its own.

## Seven mistakes, on purpose

Each of these is a real change I made to the finished project, with the exact compiler output.

**1\. A new enum constant.** Add `WINTER` to `Term` and recompile.

```log
src/main/java/com/example/grades/Grader.java:13: error: the switch expression does not cover all possible input values
        return switch (term) {
               ^
1 error

```

The message doesn't name the missing constant, which surprised me. Look at every `switch` over `Term`. That's the feature working: the compiler found the spot you'd have missed.

**2\. A dominated label.** Move the general `case Coursework c` above the guarded one.

```log
src/main/java/com/example/grades/Grader.java:60: error: this case label is dominated by a preceding case label
            case Coursework c when c.marks().isEmpty() -> 0;
                 ^
1 error

```

The error points at the label that's now unreachable. Reorder so specific cases come first.

**3\. A block arm with no `yield`.** Delete the word `yield` from `report`.

```log
src/main/java/com/example/grades/Grader.java:49: error: not a statement
                band + ". " + gap + " more mark" + (gap == 1 ? "" : "s") + " for " + next + ".";
                                                                                          ^
1 error

```

`not a statement` is the compiler saying you wrote a bare value inside a block. Put `yield` back.

**4\. Mixing arrows and colons.** Change `case 8 -> 'B';` to `case 8: return 'B';` in `letter`.

```log
src/main/java/com/example/grades/Grader.java:25: error: different case kinds used in the switch
            case 8: return 'B';
            ^
src/main/java/com/example/grades/Grader.java:25: error: attempt to return out of a switch expression
            case 8: return 'B';
                    ^
2 errors

```

A switch is either all arrows or all colons. And you can't `return` out of a switch expression, so even the second error is correct.

**5\. A missing `default`.** Delete the `default -> 'F';` line from `letter`.

```log
src/main/java/com/example/grades/Grader.java:23: error: the switch expression does not cover all possible input values
        return switch (score / 10) {
               ^
1 error

```

Same message as the enum case, different cause. Integers can't be listed out, so the compiler wants the catch-all.

**6\. A new sealed subtype.** Add `record Withdrawn() implements Submission { }` to `Submission`.

```log
src/main/java/com/example/grades/Grader.java:56: error: the switch expression does not cover all possible input values
        return switch (submission) {
               ^
1 error

```

Exactly what you wanted. `finalScore` needs a `Withdrawn` arm.

**7\. A duplicate label.** Change `case 7 -> 'C';` to `case 8 -> 'C';`.

```log
src/main/java/com/example/grades/Grader.java:26: error: duplicate case label
            case 8 -> 'C';
            ^
1 error

```

And two runtime and version snags, because they bite later rather than at compile time.

A `null` selector with no `case null` still throws. This is a plain `switch` on a `String` that is `null`:

```log
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.hashCode()" because "<local3>" is null
	at NullDemo.main(NullDemo.java:4)

```

The `<local3>` is the missing variable name; compile with `-g` to get the real one. Fix: add `case null ->`, or check for `null` before the switch. There's also an existing article on exactly this at [Java switch NullPointerException](https://ggorantala.dev/java-switch-nullpointerexception-null-selector-case-null/?ref=ggorantala.dev).

And compiling pattern switch for an older release fails like this (`--release 17`):

```log
src/main/java/com/example/grades/Grader.java:57: error: patterns in switch statements are not supported in -source 17
            case Exam e -> e.score();
                 ^
  (use -source 21 or higher to enable patterns in switch statements)
1 error

```

If you see `patterns in switch statements are not supported`, you're on an older language level than your JDK. In the IDE, that's the project's *Language level* (IntelliJ) or *Compiler compliance level* (Eclipse). In a build tool, the `release` setting.

## Before you copy this into a codebase

- **Avoid `default` on enums and sealed types you own.** The missing-case error is the main reason to use the new form at all. Keep `default` for `int`, `String`, `char` and `Object`.
- **Guard order is logic.** Put narrow `when` cases first, the general one last, and read the result top to bottom as if it were an `if` chain, because that's what it is.
- **Old `switch` statements can stay.** You don't need to rewrite working code. I'd convert one the next time I touch it, particularly if it has fall-through you can't explain.
- **Don't use `switch` as a visitor replacement everywhere.** If behaviour belongs to the type, put it on the type. The pattern switch is best when the logic is about a closed set of data shapes, as `finalScore` is.
- **`yield` inside old-style colon switch expressions** also works (`case 1: yield 'x';`), but I haven't met a reason to prefer it.
- **Newer JDKs.** Java 22 made unnamed patterns (`_`) final; on 21 they're a preview feature. I didn't run this on 22 or 25.

## What to remember

- Put `return switch` or `var x = switch` and use arrows; the old colon form is for existing code.
- Group constants in one label: `case A, B ->`.
- Drop `default` for enums and sealed types; keep it for everything else.
- Use `yield` to return from a block arm.
- Put narrow `when` guards before general cases.
- Add `case null` when `null` is a legal input.

## Switch questions that come up a lot

**What's the difference between a switch statement and a switch expression in Java?** An expression yields a value and must cover every input; a statement runs code and may skip cases. Both can use arrows. Expressions arrived in Java 14.

**Can a switch expression use pattern matching?** Yes, from Java 21\. The labels become types with optional `when` guards, and the compiler checks exhaustiveness for sealed types.

**How do I handle `null` in a Java switch?** Add `case null ->` (Java 21). Without it, a `null` selector throws `NullPointerException`. You can combine it as `case null, default ->` to treat it like everything else.

**Do I need `default` in a switch expression?** Only when the compiler can't list every possibility. Enums and sealed types: no. `int`, `String`, `Object`: yes.

## References

- [JEP 361: Switch Expressions](https://openjdk.org/jeps/361?ref=ggorantala.dev), observed 2026-10-26
- [JEP 441: Pattern Matching for switch](https://openjdk.org/jeps/441?ref=ggorantala.dev), observed 2026-10-26
- [JEP 440: Record Patterns](https://openjdk.org/jeps/440?ref=ggorantala.dev)
- Java Language Specification, chapter 14.11, switch statements and expressions