Pattern Matching for instanceof in Java (Java 16 to 25)

Pattern matching for instanceof in Java with type patterns, flow scoping, null and generics rules, equals() examples, record patterns and Java 25 updates.

Scope of a Java instanceof pattern variable: in scope after && and in the true branch, not after

Pattern matching for instanceof lets us test the type of an object and get a variable of that type in one expression, as in obj instanceof String text, so we no longer write a separate cast. The feature has been a standard part of Java since Java 16 (JEP 394), after previews in Java 14 and 15.

We use it wherever code checks a type and reads members of that type right after the check, for example when handling different payment or event types, writing equals() methods, or inspecting the cause of an exception.

The following example tests an Object and reads the length of the String inside it.

Object value = "Hello";
int length = value instanceof String text ? text.length() : 0;   // 5
Object number = 42;
int none = number instanceof String text ? text.length() : 0;    // 0

Notice that text is already a String, and it exists only where the test is true. We compare the pattern with the classic cast, look at the scope rules for the new variable, use the feature in equals() and exception handling, and connect it to record patterns and pattern matching for switch.

1. From instanceof and Cast to a Type Pattern

Before Java 16, the instanceof operator only returned true or false. Every type check was followed by a cast to the same type and a new local variable, which repeats the type name three times and leaves room for copy-paste bugs, such as testing for one type and casting to another.

The following example is a small payment model with three record types, which the rest of the article uses.

public sealed interface Payment permits CardPayment, BankTransfer, Voucher {
}
public record CardPayment(String last4, double amount) implements Payment {
}
public record BankTransfer(String iban, double amount) implements Payment {
}
public record Voucher(String code) implements Payment {
}

With the classic style, describing a payment needs a test, a cast and a declaration for every type.

static String describeOld(Payment payment) {
  if (payment instanceof CardPayment) {
    CardPayment card = (CardPayment) payment;
    return "Card ending " + card.last4();
  } else if (payment instanceof BankTransfer) {
    BankTransfer transfer = (BankTransfer) payment;
    return "Transfer from " + transfer.iban();
  }
  return "Other payment";
}

With a type pattern, the variable name follows the type in the instanceof expression, and the cast disappears.

static String describe(Payment payment) {
  if (payment instanceof CardPayment card) {
    return "Card ending " + card.last4();
  } else if (payment instanceof BankTransfer transfer) {
    return "Transfer from " + transfer.iban();
  }
  return "Other payment";
}
String card = describe(new CardPayment("4242", 25.0));        // "Card ending 4242"
String bank = describe(new BankTransfer("DE89", 120.0));      // "Transfer from DE89"
String other = describe(new Voucher("WELCOME"));              // "Other payment"

The phrase CardPayment card is a type pattern, and card is its pattern variable, also called a binding variable. The compiler generates the same instanceof and checkcast bytecode as the classic version, so the pattern is shorter without being slower.

2. Rules of the Type Pattern

A few rules of the instanceof operator decide when a pattern matches and what we can do with the variable.

2.1. null Never Matches

An instanceof test is false for null, so a pattern never binds null to its variable. We do not need a separate null check before the test.

Object nothing = null;
boolean matched = nothing instanceof String text;   // false

2.2. The Variable Is Not Final

A pattern variable behaves like a local variable, so we can assign it a new value. Java 14 and 15 made it implicitly final, and Java 16 dropped that restriction. When we want the compiler to stop reassignment, we add final to the pattern.

static String normalize(Object input) {
  if (input instanceof String text) {
    text = text.strip();
    return text.toLowerCase();
  }
  if (input instanceof final Integer number) {
    return "#" + number;
  }
  return "";
}
String code = normalize("  ABC ");      // "abc"
String id = normalize(7);               // "#7"

2.3. Generic Types Must Be Safe to Cast

A pattern with a generic type compiles only when the compiler can prove that the cast is safe. A Collection<String> can be tested against List<String>, but an Object cannot, because the type argument is erased at runtime.

