この記事について
2020/3/16にリリースされたJava16の言語仕様に関わる変更について,自分の理解を深めるのを兼ねてまとめてみます.プレビュー中の機能は含みません.
- record
- instanceof パターンマッチング (この記事)
- 値ベースのクラスに対する警告
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) {
...
}
つまり,obj
が String
であるときのみブロック内が実行され,ブロック内部ではs
というString
型の変数が利用できます.ブロックの外ではs
は使用できません.
パターン変数のスコープ
パターン変数のスコープは,通常のローカル変数のように構文やブロックから単純に決まるのではなく,フロー解析に基づいて決められる「流動的スコープ」となっています.
具体的には,パターン変数のスコープは「パターンがマッチしてパターン変数に値が代入されているとコンパイラが推論できる範囲」に限定されます.いいかえれば,「マッチが成功していることが自明である範囲」が,パターン変数のスコープです.例えば,単純なif
文では
// pという変数はスコープ内にない
if (a instanceof Point p) {
// 1つ目のpのスコープ内
...
}
// pという変数はスコープ内にない
if (b instanceof Point p) {
// 2つ目のpのスコープ内
...
}
// pという変数はスコープ内にない
のように,a
がPoint
型でなくても実行される部分は,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
以外にwhile
, do
, for
も同様に,丸かっこで囲われた部分(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と言語仕様を見比べながら書いたので,正直あまり自信はないです.何かおかしな部分などあれば,ご指摘ください…