4 Pattern Matching for instanceof

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.

For background information about pattern matching for the instanceof operator, see JEP 394.

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

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

public 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; }
}

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 pattern, making your code shorter and easier to read:

    public static double getPerimeter(Shape shape) throws IllegalArgumentException {
        if (shape instanceof Rectangle r) {
            return 2 * r.length() + 2 * r.width();
        } else if (shape instanceof Circle c) {
            return 2 * c.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, or test, that can be applied to a target and a set of local variables, called pattern variables, that are assigned values extracted from the target only if the test is successful. 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 value. The pattern variables are those that store data from the target only if the predicate returns true, which are the variables r and s.

A type pattern consists of a predicate that specifies a type, along with a single pattern variable. In this example, the type patterns are Rectangle r and Circle c.

Scope of Pattern Variables

The scope of a pattern 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 pattern variable s (of type Rectangle) here.
        } else if (shape instanceof Circle s) {
            // You can use the pattern variable s of type Circle here
            // but not the pattern variable s of type Rectangle.
        } else {
            // You cannot use either pattern variable here.
        }
    }

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

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

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

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

Because the conditional-AND operator (&&) is short-circuiting, the program can reach the r.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 r || r.length() > 0) { // error
            // ...
        }

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