instanceofのパターン・マッチング

パターン・マッチングでは、オブジェクトに特定の構造があるかどうかをテストし、一致がある場合には、そのオブジェクトからデータを抽出します。これはJavaでも実行できることですが、パターン・マッチングには新しい言語拡張が導入されており、より簡潔で強力なコードを使用してオブジェクトから条件付きでデータを抽出できます。

instanceof演算子のパターン・マッチングに関する背景情報は、JEP 394を参照してください。

特定の形状の周辺の長さを計算する次のコードを見ていきます:

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

メソッドgetPerimeterは、次を実行します:

  1. Shapeオブジェクトのタイプを判断するためのテスト
  2. instanceof演算子の結果に応じて、ShapeオブジェクトをRectangleまたはCircleにキャストする変換
  3. Shapeオブジェクトから長さと幅または半径を抽出することによる分解

パターン・マッチングを使用すると、instanceof演算子の2番目のオペランドがタイプ・パターンで変更され、変換のステップがなくなるため、短くて読みやすいコードになります:

    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");
        }
    }

ノート:

この変換のステップがなくなることで、コードもより安全になります。instanceofでオブジェクトのタイプをテストし、キャストを使用してそのオブジェクトを新しい変数に割り当てると、アプリケーションでコーディング・エラーが発生することがあります。あるオブジェクト(テスト対象のオブジェクトまたは新しい変数のいずれか)のタイプを変更し、その他のオブジェクトのタイプの変更を忘れてしまう場合があります。

パターンは、ターゲットおよびパターン変数と呼ばれるローカル変数のセットに適用できる述語(テスト)の組合せで、テストが成功した場合にのみターゲットから抽出された値が割り当てられます。述語は、ある引数のブール値を持つ関数です。この場合は、Shape引数がRectangleであるかCircleであるかをテストするinstanceof演算子が述語です。ターゲットは述語の引数で、Shape値がこれに当たります。パターン変数は、述語がtrueを返した場合のみ、ターゲットからのデータを格納する変数です。これらは、変数rおよびsです。

タイプ・パターンは、タイプを指定する述語と、単一のパターン変数で構成されます。この例では、タイプ・パターンはRectangle rおよびCircle cです。

パターン変数のスコープ

パターン変数のスコープは、instanceof演算子が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.
        }
    }

パターン変数のスコープは、それを導入した文を超えて拡張できます:

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

パターン変数は、if文の式で使用できます:

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

条件付きAND演算子(&&)は短絡演算子であるため、プログラムがr.length() > 5式に到達できるのは、instanceof演算子がtrueの場合のみです。

逆に、次のような場合には、instanceof演算子でパターン・マッチングを実行することはできません。

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

instanceofがfalseの場合、プログラムはr.length() || 5に到達できるため、ここではパターン変数rは使用できません。