Javaのinstanceofについて言語仕様を見てみる
Javaにおけるinstanceof
について言語仕様がどうなっているかを調べた。
文法上の話
instanceof演算子 (引用 https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.20.2 )
RelationalExpression: ShiftExpression RelationalExpression < ShiftExpression RelationalExpression > ShiftExpression RelationalExpression <= ShiftExpression RelationalExpression >= ShiftExpression RelationalExpression instanceof ReferenceType
これを見ると、instanceof
は左辺に式、右辺に参照型を受け取ることと、関係演算子と同等の優先度として処理されることがわかる。言語仕様を読む限り、左側に記述する式は参照型にキャスト可能である必要がある。
キャスト不可能な場合は、instanceof
の右側は常に参照型を受け取ることから、式を評価しても常にfalse(=デッドコード)となるためエラーとしている模様。つまり1 instanceof Integer
は(RelationalExpression > ... > Literal > IntegerLiteralという規則が適用できるため)文法上パースはできるものの、コンパイル時エラーとなる。
ここでオートボクシングは効かない。 また、instanceof
は当然実行時に判定が行われるが、Java VMでバイトコードが実行される際に全ての型で完全な型情報を利用できるわけではない。そのため、instanceof
演算子に記述する参照型は、実行時に完全な型情報を利用できる具象化可能型(Reifiable Types)である必要がある。具象化可能型であるには、次の6つの条件のうち1つ以上満たす必要がある。
例
class A<T> { } var a = new A<Integer>(); a instanceof A<?> // true a instanceof A<Integer> //エラー。 A<Integer>は非具象化可能型 a instanceof A<Object> //エラー。 a instanceof A<? extends Object> //エラー。 境界も記述できない。
- ネストされた型で、
.
で区切られたそれぞれの型が、全て具象化可能型である
例
class A<T> { class B {} } var a = new A<Object>() var b = a.new B() b instanceof A<?>.B // これは多分こういうことだと思われる。A<?>とBはどちらも具象化可能型なので比較可能。結果はtrueとなる b instanceof A<Object>.B // エラー。A<Object>は非具象化可能型
ここまでが文法上の話。
instanceofのバイトコード
次のコードは、
Integer i = 1; if(i instanceof Integer) { // 処理A } else { // 処理B }
次のようなバイトコードに変換される
0: iconst_1 1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 4: astore_1 5: aload_1 6: instanceof #3 // class java/lang/Integer 9: ifeq 23 // 処理A 20: goto 31 // 処理B 31: return
Java上のinstanceof
は、バイトコードに変換されてもそのままinstanceof
となる。
Java VMが実行するバイトコードのinstanceof
は、スタックの一番上に積まれている参照の型(この場合iの型)と、(コンパイル時にinstanceofに渡された)java.lang.Integer
を比較する。
型の比較
https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.instanceof
s instanceof T
という式で、s
の型をS
としたときに次のいずれかの条件を満たせばこの式の結果はtrue
となる。
1. T
と S
がどちらもクラス型で、 S
がT
と同じクラスまたはT
のサブクラスである
Number s = 1; s instanceof Integer // true. SはNumber(=クラス型)であり、Tが同じIntegerである s instanceof Number // true. IntegerはNumberのサブクラスである s instanceof String // false. IntegerはStringのサブクラスではない
2. T
がインターフェース型のとき、S
は インターフェースT
を実装している
List<String> s = new ArrayList<>(); s instanceof List<?> // true. ArrayListはListを実装している s instanceof Set<?> // false. ArrayListはSetを実装していない
3. S
が配列型で、T
がクラス型であれば、T
はObject
である
Object s = new Integer[1]; s instanceof Object // true. Sが配列型かつTはクラス型で、TがObjectである。 s instanceof Integer // false. Tはクラス型であるが、Objectではない。
4. S
が配列型で、T
がインターフェース型であれば、T
が配列によって実装されているインターフェースである
配列によって実装されているインターフェースというのはつまりCloneable
とSerializable
のインターフェースのことである。
Object[] s = new Integer[1]; s instanceof Cloneable // true. Cloneableは配列によって実装されている s instanceof java.io.Serializable // true. Serializableも配列によって実装されている s instanceof List<?> // false. 配列はListを実装していない
5. S
とT
が配列型のとき、配列型 T
と 配列型 S
の要素の型が同じプリミティブ型である
Object s = new int[0]; s instanceof int[] // true. TとSがどちらも配列型であり、要素の型がintである s instanceof float[] //false. 要素の型が異なる。
6. S
とT
が配列型かつ要素の型がどちらも参照型のとき、要素の型がS
から T
へ実行時にキャスト可能である
Object s = new Integer[1]; s instanceof Integer[] // true. IntegerからIntegerは同じ型のためキャスト可能である s instanceof Number[] //true. IntegerからNumberはサブタイプの関係にあるためキャスト可能である s instanceof String[] //false. IntegerからStringはキャスト不可能である。
以上6条件が、instanceof
でtrueが返る条件となっている。
それ以外の場合
上記の条件が適用されるのは、t
が非nullかつS
の型が解決された後となる。
逆に言うと、t
がnullのときにはinstanceof
はS
の型解決すら行わずにfalse
をプッシュする。(※もちろんコンパイル時には解決できている必要がある。)
instanceofって式がnullのときは型検査せずに固定でfalseをpushするから、classファイルを削除してもエラー落ちしないの面白いな。 pic.twitter.com/tSZcoM47hj
— noko🐶 (@noko_k) January 13, 2019
このように、判定する式がnullであれば、実行時にそのクラスが存在するかどうかは(JVMにとっては)関係ないことであることがわかる。
もっと深い話
よくよく思い出したらJJUG CCCでセッションがあった。 Java VMの実装にまで踏み込んだ深い話。
www.slideshare.net
まとめ
比較する型はなんでも良いわけではない(参照型かつ具象化可能型である必要がある)ことがわかってよかった。 一つ一つの判定条件は考えてみれば当たり前かもしれないが、このように言語化できるとすっきりする。