【Modern Java】Java16で正式リリースとなった instanceof パターンマッチング (JEP 394: Pattern Matching for instanceof)

f:id:Naotsugu:20200724174249p:plain

blog1.mammb.com


JEP 394: Pattern Matching for instanceof

インスタンスの型を判断して処理を行う instanceof-and-cast イディオム以下のようになります。

if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s);
}

instanceof パターンマッチングでは、以下のように書くことができるようになります。

if (obj instanceof String s) {
    System.out.println(s);
}

パターン変数 s がキャスト不要で利用可能になります。


例えば以下のようなよくある equals メソッドがあった場合、

public boolean equals(Object o) {
    if (!(o instanceof Point))
        return false;
    Point other = (Point) o;
    return x == other.x
        && y == other.y;
}

instanceof パターンマッチングにより以下のように簡素に記載することができます。

public boolean equals(Object o) {
    return (o instanceof Point other)
        && x == other.x
        && y == other.y;
}


パターン変数のスコープ

パターン変数は、一種のローカル変数と考えることができます。 しかしローカル変数とはスコープの解釈で異なる点があります。

これを一言で表せば、

パターン変数のスコープは、それが確実に一致する範囲内になる

ということになります。 具体的に見てみましょう。

以下の例ではパターン変数のスコープが有効で問題ありません。

if (obj instanceof String s && s.length() > 5) {
    ...
}

しかし以下の例では、パターン変数のスコープ外でコンパイルエラーとなります。

if (obj instanceof String s || s.length() > 5) {    // Error!
    ...
}


もう少し例を見ると、以下の例は問題ありません。

public void onlyForStrings(Object o) throws MyException {
    if (!(o instanceof String s))
        throw new MyException();
    System.out.println(s); // s is in scope
    ...
}

println に到達するのは、パターンマッチングが成功した場合に限られるため、パターン変数 s のスコープには、メソッドブロックの条件ステートメントに続くステートメントが安全に含まれます。

つまり「パターン変数のスコープは、それが確実に一致する範囲内になる」ということになります。


パターン変数によるシャドーイング

パターン変数は、フィールド宣言をシャドーイングできます。

フィールド変数の p と同じ名前でパターン変数を定義した場合は以下のようになります。

class Example2 {

    Point p;

    void test2(Object o) {
        if (o instanceof Point p) {
            // パターン変数 p が有効
        } else {
            // フィールド変数 p が有効(パターン変数 p は無効)
        }
    }
}

シャドーイングで注意が必要なケースがあります。

以下の例です。

class Example1 {
    String s;

    void test1(Object o) {
        if (o instanceof String s) {
            System.out.println(s); // パターン変数 p が有効
            s = s + "\n";          // パターン変数 p への割り当て
        }
        System.out.println(s);     // フィールド変数 p を参照
    }
}

以下のようなケースでは、最後の println の時点では、パターン変数 s はスコープ外となり、フィールド変数を参照することに注意が必要です。


まとめ

instanceof パターンマッチングにより醜いキャストの削減が可能となりました。

もうこのようなコードを書く必要はありません。

public boolean equals(Object o) {
    return (o instanceof CaseInsensitiveString) &&
        ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}

JEP 394 自体の変更は小さなものですが、switch 式でのパターンマッチにつながる内容となります。

Java17 では以下のようにパターン変数を利用することができる予定です。

String formatted = switch (o) {
    case Integer i -> String.format("int %d", i);
    case Long l    -> String.format("long %d", l);
    case Double d  -> String.format("double %f", d);
    case String s  -> String.format("String %s", s);
    default        -> o.toString();
};