Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Java 21 pattern switch: exhaustive branches and null policy

Last updated: 28 Sept 20263 min read
tutorial
IntermediateBy AITrove Editorial

A pattern switch selects a branch by a reference value’s type or structure and can bind matched values for use within that branch.

Java 21+. Use a JDK that supports this release.

Keep the decision exhaustive

A parcel event is either received or dispatched. The switch covers both permitted records and handles null explicitly. Adding a third permitted variant requires revisiting the decision when it is compiled.

The record pattern binds each component directly. This does not validate it; validation belongs at construction or at an input boundary. The sample checks positive parcel IDs in both record constructors.

A general type pattern placed before a more specific one can dominate it, making the later branch unreachable. Guarded cases also need an order that reflects their policy. Compiler rejection helps, but a compiling guard can still express the wrong business rule.

Null is a separate input choice

A default branch does not automatically make a pattern switch null-safe. This program uses case null to report absent input. Another application may deliberately reject null before switching.

Avoid a default that says success when a closed model gains a variant. Exhaustive handling exposes maintenance work at compilation instead of routing an unknown state through a convenient fallback.

This syntax is permanent in Java 21. It does not compile with --release 17 or --release 8; choose the matching compiler release rather than removing checks to make a build pass.

Working program

Java
public class ParcelEvents {
    sealed interface ParcelEvent permits Received, Dispatched { }
    record Received(long parcelId) implements ParcelEvent {
        Received { if (parcelId <= 0) throw new IllegalArgumentException("Invalid ID"); }
    }
    record Dispatched(long parcelId, String depot) implements ParcelEvent {
        Dispatched { if (parcelId <= 0) throw new IllegalArgumentException("Invalid ID"); }
    }
    static String describe(ParcelEvent event) {
        return switch (event) {
            case null -> "absent";
            case Received(long id) -> "received=" + id;
            case Dispatched(long id, String depot) -> "dispatched=" + id + ":" + depot;
        };
    }
    public static void main(String[] args) {
        System.out.println(describe(new Dispatched(81, "south")));
        System.out.println(describe(null));
    }
}

Output

Output
dispatched=81:south
absent

Cost and design choices

Pattern matching does not deep-copy the components it binds. A mutable component is still the same referenced object. This program’s bound long and String keep that rule simple.

Branches are small; text construction dominates this example’s allocation. Do not infer a service-level speed advantage from replacing an if chain alone.

Common Mistakes

  • Do not assume default handles null.
  • Do not place a broad unguarded pattern ahead of its specific cases.
  • Do not use Java 21 syntax while declaring a Java 8 build target.

Connect the contracts

Exhaustiveness depends on the permitted type branches known at compilation.

Review record components before treating a pattern match as a copy of nested mutable data.

java
pattern-switch
Storage details