Collection<String> names = List.of("Anna", "Lokesh");
boolean isList = names instanceof List<String> list;   // true
// does not compile: Object cannot be safely cast to List<String>
Object data = List.of("Anna");
boolean unsafe = data instanceof List<String> list;

For an Object, we test against the wildcard type List<?> and check the elements one by one.

2.4. A Pattern That Always Matches Is Allowed Since Java 21

In Java 16 to 20, the compiler rejected a pattern whose type is the same as the static type of the expression, such as text instanceof String s where text is a String. Java 21 removed that restriction, so the test compiles and works as a null check that also declares a variable.

String greeting = "Hi";
boolean present = greeting instanceof String s;    // true

3. Scope of the Pattern Variable

A pattern variable is in scope only where the compiler knows that the test was true. This rule is called flow scoping, because the scope follows the possible paths through the code instead of the braces of a block.

Scope of a Java instanceof pattern variable: in scope after && and in the true branch, not after
|, and after an if that returns when the negated test is true|The pattern variable exists wherever the code can only be reached when the instanceof test was true

3.1. Using the Variable With && and ||

The right side of && runs only when the left side is true, so the pattern variable is in scope there. The right side of || runs when the left side is false, so the variable does not exist there and the code does not compile.

Object input = "admin";
boolean longName = input instanceof String name && name.length() > 3;   // true
// does not compile: cannot find symbol, name is not in scope after ||
boolean bad = input instanceof String name || name.isEmpty();

3.2. Negated Tests and Early Returns

When an if statement tests the negated pattern and leaves the method, the code after the if runs only when the test was true. The pattern variable stays in scope after the if, which removes one level of nesting.

static double amountOf(Object item) {
  if (!(item instanceof CardPayment card)) {
    return 0;
  }
  return card.amount();
}
double paid = amountOf(new CardPayment("4242", 25.0));   // 25.0
double skipped = amountOf("not a payment");             // 0.0

A pattern variable is in scope wherever the code can be reached only when the test matched, and nowhere else. Inside the if body of a negated test, the variable is not in scope, and a field with the same name becomes visible there, which is a confusing bug to debug.

4. Pattern Matching in equals() and Exception Handling

Two places in everyday code get noticeably shorter with type patterns, namely equals() methods and exception handling.

4.1. Writing equals() in One Expression

An equals() method checks the type of its argument, casts it and compares the fields. With a type pattern and &&, the whole method is one return statement, and null is handled because instanceof returns false for it.

public final class Money {
  private final long cents;
  private final String currency;

  public Money(long cents, String currency) {
    this.cents = cents;
    this.currency = currency;
  }

  @Override
  public boolean equals(Object o) {
    return o instanceof Money other
        && cents == other.cents
        && currency.equals(other.currency);
  }

  @Override
  public int hashCode() {
    return Objects.hash(cents, currency);
  }
}
boolean same = new Money(500, "EUR").equals(new Money(500, "EUR"));   // true
boolean differs = new Money(500, "EUR").equals(new Money(500, "USD")); // false
boolean withNull = new Money(500, "EUR").equals(null);                // false

4.2. Checking the Cause of an Exception

Say a sign-up service saves a user, and the repository wraps every database error in a RuntimeException. The service must show “Email already registered” when the database reports a unique key violation, which has the SQL state 23505 in PostgreSQL. A type pattern reads the state from the cause in a single condition.

static String signUpError(RuntimeException ex) {
  if (ex.getCause() instanceof SQLException sql && "23505".equals(sql.getSQLState())) {
    return "Email already registered";
  }
  return "Sign-up failed, please try again";
}
String duplicate = signUpError(new RuntimeException(new SQLException("duplicate key", "23505")));   // "Email already registered"
String timeout = signUpError(new RuntimeException("timeout"));   // "Sign-up failed, please try again"

