Appearance
Lesson 08 · Pattern Matching
Objectives
After this lesson you will be able to:
- Use
instanceofpatterns with binding variables and flow scoping. - Use type patterns in
switch, withnulland guarded (when) labels. - Reason about exhaustiveness and dominance ordering.
- Deconstruct records with record patterns, including nesting and
var.
instanceof patterns
A type pattern tests and binds in one step — no separate cast:
java
Object o = "hello";
if (o instanceof String s && s.length() > 3) {
System.out.println(s.toUpperCase()); // s is in scope and definitely a String
}The binding s is in scope only where the pattern definitely matched — this is flow scoping. It even works across &&/|| and early returns:
java
if (!(o instanceof String s) || s.isEmpty()) return; // s is in scope in `|| s.isEmpty()`
System.out.println(s.length()); // and below — control only reaches here when matchedType patterns in switch
A switch can match on type, replacing long if/else instanceof chains:
java
String describe(Object o) {
return switch (o) {
case null -> "null";
case Integer i -> "int " + i;
case String s -> "str " + s.length();
default -> "other";
};
}null is handled only if you add a case null — you can fold it into the default with the combined label case null, default:
java
case null, default -> "fallback"; // null and everything unmatched share this labelExam trap
Without a case null, a null selector throws NullPointerException. case null is the one label allowed to precede the others; everywhere else, pattern labels must be ordered specific-first.
Exhaustiveness & dominance
A pattern switch must be exhaustive — every possible value handled:
- over a sealed type, covering all permitted subtypes suffices (no
defaultneeded); - otherwise you need a
defaultor a total type pattern (e.g.case Object o).
Labels are checked for dominance: a label that can never be reached because an earlier one already matches everything it would is a compile error. Put specific types before their supertypes.
java
switch (o) {
case CharSequence cs -> ...; // matches String too
case String s -> ...; // COMPILE ERROR — dominated by CharSequence above
}Gotcha
A guarded label (when) does not dominate the same type, because the guard might be false. So case String s when s.isEmpty() must still be followed by an unguarded case String s (or a default) for the switch to be exhaustive.
Guarded patterns (when)
Add a boolean guard with when to refine a case:
java
String size(Object o) {
return switch (o) {
case String s when s.length() > 10 -> "long string";
case String s -> "short string"; // needed: guard above isn't total
default -> "not a string";
};
}Record patterns (deconstruction)
A record pattern matches a record and binds its components directly — and nests. Components may use explicit types or var:
java
record Point(int x, int y) { }
record Line(Point from, Point to) { }
String f(Object o) {
return switch (o) {
case Line(Point(var x1, var y1), Point(var x2, var y2)) ->
"from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")";
case Point(int x, int y) -> "point " + x + "," + y;
default -> "?";
};
}SDET note
Pattern matching plus sealed types (Lesson 06) gives exhaustive, branch-complete handling the compiler verifies. For parsing/validation logic this removes a whole class of "forgot a case" bugs — exactly the kind of subtle gap to check in AI-generated switch code.
Key Takeaways
- An
instanceoftype pattern tests and binds at once; the binding follows flow scoping — usable across&&/||and after an early return when the match is guaranteed. - A pattern
switchmatches by type; addcase null(orcase null, default) to handle null, or a barenullselector throws NPE. - A pattern switch must be exhaustive: all permitted subtypes of a sealed type, or a
default/total pattern otherwise. Order specific before general — a dominated label won't compile. whenadds a boolean guard; a guarded label is not total, so an unguarded fallback is still required for exhaustiveness.- Record patterns deconstruct components (and nest), binding fields directly with explicit types or
var.
Lesson Quiz
In if (o instanceof String s) { ... }, where is s usable?
A pattern switch with no case null receives a null selector. What happens?
Why does this NOT compile?
switch (o) { case CharSequence cs -> "cs"; case String s -> "str"; }A switch over a non-sealed Object with only case Integer i and case String s (no default). Is it exhaustive?
After case String s when s.length() > 3, why is an unguarded case String s still needed?
What does the record pattern case Point(var x, var y) do?
What does when add to a case label?
Which label legally handles both null and any unmatched value?
Next: Module 03 Mini-Exam. Run the matching code in labs/src/main/java/com/jse21/m03_oop/.