Pattern Matching in Java
By the Syncfusion DevTutorials team · Last updated
Check a value's type and use it in one step with pattern matching for instanceof and switch, including guards, null cases and record patterns.
Think about sorting items at a post office.
A worker picks up each item and asks one question: “What kind of item is this?” A letter goes into the letter rack. A parcel goes on the parcel shelf. A heavy parcel goes on the floor.
The worker does two things at once: they check what the item is, and they handle it in the right way.
Pattern matching gives Java the same idea. In one step, it checks what type a value is and gives you a variable of that type, ready to use.
In simple words: pattern matching tests the shape of a value and, if it matches, gives you the value in a ready-to-use form.
What You Will Learn
In this lesson, you will learn:
- What pattern matching is and why Java added it.
- How pattern matching works with
instanceof. - What type patterns are.
- How pattern variables work and where you can use them.
- How pattern matching compares with traditional casting.
- How to use patterns inside
switch, including guards and records.
Start With a Working Example
Press Run.
One switch handles three different shapes
public class Main {
public static void main(String[] args) {
Object[] shapes = {
new Circle(3),
new Square(4),
new Rectangle(2, 5)
};
for (Object s : shapes) {
System.out.println(describe(s));
}
}
static String describe(Object shape) {
return switch (shape) {
case Circle c -> "Circle with area " + Math.round(3.14 * c.radius() * c.radius());
case Square s -> "Square with area " + (s.side() * s.side());
case Rectangle r -> "Rectangle with area " + (r.width() * r.height());
default -> "Unknown shape";
};
}
}
record Circle(int radius) { }
record Square(int side) { }
record Rectangle(int width, int height) { }
Expected output:
Circle with area 28
Square with area 16
Rectangle with area 10
case Circle c checks whether shape is a Circle. If it is, Java creates a variable c of type Circle, ready to use. There is no separate check and no cast.
Try This
Add a fourth item to the array: "a triangle". It is a String, so it matches none of the cases, and the default line prints Unknown shape.
What is Pattern Matching?
Pattern matching means testing whether a value has a certain shape, such as a certain type, and, if it does, pulling the parts you need out of it in the same step.
A pattern is the thing you test against. In Java, the most common pattern is a type pattern, like String s, which means "is it a String? If yes, call it s".
Pattern matching was added to Java in stages:
| Feature | Java version |
|---|---|
Pattern matching for instanceof | Java 16 |
| Record patterns (taking a record apart) | Java 21 |
Pattern matching for switch | Java 21 |
The goal is the same each time: less code, fewer casts, and fewer mistakes.
Pattern Matching for instanceof
You have used instanceof to check an object's type. Before Java 16, it only gave you true or false. You then had to cast the object yourself.
With pattern matching, you add a variable name after the type:
if (obj instanceof String s) {
// s is a String here
}
If obj is a String, the check is true, and s is a String variable holding the same object. If not, the check is false, and s is never created.
Check and get a typed variable in one step
public class Main {
public static void main(String[] args) {
Object[] items = {"Chennai", 42, 3.5, true};
for (Object item : items) {
if (item instanceof String text) {
System.out.println("Text with " + text.length() + " letters: " + text);
} else if (item instanceof Integer number) {
System.out.println("Whole number, doubled: " + (number * 2));
} else if (item instanceof Double decimal) {
System.out.println("Decimal, halved: " + (decimal / 2));
} else {
System.out.println("Something else: " + item);
}
}
}
}
Expected output:
Text with 7 letters: Chennai
Whole number, doubled: 84
Decimal, halved: 1.75
Something else: true
Inside each branch, the variable already has the right type. text.length() works because text is a String. number * 2 works because number is an Integer.
Type Patterns
A type pattern has two parts: a type and a variable name.
String text
│ │
type pattern variable
It matches when the value is that type, or a child of that type. For example, the pattern Number n matches an Integer, a Double and a Long, because they all extend Number.
A type pattern never matches null. null instanceof String s is false, so you never get a null pattern variable.
A type pattern also matches child types
public class Main {
public static void main(String[] args) {
Object[] values = {12, 4.5, 9_000_000_000L, "12", null};
for (Object v : values) {
if (v instanceof Number n) {
System.out.println(v + " is a Number, as a double: " + n.doubleValue());
} else {
System.out.println(v + " is not a Number");
}
}
}
}
Expected output:
12 is a Number, as a double: 12.0
4.5 is a Number, as a double: 4.5
9000000000 is a Number, as a double: 9.0E9
12 is not a Number
null is not a Number
Integer, Double and Long all matched Number n. The text "12" did not, because a String is not a Number, even if it contains digits.
null did not match either. That makes pattern matching safe: a pattern variable is never null.
9.0E9 is how Java prints large decimals: 9.0 × 10⁹.
Pattern Variables
A pattern variable is the variable a pattern creates, such as text in item instanceof String text.
Pattern variables follow special rules about where they exist. A pattern variable only exists where Java is sure the pattern matched. This is called flow scoping.
Inside the if Block
The most common case: the variable exists inside the if block.
On the Right of &&
With &&, the right side only runs if the left side was true. So the pattern variable can be used straight away on the right:
if (obj instanceof String s && s.length() > 5) { ... }
This does not work with ||. With ||, the right side runs when the left side is false, so the pattern did not match.
After a Negated Check That Exits
If you check !(obj instanceof String s) and leave the method when it is true, the variable exists after the if, because the only way to get there is if the pattern matched.
Where a pattern variable can be used
public class Main {
public static void main(String[] args) {
check("Bengaluru");
check("Goa");
check(2026);
}
static void check(Object value) {
if (value instanceof String city && city.length() > 5) {
System.out.println(city + " is a long city name");
}
if (!(value instanceof String name)) {
System.out.println(value + " is not text, stopping");
return;
}
System.out.println(name + " has " + name.length() + " letters");
}
}
Expected output:
Bengaluru is a long city name
Bengaluru has 9 letters
Goa has 3 letters
2026 is not text, stopping
In the first if, city is used on the right of &&.
In the second if, the method returns when value is not a String. So after that if, Java knows the pattern matched, and name can be used on the last line.
A pattern variable cannot have the same name as another variable already in scope. If the method already has a variable called s, write obj instanceof String text instead of obj instanceof String s.
Pattern Matching vs Traditional Casting
Before pattern matching, you had to write three steps:
- Check the type with
instanceof. - Cast the object to that type.
- Store the result in a new variable.
// Traditional
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
// Pattern matching
if (obj instanceof String s) {
System.out.println(s.length());
}
The traditional way repeats the type name three times. It also allows a nasty mistake: checking for one type and casting to another.
The old way and the new way give the same result
public class Main {
public static void main(String[] args) {
Object payment = new UpiPayment("asha@bank", 750);
if (payment instanceof UpiPayment) {
UpiPayment upi = (UpiPayment) payment;
System.out.println("Traditional: " + upi.id() + " paid " + upi.amount());
}
if (payment instanceof UpiPayment upi) {
System.out.println("Pattern: " + upi.id() + " paid " + upi.amount());
}
}
}
record UpiPayment(String id, int amount) { }
Expected output:
Traditional: asha@bank paid 750
Pattern: asha@bank paid 750
Both print the same line. The pattern version is one line shorter, and the type name appears once instead of three times.
| Traditional casting | Pattern matching | |
|---|---|---|
| Steps | Check, cast, store | One step |
| Type written | Three times | Once |
| Cast written by you | Yes | No |
| Risk of casting to the wrong type | Yes, gives ClassCastException | No |
| Variable available | Inside the block, after the cast | Only where the match is certain |
| Works since | Java 1.0 | Java 16 |
Easy Way to Remember: traditional casting is check, cast, store. Pattern matching is check and store, and the cast is done for you.
Pattern Matching for switch
Since Java 21, switch can match patterns, not just fixed values like numbers and strings. Each case can be a type pattern.
switch (obj) {
case Integer i -> ...
case String s -> ...
default -> ...
}
Java checks the cases from top to bottom and runs the first one that matches. This replaces long chains of if ... else if ... instanceof.
Guards With when
A guard adds an extra condition to a case. You write it with when:
case Integer i when i > 100 -> "big number"
The case only matches if the type matches and the condition is true. Put more specific cases, like guarded ones, above general ones.
Handling null
A normal switch throws a NullPointerException when the value is null. With pattern matching, you can add case null to handle it safely.
Type patterns, guards and null in one switch
public class Main {
public static void main(String[] args) {
Object[] inputs = {250, 12, -5, "hello", "", 9.99, null};
for (Object in : inputs) {
System.out.println(in + " -> " + classify(in));
}
}
static String classify(Object value) {
return switch (value) {
case null -> "no value";
case Integer i when i > 100 -> "a big number";
case Integer i when i < 0 -> "a negative number";
case Integer i -> "a small number";
case String s when s.isEmpty() -> "empty text";
case String s -> "text in capitals: " + s.toUpperCase();
default -> "some other value";
};
}
}
Expected output:
250 -> a big number
12 -> a small number
-5 -> a negative number
hello -> text in capitals: HELLO
-> empty text
9.99 -> some other value
null -> no value
250 matched the first Integer guard. 12 passed neither guard, so it fell to the plain case Integer i.
9.99 is a Double, which no case mentions, so default handled it.
case null caught the null safely, instead of throwing an exception.
Pattern matching for switch and record patterns need Java 21 or newer. On an older Java version, use if ... instanceof patterns, which work from Java 16.
Order Matters
A case that matches everything a later case would match makes the later case unreachable. Java refuses to compile it:
A general case above a specific one is an error
public class Main {
public static void main(String[] args) {
Object value = 500;
String result = switch (value) {
case Integer i -> "any number";
case Integer i when i > 100 -> "big number";
default -> "other";
};
System.out.println(result);
}
}
The compiler reports this case label is dominated by a preceding case label. Every Integer is caught by the first case, so the second can never run. Swap the two cases to fix it.
Record Patterns
A record pattern takes a record apart in the case itself. You list the record's components in brackets, and each one becomes a variable.
case Point(int x, int y) -> "x is " + x + ", y is " + y
You no longer need to call p.x() and p.y().
Sealed Types and switch
With a sealed interface, Java knows every possible type. If your switch covers them all, you do not need a default case. If you later add a new type to the sealed interface, every switch that misses it stops compiling, which reminds you to handle it.
Record patterns over a sealed type, with no default
public class Main {
public static void main(String[] args) {
Payment[] payments = {
new Cash(500),
new Card("4321", 1200),
new Upi("ravi@bank", 250)
};
int total = 0;
for (Payment p : payments) {
System.out.println(describe(p));
total += amount(p);
}
System.out.println("Total collected: " + total);
}
static String describe(Payment p) {
return switch (p) {
case Cash(int amount) -> "Cash: " + amount;
case Card(String last4, int amount) -> "Card ending " + last4 + ": " + amount;
case Upi(String id, int amount) -> "UPI from " + id + ": " + amount;
};
}
static int amount(Payment p) {
return switch (p) {
case Cash c -> c.amount();
case Card c -> c.amount();
case Upi u -> u.amount();
};
}
}
sealed interface Payment permits Cash, Card, Upi { }
record Cash(int amount) implements Payment { }
record Card(String last4, int amount) implements Payment { }
record Upi(String id, int amount) implements Payment { }
Expected output:
Cash: 500
Card ending 4321: 1200
UPI from ravi@bank: 250
Total collected: 1950
case Card(String last4, int amount) checks that p is a Card, then pulls out its two components as variables.
Neither switch has a default. Java knows Payment can only be Cash, Card or Upi, and all three are covered.
| Old style | Pattern matching style |
|---|---|
if (p instanceof Cash) { Cash c = (Cash) p; ... } | case Cash c -> ... |
A long if ... else if chain | One switch with a case per type |
c.amount() after a cast | case Cash(int amount) -> ... |
if (x == null) before the switch | case null -> ... |
Extra if checks inside a branch | case Integer i when i > 100 -> ... |
Common mistakes
The following mistakes are common when using pattern matching. Each row shows what you see, why it happens and how to fix it.
| Symptom | Cause | Fix |
|---|---|---|
cannot find symbol for a pattern variable | It was used where the match is not certain | Use it only inside the matching branch or after && |
dominated by a preceding case label | A general case comes before a specific one | Put specific and guarded cases first |
does not cover all possible input values | The switch is not complete | Add default, or cover every type of a sealed interface |
NullPointerException from a switch | The value was null and there is no case null | Add case null |
already defined error on a pattern variable | Its name clashes with another variable | Choose a different name |
What to Remember
- Pattern matching checks a value's shape and, if it matches, gives you the parts in a ready-to-use form.
obj instanceof String schecks the type and creates a typed variable, with no cast.- A type pattern also matches child types, and never matches
null. - A pattern variable exists only where the match is certain: inside the
if, after&&, or after a negated check that exits. - Pattern matching replaces check, cast and store with one safer step.
switchcan match type patterns, add guards withwhen, handlecase null, and take records apart with record patterns.- Put specific cases first. A
switchover a sealed type can skipdefaultwhen every type is covered.
Next: Cheat Sheet