Java 21's pattern matching makes data-oriented code concise and lets the compiler prove you handled every case.
Java 21 finalised a set of features that work together for data-oriented programming.
Pattern matching for instanceof tests and binds in one step, removing the cast: if (obj instanceof String s && !s.isEmpty()) { use(s); }.
A sealed class or interface lists exactly which types may extend it:
sealed interface Shape permits Circle, Rect, Triangle { }
record Circle(double r) implements Shape { }Permitted subclasses must be final, sealed or non-sealed (records are implicitly final). Because the compiler knows every possible subtype, it can check exhaustiveness.
Pattern matching for switch lets cases match on type, and record patterns destructure records into their components, even nested:
double area = switch (shape) {
case Circle(double r) -> Math.PI * r * r;
case Rect(double w, double h) -> w * h;
case Triangle t -> t.area();
};No default is needed: the switch over a sealed type is exhaustive. When someone adds a new permitted type, every such switch stops compiling until it handles it — the same safety you get with enums, now for whole data hierarchies.
Cases can add a guard with when: case Rect(double w, double h) when w == h -> "square";. Order matters: the compiler rejects a case that is dominated by an earlier, more general one. A switch with patterns can also handle case null explicitly; otherwise a null selector throws NullPointerException.
var works inside patterns (case Rect(var w, var h)). Unnamed patterns _ are a preview in Java 21 and final in 22.
This style complements, rather than replaces, polymorphism: use virtual methods when behaviour belongs with the type, and sealed types plus switch when operations live outside the data — for example interpreting an AST or handling API results.
Example
public class Patterns {
sealed interface Shape permits Circle, Rect, Group { }
record Circle(double r) implements Shape { }
record Rect(double w, double h) implements Shape { }
record Group(Shape a, Shape b) implements Shape { }
static double area(Shape s) {
return switch (s) { // exhaustive: no default
case Circle(double r) -> Math.PI * r * r;
case Rect(double w, double h) -> w * h;
case Group(Shape a, Shape b) -> area(a) + area(b);
};
}
static String describe(Object o) {
return switch (o) {
case null -> "nothing";
case Rect(var w, var h) when w == h -> "square of side " + w;
case Rect r -> "rectangle " + r.w() + "x" + r.h();
default -> "something else";
};
}
public static void main(String[] args) {
System.out.printf("%.2f%n", area(new Group(new Circle(1), new Rect(2, 3)))); // 9.14
System.out.println(describe(new Rect(2, 2)) + " / " + describe(null)); // square of side 2.0 / nothing
}
}Remember
sealed ... permitscloses a hierarchy so the compiler knows all subtypes- Record patterns destructure;
whenguards refine a case - Switches over sealed types are exhaustive — new subtypes cause compile errors, not runtime bugs
Watch out for
- Putting a general case (e.g.
case Rect r) before a guarded, more specific one — the compiler rejects the dominated case.