3 Pattern Matching for the instanceof Operator

Pattern matching involves testing whether an object has a particular structure, then extracting data from that object if there's a match. You can already do this with Java; however, pattern matching introduces new language enhancements that enable you to conditionally extract data from objects with code that's more concise and robust.

More specifically, JDK 14 extends the instanceof operator: you can specify a binding variable; if the result of the instanceof operator is true, then the object being tested is assigned to the binding variable.

Note:

This is a preview feature, which is a feature whose design, specification, and implementation are complete, but is not permanent, which means that the feature may exist in a different form or not at all in future JDK releases. To compile and run code that contains preview features, you must specify additional command-line options. See Preview Features.

For background information about pattern matching for the instaceof operator, see JEP 305.

Consider the following code the calculates the perimeter of certain shapes:

public interface Shape { }

final class Rectangle implements Shape {
    final double length;
    final double width;
    
    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    
    double length() { return length; }
    double width() { return width; }
}

public class Circle implements Shape {
    final double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    double radius() { return radius; }
}

    public static double getPerimeter(Shape shape) throws IllegalArgumentException {
        if (shape instanceof Rectangle) {
            Rectangle s = (Rectangle) shape;
            return 2 * s.length() + 2 * s.width();
        } else if (shape instanceof Circle) {
            Circle s = (Circle) shape;
            return 2 * s.radius() * Math.PI;
        } else {
            throw new IllegalArgumentException("Unrecognized shape");
        }
    }

The method getPerimeter performs the following:

  1. A test to determine the type of the Shape object
  2. A conversion, casting the Shape object to Rectangle or Circle, depending on the result of the instanceof operator
  3. A destructuring, extracting either the length and width or the radius from the Shape object

Pattern matching enables you to remove the conversion step by changing the second operand of the instanceof operator with a type test pattern, making your code shorter and easier to read:

    public static double getPerimeter(Shape shape) throws IllegalArgumentException {
        if (shape instanceof Rectangle s) {
            return 2 * s.length() + 2 * s.width();
        } else if (shape instanceof Circle s) {
            return 2 * s.radius() * Math.PI;
        } else {
            throw new IllegalArgumentException("Unrecognized shape");
        }
    }

Note:

Removing this conversion step also makes your code safer. Testing an object's type with the instanceof, then assigning that object to a new variable with a cast can introduce coding errors in your application. You might change the type of one of the objects (either the tested object or the new variable) and accidentally forget to change the type of the other object.

A pattern is a combination of a predicate that can be applied to a target and a set of binding variables that are extracted from the target only if the predicate successfully matches it. The predicate is a Boolean-valued function of one argument; in this case, it’s the instanceof operator testing whether the Shape argument is a Rectangle or a Circle. The target is the argument of the predicate, which is the Shape argument. The binding variables are those that store data from the target only if the predicate returns true, which is the variable s.

A type test pattern consists of a predicate that specifies a type, along with a single binding variable. In this example, the type test pattens are Rectangle s and Circle s.

Scope of Binding Variables

The scope of a binding variable are the places where the program can reach only if the instanceof operator is true:

    public static double getPerimeter(Shape shape) throws IllegalArgumentException {
        if (shape instanceof Rectangle s) {
            // You can use the binding variable s (of type Rectangle) here.
        } else if (shape instanceof Circle s) {
            // You can use the binding variable s of type Circle here
            // but not the binding variable s of type Rectangle.
        } else {
            // You cannot use either binding variable here.
        }
    }

The scope of a binding variable can extend beyond the statement that introduced it:

    public static boolean bigEnoughRect(Shape s) {
        if (!(s instanceof Rectangle r)) {
            // You cannot use the binding variable r here.
            return false;
        }
        // You can use r here.
        return r.length() > 5; 
    }

You can use a binding variable in the expression of an if statement:

        if (shape instanceof Rectangle s && s.length() > 5) {
            // ...
        }

Because the conditional-AND operator (&&) is short-circuiting, the program can reach the s.length() > 5 expression only if the instanceof operator is true.

Conversely, you can't pattern match with the instanceof operator in this situation:

        if (shape instanceof Rectangle s || s.length() > 0) { // error
            // ...
        }

The program can reach the s.length() || 5 if the instanceof is false; thus, you cannot use the binding variable s here.