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

# この記事について

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

+ [record](https://qiita.com/ReiTsukikazu/items/6dc3ec9ea9646c472db0)
+ instanceof パターンマッチング (この記事)
  + [型指定の制約について](https://qiita.com/ReiTsukikazu/items/bfac1d3bbac60915b73e)
+ [値ベースのクラスに対する警告](https://qiita.com/ReiTsukikazu/items/ddcfc8f279017e9ba186)

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

## 出典

+ [JEP 394: Pattern Matching for instanceof](https://openjdk.java.net/jeps/394)
+ [Java SE 16 言語仕様](https://docs.oracle.com/javase/specs/jls/se16/html/index.html)

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

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

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

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

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



# 基本構文


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

```java
obj instanceof String s
```

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

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

### 簡単な使用例

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

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

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

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

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

## パターン変数のスコープ

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

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

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

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

[^diffFromUnassigned]: 「代入されていない」のとは異なる

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

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

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

一方で，

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

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

[^fieldIsOk]: `s`というフィールド等があるときは別．


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

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

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

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


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

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

[^scopeKakko]: スコープは`()`の外には出るが`{}`の外には出ないので，ややこしい…

```java
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 は使えない
}
```


### 応用例

例えば，これまで

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

と書いていた処理は

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

と書き直せて，かなり見やすくなります．


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

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

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

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

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

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

[^futureDoubleDef]: [Java15のプレビュー仕様](https://docs.oracle.com/en/java/javase/15/docs/specs/patterns-instanceof-jls.html#jls-6.3.1)では「将来のバージョンで緩和される可能性がある」とされていました



## ローカル変数であること

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

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


## 自明な変換の禁止

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

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

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

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

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

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

この制約に関しては[別の記事](https://qiita.com/ReiTsukikazu/items/bfac1d3bbac60915b73e)でも紹介しているので，良ければご覧ください．

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

### `null`の扱い

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

# あとがき

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