※本記事は、Mala Guptaによる”Pattern Matching for instanceof in Java 14“を翻訳したものです。


instanceofのパターン・マッチングでJavaのinstanceof演算子の使用が簡略化されるため、コードが安全になり書きやすくなる
著者:Mala Gupta
2020年5月18日

 

多くのJava開発者は、参照変数と型を比較する際にinstanceof演算子を使用しています。この結果がtrueである場合、次の手順は明らかです。比較対象とした型に明示的にキャストして、そのメンバーにアクセスします。この手順には、型と比較 – 真であれば – 型にキャストという繰り返しが含まれています。

次のようなコードをよく見かけます。

1. void outputValueInUppercase(Object obj) {
2.     if (obj instanceof String) {               
3.       String str = (String) obj;            
4.       System.out.println(str.toUpperCase());  
5.     }
6. }

このコードでは、2行目で参照変数objとString型を比較しています。結果がtrueである場合、3行目のコードでローカル変数strを定義し、objを明示的にString型にキャストして変数strに代入します。4行目のコードでは、strが参照するStringの値のメンバーにアクセスできます。

次のコードは、instanceofのパターン・マッチングを使って、上記の冗長なコードを除去する方法を示しています。具体的には、String型の直後にパターン変数strを記述するという形式でinstanceof演算子を使っています。

1. void outputValueInUppercase(Object obj) {
2.     if (obj instanceof String str) {
3.         System.out.println(str.toUpperCase());
4.     }
5. }

このコードでは、instanceofの条件がtrueである場合、パターン変数strは変数objが参照するインスタンスにバインドされます。これにより、toUpperCase()メソッドを呼び出す前に、新しい変数を定義する必要や、その変数を明示的にStringにキャストする必要はなくなります。

パターン変数

パターン変数は、宣言と同時に初期化されたfinalなローカル変数です。その他のfinalなローカル変数の場合、宣言だけを行い、後で代入することも可能です。なお、パターン変数は暗黙的にfinalであるため、別の値を代入することはできません。

パターン変数のスコープは限られています。elseブロックでアクセスしようとした場合、エラーが発生します。

これはわかりにくいかもしれません。次のコードでは、PatternMatchingクラスでパターン変数(s)と同じ名前のインスタンス変数を定義していますが、コードはコンパイル可能です。クラスで同名の静的変数を定義している場合も同様です。この場合、elseブロック内のsが参照しているのは、ifブロックのパターン変数ではありません。

public class PatternMatching {
    private String s = "initial value";
    void outputValueInUppercase(Object obj) {
        if (obj instanceof String s) {
            System.out.println(s.toUpperCase()); // refers to pattern var
        } else {
            System.out.println(s.toLowerCase()); // refers to field s
        }
    }
}

equals()メソッドを簡略化する

パターン・マッチングを使うことで表記を簡略化できますが、間違いも犯しがちです。一般的に、開発者は次のようにしてクラスのequals()メソッドをオーバーライドします。次のコードでは、Monitorクラスでmodel(String値)とprice(double値)という2つのフィールドを定義しています。

public class Monitor {
   String model;
   double price;
 
   @Override
   public boolean equals(Object o) {
       if (o instanceof Monitor) {
           Monitor other = (Monitor) o;
           if (model.equals(other.model) && price == other.price) {
               return true;
           }
       }
       return false;
   }
}

instanceofのパターン・マッチングを使い、if文をさらに簡略化することで、上記のequals()メソッドを簡略化することができます。次のコードをご覧ください。

public class Monitor {
    String model;
    double price;

    @Override
    public boolean equals(Object o) {
        return o instanceof Monitor other &&  
            model.equals(other.model) && 
            price == other.price;
    }
}

簡潔で読みやすいコード

instanceofのパターン・マッチングを使用して、さまざまな場所でコードを簡略化することができます。次のコードのisFeasibleメソッドに注目します。

class Project {
    Lang lang;
    Emp projManager;

    private boolean isFeasible(Project project, Location location) {
        if (project.getLang() != Lang.PASCAL) {
            return false;
        }
        if (!(project.getProjManager() instanceof CEO ceo)) {
            return false;
        }
        return ceo.availableAt(location);
    }

    public Emp getProjManager() {
        return projManager;
    }
    public void setProjManager(Emp projManager) {
        this.projManager = projManager;
    }
    public Lang getLang() {
        return lang;
    }
    public void setLang(Lang lang) {
        this.lang = lang;
    }
}
Replace from here to the end of the code listing with

enum Lang {JAVA, PASCAL}
class Emp { }
class Location { }
class CEO extends Emp {
    Location loc;

    boolean availableAt(Location location) {
        return loc.equals(location);
    }
}

次のコードは、instanceofのパターン・マッチングを使用して、isFeasibleメソッドをどのように簡略化できるかを示しています。ここでは、冗長なキャストを除去してから、if文を簡略化しています。

