LoginSignup
4
0

More than 3 years have passed since last update.

[Java16]instanceofパターンマッチングとスコープ範囲

Last updated at Posted at 2021-03-18

この記事について

2020/3/16にリリースされたJava16の言語仕様に関わる変更について,自分の理解を深めるのを兼ねてまとめてみます.プレビュー中の機能は含みません.

instanceofパターンマッチングは一見微妙に新しい形の構文を1つ追加しただけに思えますが,実は変数のスコープに関して複雑な仕様を含んでいて,かなり奥深いものになっています.

出典

Javaにおけるパターンマッチング

Java16で標準化されるinstanceofパターンマッチングは,今後Javaへ導入が予定されている様々なパターンマッチングの一部です.関連して,switch式やrecordが既に導入されています.今後は,switch文・switch式でのパターンマッチングやrecordのデコンストラクションパターンマッチングなどが計画されています.

パターンマッチングの用語

パターンマッチングは,値をパターンと比較し,その値がそのパターンに当てはめられるかどうかで分岐を行うものです.パターンに当てはまった場合にのみ,元の値からパターンに従って取り出した値を利用することが出来ます.

パターンと比較する元の値を「ターゲット」といいます.また,マッチ時に取り出した値を格納するローカル変数を「パターン変数」といいます.

基本構文

instanceofのパターンマッチングは次のような式です.

obj instanceof String s
  • 左辺の obj が「ターゲット」
  • 右辺の s が「パターン変数」
  • String s の部分を「型テストパターン」という

このときのinstanceofを「パターンマッチ演算子(pattern match operator)」,従来のinstanceofを「型比較演算子(type comparison operator)」と呼んで区別します.

簡単な使用例

従来のJavaでは,このようなコードが頻繁に見られました.

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

これを,今後はこのように書き直すことが出来ます.

if (obj instanceof String s) {
    ...
}

つまり,objString であるときのみブロック内が実行され,ブロック内部ではsというString型の変数が利用できます.ブロックの外ではsは使用できません.

パターン変数のスコープ

パターン変数のスコープは,通常のローカル変数のように構文やブロックから単純に決まるのではなく,フロー解析に基づいて決められる「流動的スコープ」となっています.

具体的には,パターン変数のスコープは「パターンがマッチしてパターン変数に値が代入されているとコンパイラが推論できる範囲」に限定されます.いいかえれば,「マッチが成功していることが自明である範囲」が,パターン変数のスコープです.例えば,単純なif文では

// pという変数はスコープ内にない
if (a instanceof Point p) {
   // 1つ目のpのスコープ内
   ...
}
// pという変数はスコープ内にない
if (b instanceof Point p) {
   // 2つ目のpのスコープ内
   ...
}
// pという変数はスコープ内にない

のように,aPoint型でなくても実行される部分は,pのスコープではありません1.従って,1つ目のif文のブロックが終了した後に,新しく別のpという変数を宣言することが出来ます.

if文やwhile文等の条件部だけでなく,条件付き論理演算子や三項演算子による分岐にもこのスコープ制御は適用されます.例えば,

if (obj instanceof String s && s.length() > 5) {
    flag = s.contains("jdk");
}

は,&&の右側が評価されるときには必ずString sが存在するので,s.length() > 5の部分もsのスコープに含まれ,コンパイルできます.

一方で,

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

これは||の右側で有効なスコープを持つsという変数がないことを理由にコンパイルエラーになります.2

パターン変数がif文の中で定義されるとき,スコープがif文のブロックより後に存在する場合があります.例えば

public void onlyForStrings(Object o) throws MyException {
    if (!(o instanceof String s)){
        // ここは s のスコープではない
        throw new MyException();
    }
    // s が使える
    System.out.println(s);
}

一見sのスコープはif文の条件式とブロック内部にしかなさそうに見えますが,このコードはコンパイルできます3

if以外にwhiledofor も同様に,丸かっこで囲われた部分(substatement)内部で宣言されたパターン変数のスコープが本体ブロックの外側に存在する場合があります.

一方で,マッチした場合しか到達しないと推論できる場合でも,スコープがブロックの外側に延長されることはありません4

void test(Object obj){
    {
        if(!(obj instanceof String s)) return;
        // ここでは s を使える
    }
    // ここからは s を使えない

    if(obj.toString().isEmpty()){
        if(!(obj instanceof Number n)) return;
        // ここでは n を使える
    }else{
        return;
    }
    // フロー解析上,ここでは必ず obj instanceof Number が成り立っている
    // しかし,ブロックの外側に出てしまったので,ここでは n は使えない
}

応用例

例えば,これまで

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

と書いていた処理は

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

と書き直せて,かなり見やすくなります.

条件付き演算子の両側での定義

次のコードはJava16でもコンパイルできません.

if(a instanceof String s || b instanceof String s){
    System.out.println(s);
}

これは「sを二重に定義している」というエラーになります.ブロック内を実行するとき,どちらのマッチが成功したかのかはコンパイル時に分からないからです.

しかし,条件付き演算子の特性を考えると,両方のマッチングが評価されてかつ両方がマッチすることはあり得ません.従って,ブロック内が実行されるときsは必ず1回だけ宣言されているので,これはコンパイルできても良いように思えます.

一方で,この例ではsの型は両方Stringなので分かりやすいですが,型が異なる同じ名前のパターン変数を認めるかなど細かい仕様を考えるとかなり煩雑になってしまいます.結果として,現時点5ではこの記法を認めないことになったようです.

ローカル変数であること

パターン変数は,特殊なローカル変数です.従って,既存のローカル変数や引数名と同じ名前のパターン変数を後から宣言することは出来ません.逆に,パターン変数のスコープ内で同じ名前のローカル変数を宣言したりすることもできません.

一方で,やはり通常のローカル変数と同様に,パターン変数と同じ名前のフィールドがあることは許容されます.フィールドを利用したいときはthis.hogeなどの方法で明示できます.

自明な変換の禁止

明らかにマッチしないようなパターン,例えば

String s = "abc";
if(s instanceof Exception ex){
    System.out.println(ex);
}

のようなマッチングはコンパイルエラーになります.これは型比較演算子のinstanceofと同様です.

これに加えてパターンマッチングでは,ターゲットの型がパターン型のサブタイプである場合,つまり例えば

String s = "abc";
if(s instanceof CharSequence cs){
  System.out.println(cs);
}

のようにマッチングが必ず成功する場合もコンパイルエラーになります.

この制約に関しては別の記事でも紹介しているので,良ければご覧ください.

同様に,obj instanceof var s という記述も自明な変換なのでコンパイルエラーになります.

nullの扱い

パターンマッチ演算子instanceof のターゲットの値がnullである場合,型比較演算子のinstanceofと同様に結果はfalseになります.ただし,null というは全てのクラスのsubtypeなので,null instanceof String str はコンパイルエラーになります.

あとがき

特にスコープについてはJEPと言語仕様を見比べながら書いたので,正直あまり自信はないです.何かおかしな部分などあれば,ご指摘ください…


  1. 「代入されていない」のとは異なる 

  2. sというフィールド等があるときは別. 

  3. これは状況によっては新しいアンチパターンを生みそうですね… Linterなんかで警告を出す設定なども出てくるかもしれません. 

  4. スコープは()の外には出るが{}の外には出ないので,ややこしい… 

  5. Java15のプレビュー仕様では「将来のバージョンで緩和される可能性がある」とされていました 

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0