Java Pattern Matching
Java Pattern matching involves testing whether an object has a particular structure, then extracting data from that object if there’s a match. You can already do this with Java; however, java pattern matching introduces new language enhancements that enable you to conditionally extract data from objects with code that’s more concise and robust.
With Java pattern matching and switch, you can also handle null values by using null as a case label. Also, each case label declares a pattern variable regardless of whether they are used in the corresponding code block. If you are concerned about the missing break labels, they are not required when you use the arrow styles with switch.
History
Java Pattern Matching for switch was proposed as a preview feature by JEP 406 and delivered in JDK 17, and proposed for a second preview by JEP 420 and delivered in JDK 18. This JEP proposes a third preview with further refinements based upon continued experience and feedback.
The main changes since the second preview are:
Guarded patterns are replaced with
whenclauses in switch blocks.The runtime semantics of a pattern switch when the value of the selector expression is
nullare more closely aligned with legacy switch semantics.
A Practical, Developer-Friendly Guide
Pattern Matching has quietly become one of the most transformational features in modern Java (Java 16 → 21). If you’ve been writing Java for years, you’ll immediately feel the difference:
less boilerplate, fewer bugs, cleaner domain logic, and clearer intent.
In this guide, I’ll break down Pattern Matching with a developer’s mindset, share personal examples, use cases, pitfalls, and how it improves daily coding.
Enhance the programming language with Java pattern matching for switch expressions and statements. Extending pattern matching to switch allows an expression to be tested against a number of patterns, each with a specific action, so that complex data-oriented queries can be expressed concisely and safely. This is a preview language feature.
What is Pattern Matching in Java?
Expand the expressiveness and applicability of
switchexpressions and statements by allowing patterns to appear incaselabels.Allow the historical null-hostility of
switchto be relaxed when desired.Introduce two new kinds of patterns: guarded patterns, to allow pattern matching logic to be refined with arbitrary boolean expressions, and parenthesized patterns, to resolve some parsing ambiguities.
Ensure that all existing
switchexpressions and statements continue to compile with no changes and execute with identical semantics.Do not introduce a new
switch-like expression or statement with pattern-matching semantics that is separate from the traditionalswitchconstruct.Do not make the
switchexpression or statement behave differently when case labels are patterns versus when case labels are traditional constants.
Pattern Matching in Java lets you test the type of an object AND extract/assign it in the same expression.
Instead of this old boilerplate:
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}
You write:
if (obj instanceof String s) {
System.out.println(s.toUpperCase());
}
Why Pattern Matching Matters in Modern Java
As a backend engineer, I’ve seen pattern matching drastically reduce:
duplicate casting code
null checks
object mapping noise
brittle if–else chains
verbosity in DTO → Entity conversions
repetitive instanceof checks
It makes Java feel more expressive without losing type safety.
Types of Pattern Matching in Java (Java 16–21)
Java now supports:
Pattern Matching for
instanceof(Java 16)Pattern Matching for
switch(Java 17–21)Record Patterns (Java 21)
Nested Patterns (Java 21)
Dominance & Exhaustiveness rules
We’ll cover all with examples.
1. Pattern Matching for the instanceof Operator
Old Java (Before 16)
if (obj instanceof User u) {
System.out.println(u.getName());
}
New Java (16+)
if (obj instanceof User) {
User u = (User) obj;
System.out.println(u.getName());
}
Why this matters in real projects?
Because in microservices, we constantly deal with:
generic API payloads
polymorphic DTOs
multiple subtypes of events
exception hierarchies
parent → child type flows
Pattern matching cuts the repetitive “check → cast → use”.
The following example calculates the perimeter of the parameter
shapeonly if it’s an instance ofRectangleorCircle:
interface Shape { }
record Rectangle(double length, double width) implements Shape { }
record Circle(double radius) implements Shape { }
...
public static double getPerimeter(Shape shape) throws IllegalArgumentException {
if (shape instanceof Rectangle r) {
return 2 * r.length() + 2 * r.width();
} else if (shape instanceof Circle c) {
return 2 * c.radius() * Math.PI;
} else {
throw new IllegalArgumentException("Unrecognized shape");
}
}
A pattern in Java is a combination of a test, which is called a predicate; a target; and a set of local variables, which are called pattern variables:
- The predicate is a Boolean-valued function with one argument; in this case, it’s the
instanceofoperator testing whether theShapeargument is aRectangleor aCircle. - The target is the argument of the predicate, which is the
Shapevalue. - The pattern variables are those that store data from the target only if the predicate returns
true, which are the variablesrands.
See Pattern Matching for instanceof for more information.
- The predicate is a Boolean-valued function with one argument; in this case, it’s the
2. Record Patterns (Java 21)
Record patterns let you deconstruct record objects directly.
Example:
record Address(String city, String country) {}
record User(String name, Address address) {}
if (user instanceof User(String name, Address(String city, String country))) {
System.out.println(name + " lives in " + city + ", " + country);
}
3. Pattern Matching for switch (Java 17–21)
A
switchstatement transfers control to one of several statements or expressions, depending on the value of its selector expression. In earlier releases of java, the selector expression must evaluate to a number, string orenumconstant, and case labels must be constants. However, in this release, the selector expression can be of any type, andcaselabels can have patterns. Consequently, aswitchstatement or expression can test whether its selector expression matches a pattern, which offers more flexibility and expressiveness compared to testing whether its selector expression is exactly equal to a constant.
Consider the following code that calculates the perimeter of certain shapes from the section Pattern Matching for instanceof:
interface Shape { }
record Rectangle(double length, double width) implements Shape { }
record Circle(double radius) implements Shape { }
...
public static double getPerimeter(Shape shape) throws IllegalArgumentException {
if (shape instanceof Rectangle r) {
return 2 * r.length() + 2 * r.width();
} else if (shape instanceof Circle c) {
return 2 * c.radius() * Math.PI;
} else {
throw new IllegalArgumentException("Unrecognized shape");
}
}
You can rewrite this code to use a pattern switch expression as follows:
public static double getPerimeter(Shape shape) throws IllegalArgumentException {
return switch (shape) {
case Rectangle r -> 2 * r.length() + 2 * r.width();
case Circle c -> 2 * c.radius() * Math.PI;
default -> throw new IllegalArgumentException("Unrecognized shape");
};
}
The following example uses a switch statement instead of a switch expression:
public static double getPerimeter(Shape shape) throws IllegalArgumentException {
switch (shape) {
case Rectangle r: return 2 * r.length() + 2 * r.width();
case Circle c: return 2 * c.radius() * Math.PI;
default: throw new IllegalArgumentException("Unrecognized shape");
}
}
Selector Expression Type
The type of a selector expression in java pattern matching can either be an integral primitive type or any reference type (such as in the previous examples). The following switch expression matches the selector expression obj with type patterns that involve a class type, an enum type, a record type, and an array type:
record Point(int x, int y) { }
enum Color { RED, GREEN, BLUE; }
...
static void typeTester(Object obj) {
switch (obj) {
case null -> System.out.println("null");
case String s -> System.out.println("String");
case Color c -> System.out.println("Color with " + c.values().length + " values");
case Point p -> System.out.println("Record class: " + p.toString());
case int[] ia -> System.out.println("Array of int values of length" + ia.length);
default -> System.out.println("Something else");
}
}Pattern Label Dominance
It’s possible that many pattern labels could match the value of the selector expression in java. To help readability, the labels are tested in the order that they appear in the switch block. In addition, the compiler raises an error when a pattern label can never match because a preceding one will always match first. The following example results in a compile-time error:
static void error(Object obj) {
switch(obj) {
case CharSequence cs ->
System.out.println("A sequence of length " + cs.length());
case String s -> // Error - pattern is dominated by previous pattern
System.out.println("A string: " + s);
default ->
throw new IllegalStateException("Invalid argument");
}
}The first pattern label case CharSequence cs dominates the second pattern label case String s because every value that matches the pattern String s also matches the pattern CharSequence cs but not the other way around. It’s because String is a subtype of CharSequence.
You’ll get a compile-time error if any pattern dominates a subsequent pattern in a switch block.
There are two labels that match all values: the default label and a total type pattern (see Null-Matching case Labels). You can’t have more than one of these two labels in a switch block.
Type Coverage in switch Expressions
As described in Switch Expressions, cases of switch expressions must be exhaustive, which means that for all possible values, there must be a matching switch label in java. The following switch expression is not exhaustive and generates a compile-time error. Its type coverage consists of only types or subtypes of String or Integer, which doesn’t cover all possible values for obj:
return switch (obj) { // Error - incomplete
case String s -> s.length();
case Integer i -> i;
};
}However, the type coverage of the case label default is all types, so the following example compiles:
return switch (obj) { // Error - incomplete
case String s -> s.length();
case Integer i -> i;
default -> 0;
};
}switch expression compiles. It doesn’t need a default case label because its type coverage is the classes A, B, and C, which are the only permitted subclasses of S, the type of the selector expression:sealed interface S permits A, B, C { }
final class A implements S { }
final class B implements S { }
record C(int i) implements S { } // Implicitly final
...
static int testSealedCoverage(S s) {
return switch (s) {
case A a -> 1;
case B b -> 2;
case C c -> 3;
};
}Scope of Pattern Variable Declarations
As described in the section Pattern Matching for instanceof, the scope of a pattern variable is the places where the program can reach only if the instanceof operator is true:
if (shape instanceof Rectangle s) {
// You can use the pattern variable s of type Rectangle here.
} else if (shape instanceof Circle s) {
// You can use the pattern variable s of type Circle here
// but not the pattern variable s of type Rectangle.
} else {
// You cannot use either pattern variable here.
}
}In a switch expression, you can use a pattern variable inside the expression, block, or throw statement that appears right of the arrow. For example:
switch (obj) {
case Character c -> {
if (c.charValue() == 7) {
System.out.println("Ding!");
}
System.out.println("Character, value " + c.charValue());
}
case Integer i ->
System.out.println("Integer: " + i);
default ->
throw new IllegalStateException("Invalid argument");
}
}The scope of pattern variable c is the block to the right of case Character c ->. The scope of pattern variable i is the println statement to the right of case Integer I ->.
In a switch statement, you can use a case label’s pattern variable in its switch-labeled statement group. However, you can’t use it in any other switch-labeled statement group, even if the program flow can fall through a default statement group in java. For example:
switch (obj) {
case Character c:
if (c.charValue() == 7) {
System.out.print("Ding ");
}
if (c.charValue() == 9) {
System.out.print("Tab ");
}
System.out.println("character, value " + c.charValue());
default:
// You cannot use the pattern variable c here:
throw new IllegalStateException("Invalid argument");
}
}The scope of Java pattern variable c consists of the case Character c statement group: the two if statements and the println statement that follows them. The scope doesn’t include the default statement group even though the switch statement can execute the case Character c statement group, fall through the default case label, and then execute the default statement group in java.
You will get a compile-time error if it’s possible to fall through a case label that declares a pattern variable. The following example doesn’t compile:
switch (obj) {
case Character c:
if (c.charValue() == 7) {
System.out.print("Ding ");
}
if (c.charValue() == 9) {
System.out.print("Tab ");
}
System.out.println("character");
case Integer i: // Compile-time error
System.out.println("An integer " + i);
default:
System.out.println("Neither character nor integer");
}
}obj, was a Character, then the switch statement can execute the case Character c statement group, then fall through the case Integer i case label, where the pattern variable i would have not been initialized.Similarly, you can’t declare multiple pattern variables in a case label. The following aren’t permitted; either c or i would have been initialized (depending on the value of obj).
case Character c, Integer i -> ...Null-Matching case Labels
Prior to this Java preview feature, switch expressions and switch statements throw a NullPointerException if the value of the selector expression is null. However, pattern labels offer more flexibility – there are now two new null-matching case labels. First, a null case label is available:
switch (obj) {
case null -> System.out.println("null!");
case String s -> System.out.println("String");
default -> System.out.println("Something else");
}
}This example prints null! when obj is null instead of throwing a NullPointerException.
Second, a pattern label whose pattern is a total type pattern matches null if the value of the selector expression is null. A type pattern, T t, is total for a type S if the type erasure of S is a subtype of the type erasure of T. For example, the type pattern Object obj is total for the type String. Consider the following switch statement:
switch (s) {
case Object obj -> ... // total type pattern, so it matches null!
}Common Mistakes Developers Make
1. Forgetting null handling
Pattern matching does NOT match null unless you explicitly handle it.
2. Incorrect order of guarded patterns
More specific guards must appear first.
3. Misusing record patterns for huge records
Pattern matching is not a replacement for business validation.
Performance Considerations
Pattern Matching is optimized by JVM:
uses constant-time type checks
no extra allocations
sealed-class switches may compile down to tableswitch/lookupswitch
Generally faster than deep if instanceof chains.cccc
Conclusion: Pattern Matching Makes Java More Modern
Pattern Matching is one of those features that:
reduces boilerplate
improves readability
fits naturally with sealed classes
supports better domain modeling
enhances functional styles
makes Java feel more like 2025
If you upgrade your Java codebase today, pattern matching is one of the fastest wins.
FAQ Section
1. Does Pattern Matching slow down Java?
No. It is optimized and often faster than nested instanceof checks.
2. Is Pattern Matching only for records?
No — it works with classes, interfaces, sealed types, records.
3. Do I need to upgrade to Java 21?
Recommended.
Java 21 is LTS and includes full pattern matching features.
4. Is pattern matching replacing polymorphism?
No. It’s additive.
You can still use classic OOP with overridden methods.
5. Does this affect older frameworks like Spring Boot?
Works perfectly with Spring Boot 3.x (Java 17–21 compatible).
6. Does it help with JSON → object parsing?
Yes — especially with record patterns & nested patterns.


Leave a Reply