Int#+の怪
Scalaでは32ビット符号付き整数を表すIntは特別な型ではなく、単にAnyのサブクラスであるAnyValのサブクラスであるIntクラスだという事になっているし、実際そのように扱われる。また、Intに対する+などの各種演算子も単なるIntクラスのメソッドであることになっているし、実際にそのように扱われる。では、Int型の+が単なるメソッドだとしたら、JavaのリフレクションAPIを経由して+メソッドを表すMethodオブジェクトを取得し、それを呼び出すことはできるのだろうか?
結論から言うと、無理である。まず、ScalaのInt型を使ったコードがクラスファイルにコンパイルされる際はJVMレベルでは可能な限りJVMのプリミティブ型であるint型を利用し、コレクションにInt型の値が格納される場合など、それが不可能な場合はjava.lang.Integer型に自動的にboxingされるようになっている。ここで、java.lang.Integer型に+メソッドなどというものがあれば良いのだが、Javaプログラマならわかるように、当然そんなメソッドは無いので、リフレクションAPIを経由して+メソッドを表すMethodオブジェクトを取得することはできない、ということになる。
念のため、REPLで
scala> 0.asInstanceOf[AnyRef].getClass.getMethods.filter(_.getName == "$plus")
のように入力して、$plusメソッド(Scalaの+メソッドはJVMレベルでは$plusという名前にmanglingされる)を探してみたが、当然、
res4: Array[java.lang.reflect.Method] = Array()
となり、そのようなメソッドは見つからなかった。というわけで、Int型の$plusメソッドをリフレクション経由で呼び出すことは無理なわけだが、だとすると一つ疑問がある。Scalaにはstructural typeという機能があり、「あるシグニチャを持ったメソッドだけを持った型」のようなものを表現できる。これを使うと、以下のようなコードを評価することができる。Any{ def +(x: Int): Int}
というコードで、Anyのサブタイプでかつ「Int型を引数として受け取って、Int型を返す+メソッド」を持った任意のオブジェクトを受け取れるような型を表現している。
scala> {val i: Any{def +(x: Int): Int} = 1; i + 1} res5: Int = 2
このstructural typeという機能、知っている人ならわかると思うが、リフレクションAPIを使ってMethodオブジェクトを取得してそれを呼び出すことで実現されている。しかし、Int型に対して+メソッドを表すMethodオブジェクトを取得することはできないはず。一体どのようにして実現しているのだろうか…というわけで、
object I { def main(args: Array[String]) { val x: Any{ def +(i: Int): Int } = 10 println(x + 10) } }
というコードをscalacでコンパイルして、javap -cで逆アセンブルしてみた。以下が逆アセンブル結果である。
Compiled from "I.scala" public final class I$ extends java.lang.Object implements scala.ScalaObject{ public static final I$ MODULE$; public static {}; Code: 0: new #10; //class I$ 3: invokespecial #13; //Method "<init>":()V 6: return public void main(java.lang.String[]); Code: 0: ldc #41; //int 10 2: invokestatic #47; //Method scala/runtime/BoxesRunTime.boxToInteger:(I )Ljava/lang/Integer; 5: astore_2 6: getstatic #52; //Field scala/Predef$.MODULE$:Lscala/Predef$; 9: aload_2 10: astore_3 11: aload_2 12: instanceof #54; //class java/lang/Number 15: ifne 25 18: aload_2 19: instanceof #56; //class java/lang/Character 22: ifeq 40 25: aload_3 26: ldc #41; //int 10 28: invokestatic #47; //Method scala/runtime/BoxesRunTime.boxToInteger:(I )Ljava/lang/Integer; 31: invokestatic #60; //Method scala/runtime/BoxesRunTime.add:(Ljava/lang /Object;Ljava/lang/Object;)Ljava/lang/Object; 34: checkcast #22; //class java/lang/Integer 37: goto 73 40: aconst_null 41: astore 4 43: aload_3 44: invokevirtual #64; //Method java/lang/Object.getClass:()Ljava/lang/Cla ss; 47: invokestatic #68; //Method reflMethod$Method1:(Ljava/lang/Class;)Ljav a/lang/reflect/Method; 50: aload_3 51: iconst_1 52: anewarray #35; //class java/lang/Object 55: dup 56: iconst_0 57: ldc #41; //int 10 59: invokestatic #47; //Method scala/runtime/BoxesRunTime.boxToInteger:(I )Ljava/lang/Integer; 62: aastore 63: invokevirtual #74; //Method java/lang/reflect/Method.invoke:(Ljava/lan g/Object;[Ljava/lang/Object;)Ljava/lang/Object; 66: astore 4 68: aload 4 70: checkcast #22; //class java/lang/Integer 73: invokevirtual #78; //Method scala/Predef$.println:(Ljava/lang/Object;) V 76: return 77: astore 5 79: aload 5 81: invokevirtual #84; //Method java/lang/reflect/InvocationTargetExceptio n.getCause:()Ljava/lang/Throwable; 84: athrow Exception table: from to target type 43 68 77 Class java/lang/reflect/InvocationTargetException public static java.lang.reflect.Method reflMethod$Method1(java.lang.Class); Code: 0: getstatic #33; //Field reflPoly$Cache1:Lscala/runtime/MethodCache; 3: aload_0 4: invokevirtual #97; //Method scala/runtime/MethodCache.find:(Ljava/lang /Class;)Ljava/lang/reflect/Method; 7: astore_1 8: aload_1 9: ifnull 14 12: aload_1 13: areturn 14: aload_0 15: ldc #99; //String $plus 17: getstatic #28; //Field reflParams$Cache1:[Ljava/lang/Class; 20: invokevirtual #103; //Method java/lang/Class.getMethod:(Ljava/lang/Str ing;[Ljava/lang/Class;)Ljava/lang/reflect/Method; 23: astore_1 24: getstatic #33; //Field reflPoly$Cache1:Lscala/runtime/MethodCache; 27: aload_0 28: aload_1 29: invokevirtual #106; //Method scala/runtime/MethodCache.add:(Ljava/lang /Class;Ljava/lang/reflect/Method;)Lscala/runtime/MethodCache; 32: putstatic #33; //Field reflPoly$Cache1:Lscala/runtime/MethodCache; 35: aload_1 36: areturn }
若干長いが、重要なのは以下の部分だ。
12: instanceof #54; //class java/lang/Number 15: ifne 25 18: aload_2 19: instanceof #56; //class java/lang/Character 22: ifeq 40 25: aload_3 26: ldc #41; //int 10 28: invokestatic #47; //Method scala/runtime/BoxesRunTime.boxToInteger:(I )Ljava/lang/Integer; 31: invokestatic #60; //Method scala/runtime/BoxesRunTime.add:(Ljava/lang /Object;Ljava/lang/Object;)Ljava/lang/Object; 34: checkcast #22; //class java/lang/Integer 37: goto 73
このコードを見ると、レシーバがjava.lang.Number
のインスタンスまたjava.lang.Character
のインスタンスかをチェックして、もしそうならばBoxesRuntime.add(Object,Object)
という専用のメソッドを呼び出していることがわかる。というわけで、structural typeの実現にあたって、プリミティブ型のラッパークラスに関しては特別扱いするようなコードをscalacが吐いているのであった。ちゃんちゃん。