Mastery
Mastery/Java/G. Modern language features (8 → 21)
T1 · high-leverage
Compare with Python: Structural pattern matching in a control-flow statement

Switch expressions + pattern matching for switch + sealed, working together

A sealed interface declares its complete, closed set of permitted implementations. Combine that with pattern-matching switch over the sealed type, and the compiler can verify exhaustiveness -- no default branch needed, and adding a new permitted subtype without updating the switch is a compile error, not a runtime surprise. Record patterns let a case deconstruct a record's components directly, with an optional when guard.

(The sealed interface and its records are declared together inside one wrapper class below -- purely a notebook/JShell mechanic: JShell evaluates each top-level declaration as its own incremental unit and can't resolve the circular permits/implements reference across separate snippets the way a real .java file compiling everything together can. Nesting them inside one class makes the whole hierarchy a single unit again. This has no bearing on the language feature itself -- in a real source file you'd write these as ordinary top-level or sibling types.)

java
class Shapes {
    sealed interface Shape permits Circle, Square, Rectangle {}
    record Circle(double radius) implements Shape {}
    record Square(double side) implements Shape {}
    record Rectangle(double w, double h) implements Shape {}

    static double area(Shape s) {
        return switch (s) {
            case Circle c -> Math.PI * c.radius() * c.radius();
            case Square sq -> sq.side() * sq.side();
            case Rectangle r -> r.w() * r.h();
            // no default -- the compiler knows Circle/Square/Rectangle are the ONLY permitted shapes
        };
    }
}

for (Shapes.Shape shape : List.of(new Shapes.Circle(2), new Shapes.Square(3), new Shapes.Rectangle(2, 5))) {
    System.out.printf("%s area = %.2f%n", shape.getClass().getSimpleName(), Shapes.area(shape));
}

Record patterns deconstruct components directly in the case, and when adds a guard condition:

java
Shapes.Shape s = new Shapes.Rectangle(4, 5);
String desc = switch (s) {
    case Shapes.Rectangle(double w, double h) when w == h -> "square-shaped rectangle";
    case Shapes.Rectangle(double w, double h) -> "rectangle " + w + "x" + h;
    case Shapes.Circle(double r) -> "circle r=" + r;
    case Shapes.Square(double side) -> "square " + side;
};
System.out.println(desc);

Removing a case leaves the switch non-exhaustive -- a real compile error, reproduced verbatim (two-shape sealed interface, switch only handles one):

SealedFail.java:7: error: the switch expression does not cover all possible input values
        return switch (s) {
               ^
1 error

Interview angle

A strong "do you know modern Java" question with real depth to probe: ask why a sealed interface plus a pattern-matching switch lets you drop the default branch, and what happens if someone adds a new permitted subtype later. The correct answer — the compiler forces every exhaustive switch over that sealed type to be updated, turning a class of runtime bugs (an unhandled new case silently falling through) into a compile-time error — is a great signal for someone who thinks about API evolution, not just current-state correctness.

In the industry

sealed plus exhaustive pattern-matching switches are rapidly becoming the idiomatic way to model closed sets of variants in Java 21+ codebases — effectively Java's answer to algebraic data types / discriminated unions from other languages — replacing older patterns like the visitor design pattern or instanceof chains for exactly this use case. Teams modeling domain events, parser ASTs, or API response variants are early, enthusiastic adopters specifically because the compiler-enforced exhaustiveness catches an entire category of "forgot to handle the new case" bugs at build time instead of in production.