Skip to content

Lesson 08 · Pattern Matching

Objectives

After this lesson you will be able to:

  • Use instanceof patterns with binding variables and flow scoping.
  • Use type patterns in switch, with null and 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 matched

Type 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 label

Exam 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 default needed);
  • otherwise you need a default or 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 instanceof type 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 switch matches by type; add case null (or case null, default) to handle null, or a bare null selector 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.
  • when adds 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

Lesson Quiz · Pattern Matching0 / 8
  1. In if (o instanceof String s) { ... }, where is s usable?

    • AAnywhere in the method
    • BOnly inside the if block where the match holds
    • COnly in the condition
    • DNowhere — you still need a cast
  2. A pattern switch with no case null receives a null selector. What happens?

    • AMatches default
    • BReturns null
    • CNullPointerException
    • DCompile error
  3. Why does this NOT compile?

    switch (o) {
      case CharSequence cs -> "cs";
      case String s -> "str";
    }
    • AMissing default
    • Bcase String is unreachable — dominated by case CharSequence
    • CPatterns aren't allowed in switch
    • Dcs is unused
  4. A switch over a non-sealed Object with only case Integer i and case String s (no default). Is it exhaustive?

    • AYes
    • BNo — needs a default or a total pattern like case Object o
    • COnly if Object is sealed
    • DYes, null is implied
  5. After case String s when s.length() > 3, why is an unguarded case String s still needed?

    • AIt isn't — the guard covers all strings
    • BA guarded label isn't total (the guard may be false), so it doesn't make the switch exhaustive
    • CGuards aren't allowed
    • DTo handle null
  6. What does the record pattern case Point(var x, var y) do?

    • ACalls Point's constructor
    • BMatches a Point and binds its components to x and y
    • CCreates a new Point
    • DCompares x and y
  7. What does when add to a case label?

    • AA loop
    • BA boolean guard condition
    • CA default
    • DException handling
  8. Which label legally handles both null and any unmatched value?

    • Acase default null ->
    • Bcase null, default ->
    • Ccase null | default ->
    • Ddefault null ->

Next: Module 03 Mini-Exam. Run the matching code in labs/src/main/java/com/jse21/m03_oop/.