Back to Blog
Java

Java Type Patterns: instanceof and switch

java type patterns: Learn how Java type patterns simplify instanceof checks and switch statements, with practical examples of pattern variables, guards, and record pat...

pattern matchinginstanceofswitch expressionstype patternsrecord patterns
Illustration of Java type patterns showing a variable being matched against multiple types in a switch statement.

Java type patterns, introduced as a preview in Java 16 and standardized in later releases, replace the conventional instanceof-then-cast idiom with a single, type-safe construct. Instead of writing a separate cast after an instanceof check, you declare a pattern variable directly in the condition. The compiler then guarantees that the variable is in scope only when the type check succeeds, eliminating a whole class of boilerplate and reducing the risk of casting errors.

The Problem with Traditional instanceof Checks

Before type patterns, a typical instanceof check required two steps: verify the type, then cast. This pattern is verbose and easy to get wrong, especially when the cast target changes during refactoring.

Object value = getValue(); if (value instanceof String) { String text = (String) value; System.out.println(text.length()); }

The cast is redundant because the instanceof check already guarantees the type. Worse, the variable text is scoped to the entire block, so it is possible to accidentally use it outside the branch if the code is rearranged. Java type patterns address both issues by binding the variable directly in the condition.

Basic Type Pattern Syntax

A type pattern consists of a type name and a pattern variable. The variable is declared in the pattern and assigned the value if the instanceof test succeeds.

if (value instanceof String text) { System.out.println(text.length()); }

Here, text is only in scope inside the if block. The compiler knows that text is a String, so no cast is needed. This works for any reference type, including interfaces and abstract classes. The pattern variable is effectively final, meaning you cannot reassign it after it is bound.

Pattern Variables and Flow Scoping

The scope of a pattern variable is determined by flow analysis. The variable is definitely assigned only in the code paths where the pattern matches. This behavior is called flow scoping.

if (value instanceof String text) { // text is in scope System.out.println(text.toUpperCase()); } // text is not in scope here

Flow scoping also works with logical operators. For example, in a conjunction (&&), the pattern variable is in scope in the right-hand side and the body. In a disjunction (||), the variable is only in scope in the right-hand side if the left-hand side fails, but not after the entire expression.

if (value instanceof String text && text.length() > 5) { // text is in scope and length > 5 }

This behavior prevents accidental use of a pattern variable when the type check might not have succeeded, making the code safer and more predictable.

Using Type Patterns in Switch Statements

Java 21 standardized pattern matching for switch expressions and statements. You can use type patterns as case labels, allowing a switch to dispatch on the runtime type of the selector expression.

Object value = getValue(); String result = switch (value) { case String s -> "String: " + s; case Integer i -> "Integer: " + i; case null -> "null"; default -> "Unknown type"; };

Each case label uses a type pattern. The null case is separate because type patterns do not match null by default. The default case handles any other type. The switch expression is exhaustive when a default is present or when all possible types are covered. This form is much more concise than a chain of if-else statements and keeps the logic in one place.

Guarded Patterns for Additional Conditions

Sometimes a type match alone is not enough. You need to check an additional property of the value. Guarded patterns, written with when, let you combine a type pattern with a boolean condition.

Object value = getValue(); String result = switch (value) { case String s when s.length() > 10 -> "Long string: " + s; case String s -> "Short string: " + s; case Integer i when i < 0 -> "Negative integer"; case Integer i -> "Non-negative integer"; default -> "Other"; };

The guard is evaluated only after the pattern matches. If the guard evaluates to false, the case is not selected and the switch continues to the next case. Guards are also available in if statements using the same when syntax.

if (value instanceof String s when s.length() > 5) { // s is a String with length > 5 }

Guarded patterns reduce the need for nested if statements and make the intent explicit.

Record Patterns for Deconstructing Values

Record patterns extend type patterns to deconstruct record components directly. When a record class matches, you can bind its components to pattern variables in one step.

record Point(int x, int y) {} Object obj = new Point(3, 4); if (obj instanceof Point(int x, int y)) { System.out.println(x + ", " + y); }

The pattern Point(int x, int y) matches any Point instance and binds x and y to the record components. You can nest record patterns to destructure nested records.

record Line(Point start, Point end) {} Object obj = new Line(new Point(0, 0), new Point(10, 10)); if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) { // all four coordinates are bound }

Record patterns work in switch cases as well, enabling concise and type-safe data extraction. This is particularly useful when processing parsed structures or domain objects.

Performance and Compatibility Considerations

Type patterns do not add significant runtime overhead. The instanceof check is the same operation as before; the pattern variable binding is a compile-time convenience. The JVM can optimize repeated type checks, and the bytecode generated is equivalent to a manual cast in most cases. There is no reflection involved.

Compatibility is a more practical concern. Type patterns for instanceof are available since Java 16 as a final feature. Pattern matching for switch was finalized in Java 21. If your codebase runs on an older JDK, you cannot use these constructs without a language-level flag or a backport. For most teams, the minimum Java version is now 17 or 21, so adopting type patterns is straightforward. When migrating, be aware that switch pattern matching changes how null is handled: a case null is required to match null explicitly, whereas a traditional switch on a reference type would throw NullPointerException if the selector is null.

Common Pitfalls and How to Avoid Them

One common mistake is assuming that a type pattern variable is mutable. Pattern variables are effectively final, so you cannot assign a new value to them. If you need to reassign, use a separate variable inside the block.

Another pitfall is forgetting that type patterns do not match null. In an if statement, a null value simply falls through to the else branch. In a switch, you need an explicit case null to handle null; otherwise, a NullPointerException is thrown. Always consider null handling when writing pattern-based code.

A third issue arises with guarded patterns that are too broad. If a guard is not exhaustive, you may need a subsequent case to catch the remaining values. Order matters: the first matching case wins. Place more specific patterns before more general ones. For example, case String s when s.length() > 10 must come before case String s, or the second case would catch all strings and the guard would never be evaluated.

Finally, record patterns require that the record class is accessible and that the component types match exactly. If you use a raw type or a wildcard, the pattern may not compile. Use the exact record type and its component types in the pattern to avoid surprises.

java type patterns: Practical Usage and Code Examples | RYUSLOG DEV