5. Record Patterns and Pattern Matching for switch

Java 21 extended pattern matching in two directions, and both build on the type pattern from section 1. Record patterns take a record apart in the instanceof test, and pattern matching for switch tests one value against many patterns.

A record pattern lists the components of the record in parentheses, and each component gets its own variable. The guide to record patterns covers nested patterns in depth.

Payment payment = new CardPayment("4242", 25.0);
String label = payment instanceof CardPayment(String last4, double amount) && amount > 20
        ? "Large card payment " + last4
        : "Other";
String result = label;                  // "Large card payment 4242"

When code tests the same value against three or more types, a switch with type patterns reads better than an if chain. Because Payment is a sealed interface, the compiler checks that every permitted type has a case, so the switch needs no default.

static double fee(Payment payment) {
  return switch (payment) {
    case CardPayment card -> card.amount() * 0.02;
    case BankTransfer transfer -> 0.5;
    case Voucher voucher -> 0.0;
  };
}
double cardFee = fee(new CardPayment("4242", 25.0));      // 0.5
double voucherFee = fee(new Voucher("WELCOME"));          // 0.0

We keep instanceof patterns for one or two types and for conditions combined with &&, and move to switch when the list of types grows or when we want the compiler to report a missing case.

6. Pattern Matching Versions From Java 14 to 25

Pattern matching arrived in small steps, and older articles often name the version of a preview instead of the final release.

Java versionJEPChange
14JEP 305Pattern matching for instanceof, first preview
15JEP 375Second preview, no changes
16JEP 394Final; pattern variables no longer implicitly final
21JEP 440, JEP 441Record patterns and pattern matching for switch final; unconditional instanceof patterns allowed
25JEP 507Primitive types in patterns, third preview

Primitive patterns are still a preview feature in Java 25, so the following test compiles only with –enable-preview. It checks whether an int fits into a byte without losing information.

// does not compile: primitive patterns are a preview feature in Java 25 (needs --enable-preview)
int level = 42;
boolean fits = level instanceof byte small;

7. Pattern Matching for instanceof FAQs

Many tutorials were written during the preview years, so their version numbers and performance claims often disagree with current Java.

7.1. Is Pattern Matching for instanceof a Java 16 or Java 17 Feature?

Java 16 made it a standard feature through JEP 394. Java 14 and 15 had it only as a preview that needed –enable-preview. Any Java 17, 21 or 25 compiler supports it without flags, which is why some articles call it a Java 17 feature, since 17 was the first long-term support release that included it.

7.2. Is Pattern Matching Slower Than a Cast?

No. The compiler turns obj instanceof String s into the same instanceof check followed by a checkcast that the hand-written cast produces, which we can confirm with javap -c.

7.3. Can a Pattern Variable Have the Same Name as Another Variable?

A pattern variable cannot reuse the name of a local variable or parameter that is in scope, which is a compile error. It can have the same name as a field, and in that case the field is visible wherever the pattern variable is not in scope, as section 3 warns.

7.4. Can We Combine Two Patterns in One Condition?

Yes, with &&, as in a instanceof String x && b instanceof String y, and both variables are in scope in the if body. With ||, neither variable is in scope after the operator, because either test may have failed.

7.5. Does Pattern Matching Work With Interfaces and Arrays?

Yes. The type in the pattern can be any reference type, including an interface such as CharSequence or an array type such as int[], as in obj instanceof int[] numbers.

8. Conclusion

Pattern matching for instanceof merges the type test, the cast and the variable declaration into one expression. It has been final since Java 16, never matches null, and produces the same bytecode as the classic cast.

The pattern variable follows flow scoping, so it works after && and after a negated test that returns early, but not after ||. For two types or a combined condition, instanceof patterns are the right tool, whereas record patterns and switch patterns from Java 21 take over when we take records apart or test many types. For the rest of the basics, see the Java tutorial.

9. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.