instanceof演算子のパターン・マッチング

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

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

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

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

ノート:

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

パターンは、述語と呼ばれるテスト、ターゲット、およびパターン変数と呼ばれる一連のローカル変数の組合せですgetPerimeterの例には、s instanceof Rectangle rs instanceof Circle cの2つのパターンが含まれています:

  • 述語は、引数が1つのboolean値関数です。これら2つのパターンで、これはinstanceof演算子で、s引数がRectangleCircleのどちらかをテストします。
  • ターゲットは述語の引数です。これら2つのパターンで、これはs値です。
  • パターン変数は、述語がtrueの場合のみ、ターゲットのデータを格納するものです。これら2つのパターンで、これらはrcです。

タイプ・パターンは、タイプを指定する述語と、単一のパターン変数で構成されます。この例では、タイプ・パターンは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は使用できません。

パターン変数を使用できるその他の例は、「パターン変数宣言のスコープ」を参照してください。