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

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

より具体的には、JDK 14で、instanceof演算子が拡張されており、バインディング変数を指定できます。instanceof演算子の結果がtrueの場合、テスト対象のオブジェクトは、バインディング変数に割り当てられます。

注意:

これは、設計、仕様および実装は完了しているが、永続的でないプレビュー機能であり、将来のJDKリリースでは別の形式で存在するか、完全になくなる可能性があることを意味します。プレビュー機能が含まれているコードをコンパイルして実行するには、追加のコマンド行オプションを指定する必要があります。「プレビュー機能」を参照してください。

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

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

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

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

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

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

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

注意:

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

パターンは、ターゲットに適用可能な述語と、述語が一致した場合にのみターゲットから抽出される一連のバインディング変数の組合せです。述語は、ある引数のブール値を持つ関数です。この場合は、Shape引数がRectangleであるかCircleであるかをテストするinstanceof演算子が述語です。ターゲットは述語の引数で、Shape引数がそれに当たります。バインディング変数は、述語がtrueを返す場合にのみターゲットのデータを格納する変数で、変数sがそれに当たります。

タイプ・テスト・パターンは、タイプを指定する述語と、単一のバインディング変数で構成されます。この例では、タイプ・テスト・パターンはRectangle sCircle sです。

バインディング変数のスコープ

バインディング変数のスコープは、instanceof演算子が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.
        }
    }

バインディング変数のスコープは、それが導入された文を超えて拡張できます。

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

バインディング変数は、if文の式で使用できます:

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

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

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

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

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