1.    private boolean isFeasible(Project project, Location location) {
2.        return project.getLang() == Lang.PASCAL &&
3.            project.getProjManager() instanceof CEO ceo &&
4.            ceo.availableAt(location);
5.    }

上記のコードでは、3行目でinstanceofのパターン・マッチングを使用しています。

Stream APIでinstanceof のパターン・マッチングを使用する

パターン変数の導入によって、さまざまな改善の可能性が生まれます。processという名前のメソッドが次のように定義されているとします。

void process(Font font, int size) {
    final ArrayList
    list = modules.getChildren();
    for (Iterator
   
     i = list.iterator(); i.hasNext(); ) {
        final Object o = i.next();
        if (o instanceof LetterNode) {
            final LetterNode letterNode = (LetterNode) o;
            if (letterNode.isLatin()) {
                if (!isLetterTrueFont(letterNode.getNodeValue(), font, size)) {
                    i.remove();
                }
            }
        }
    }
}

   
  

上記のコードは、instanceofのパターン・マッチングとStream APIを使用するコードを渡すことで簡略化することができます。次のコードをご覧ください。

void process(Font font, int size) {
    modules.getChildren().removeIf(o -> o instanceof LetterNode letterNode
                               && letterNode.isLatin()
                               && !isLetterTrueFont(letterNode.nodeValue, 
                font, size));
}

コード・ブロックでジェネリクスと複数のinstanceofを使用している場合

instanceofのパターン・マッチングは、ジェネリクスでも動作します。

instanceofのパターン・マッチングを使用できる場所を探すには、instanceof演算子と変数の明示的なキャストを使っている場所を探します。たとえば次のコードでは、instanceof演算子と明示的キャストが複数回使われています。

void processChildNode(Tree tree) {
    if (tree.getChildNodes() instanceof Map) {
        Map<?, Node> childNodes = (Map<?, Node>) tree.getChildNodes();
        if (childNodes.size() == 1) {
            Node = childNodes.get("root");
            if (node instanceof LetterNode) {
                LetterNode = (LetterNode) node;
                System.out.println(letterNode.isLatin());
                }
            }
        }
    }

上記のコード・ブロックは、次のように簡略化することができます。

void processChildNode(Tree tree) {
    if (tree.getChildNodes() instanceof Map<?, Node> childNodes
                 && childNodes.size() == 1
                 && childNodes.get("root") instanceof LetterNode letterNode) {
        System.out.println(letterNode.isLatin());
    }
}

上記の例では、キャストのチェックを行っていません。この点について疑問に思う方のために説明すると、getChildNodes()メソッドはMap<String, Node>型の値を返します。Map<String, Node>をMap<?, Node>にキャストする操作は、アップキャストであるため問題はありません。

instanceofのパターン・マッチングはプレビュー言語機能

instanceofのパターン・マッチングは、JEP 305に基づく、Java 14のプレビュー言語機能としてリリースされました。実際のところ、プレビュー機能とは、開発者に使ってもらう準備は整っているものの、開発者からのフィードバックによっては、今後のJavaリリースで細かい部分が変更される可能性もある機能という意味です。

Javaに6か月の新しいリリース周期が導入されたことで、新しい言語機能はプレビュー機能としてリリースされています。つまり、機能としては完成していますが、確定版ではありません。言語機能はAPIとは異なり、将来において廃止される可能性はありません。そのため、instanceofのパターン・マッチングに関してフィードバックがある方は、JDKメーリング・リスト宛てにお送りください。

プレビュー言語機能を使用するためには、その機能を使用しているコードをコンパイルおよび実行するときに、機能を有効化する必要があります。こうすることで、プレビュー言語機能が誤って使用されることがないようになっています。

instanceofのパターン・マッチングを含むソース・ファイルをコンパイルするためには、–enable-previewオプションと-release 14オプションを使用する必要があります。コマンドラインを使ってJava14.javaというソース・ファイルをコンパイルする例を次に示します。

javac --enable-preview --release 14 Java14.java

プレビュー機能は変更される可能性もあることを強調するため、先ほどのコマンドを実行した際には、図1に示すようなコンパイラ警告が表示されます。

 
図1:プレビュー言語機能が使われているコードに対して表示されるコンパイラ警告

クラスJava14を実行するときも、–enable-previewオプションを使う必要があります。

java --enable-preview Java14

まとめ

Java 14のプレビュー言語機能である、instanceofのパターン・マッチングを使うことで、毎日読み書きするコードを簡略化することができます。instanceof演算子にパターン変数を追加することで、コードが簡略化されて読み書きしやすくなります。Javaの今後のバージョンでは、switch構造やその他の文で使用されるように拡張が行われる可能性があります。
 


Mala Gupta

Mala Gupta(@eMalaGupta):Java Champion。JetBrainsのデベロッパー・アドボケート。eJavaGuru.comの創設者で、認定試験に関する何冊かの人気書籍を執筆。Delhi Java User Groupの共同リーダーであり、Women Who Codeデリー支部のディレクターも務める。