pattern matching
instanceof patterns, switch patterns, guards and dominance.
- chapter
- chapter 08.05
- reading time
- 8 min read
- java 21+
a pattern tests a value and pulls it apart in one go. o instanceof String s checks the type and hands you s, already cast, in exactly the places where the test is known to be true (flow scoping). final in java 16.
in a switch java 21+, every case can be a pattern: case Integer i when i > 0. you get guards (when), case null, and two compile-time checks that catch real bugs: dominance (a case that can never match) and exhaustiveness (an input that matches nothing).
the picture
you know the instanceof dance? check the type, then cast to the type you just checked, then name it, three times saying the same thing:
if (o instanceof String) {
String s = (String) o;
IO.println(s.strip().length());
}if (o instanceof String s) {
IO.println(s.strip().length());
}the cast was never the point, it was noise you had to write because the compiler didn't connect the check to the next line. a pattern makes the connection.
think of the sorting belt at a post office. the old way: a worker checks if a parcel is a letter, shouts "letter!", and a second worker picks it up and puts a "letter" sticker on it. the new way: one scanner that checks the shape and slaps the sticker on in the same motion. and the sticker only exists on parcels that passed.
instanceof patterns and flow scoping
the variable in a pattern (s) isn't scoped to a block like a normal local. it exists wherever javac can prove the match succeeded, and nowhere else. that sounds abstract until you see the shapes:
- 1if (!(o instanceof String s)) {
- 2 return 0;
- 3}
- 4return s.length(); // s is here!
the clever one. if the test failed, the method returned. so every line after the if knows the test passed, and s is in scope for the rest of the method. great for guard clauses.
s is in scope. a red line uses s where the match isn't guaranteed, and javac 25 rejects it. the "if (!...) return" shape is the one worth remembering.void main() {
Object o = " hello ";
if (o instanceof String) {
String s = (String) o;
IO.println(s.strip().length());
}
if (o instanceof String s) {
IO.println(s.strip().length());
}
if (o instanceof String s && !s.isBlank()) {
s = s.strip(); // pattern variables aren't final
IO.println(s);
}
}5 5 hello
patterns in switch
instanceof tests one pattern. a switch tests a list of them, top to bottom, and the first one that matches wins. each case can add a guard with when, a boolean that has to be true too:
static String describe(Object o) {
return switch (o) {
case null -> "null";
case Integer i when i < 0 -> "negative int " + i;
case Integer i -> "int " + i;
case String s when s.isBlank() -> "blank string";
case String s -> "string of length " + s.length();
case int[] a -> "int[] of " + a.length;
default -> "something else: " + o;
};
}
void main() {
Object[] inputs = { null, -3, 42, " ", "hi", new int[] {1, 2, 3}, 3.14, List.of(1) };
for (Object o : inputs) IO.println(describe(o));
}null negative int -3 int 42 blank string string of length 2 int[] of 3 something else: 3.14 something else: [1]
- 1static String describe(Object o) {
- 2 return switch (o) {
- 3 case null -> "null";✗ not null
- 4 case Integer i when i < 0 -> "negative int " + i;← match
- 5 case Integer i -> "int " + i;
- 6 case String s when s.isBlank() -> "blank string";
- 7 case String s -> "string of length " + s.length();
- 8 case int[] a -> "int[] of " + a.length;
- 9 default -> "something else: " + o;
- 10 };
- 11}
negative int -3
tested 2 cases, top to bottom. a label fails either on its type test or on its when guard, and the next one gets a go. the first match wins, and everything below it is never considered.
case null line and pass null. outputs are real jdk 25.(that's the mental model. what javac actually emits is a single invokedynamic to SwitchBootstraps.typeSwitch, which works out the matching case index in one call instead of a chain of instanceofs.)
dominance
because the first match wins, order matters, and javac checks it. if a case can never be reached because an earlier one always matches first, the later case is dominated, and that's a compile error, not a warning:
- supertype first
case CharSequence csthencase String s: everyStringis aCharSequence, so the second case is dead.- unguarded first
case Integer ithencase Integer i when i > 0: the guard can never get a look in.- pattern before constant
case Integer ithencase 42(switching on anInteger): 42 already matched.- default first
defaultthencase String s: with patterns in play,defaulthas to come after them.
all four get the same message from javac 25:
error: this case label is dominated by a preceding case labelthe fix is always to go from specific to general: constants, then guarded patterns, then plain patterns, then the catch-all.
Object o = 5;
String r = switch (o) {
case Integer i when i > 0 -> "positive";
case Number n -> "number";
case Integer i -> "int";
default -> "other";
};
IO.println(r);null and exhaustiveness
two things the old switch did that pattern switches keep, and one they add:
nullstill throws, unless you ask. aswitchonnullthrowsNullPointerExceptionbefore testing any case, exactly like before java 21. to handle it, addcase null, or fold it into the catch-all withcase null, default ->.- pattern switches must be exhaustive. a switch expression always had to cover every input; now a switch statement that uses patterns does too. over
Objectthat means adefault(or a total pattern likecase Object x). miss it and javac says the switch statement "does not cover all possible input values". - sealed types make exhaustiveness useful. over a sealed interface, a case per permitted subtype is enough, no
default, and adding a subtype later turns every incomplete switch into a compile error. that's the whole story in sealed types, and it gets better with record patterns.
- a pattern tests and binds at once:
o instanceof String s. - flow scoping: the variable exists exactly where the match is guaranteed, even after an early return.
- in switch, cases are tried top to bottom, first match wins,
whenadds a condition. - a case that can never match is a compile error (dominance). go specific to general.
nullthrows unless there's acase null. pattern switches must be exhaustive.