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 pattern switch: exhaustive branches and null policy
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
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
dispatched=81:south
absentCost 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.
