読者です 読者をやめる 読者になる 読者になる

ラムダ式、完全マスターした。

今日はここのところずっと調べていたラムダ式、およびメソッド参照について調べていましたので
それをこの記事でまとめたいと思います。

まずはじめに。

ここに、テストコードを含む、ラムダ式およびその他のコードをあわせた、3種類のコードを用意した。
もちろん、ラムダ式(というかメソッド参照)に加えて、staticなinner classとinner classを利用したコードである。

public class LambdaTest {  
  private static void privateMethod(String str) {  
  
  }  
  
  @Test  
  public void test1() {  
    System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");  
    Consumer<String> consumer = LambdaTest::privateMethod;  
    consumer.accept("lambda");  
  }  
  
  private static class StaticNestedClass implements Consumer<String> {  
    @Override  
    public void accept(String str) {  
      privateMethod(str);  
    }  
  }  
  
  @Test  
  public void test2() {  
    new StaticNestedClass().accept("staticNestedClass");  
  }  
  
  private class InnerClass implements Consumer<String> {  
    @Override  
    public void accept(String str) {  
      privateMethod(str);  
    }  
  }  
  
  @Test  
  public void test3() {  
    new InnerClass().accept("innerClass");  
  }  
}  

今回見るのはこのクラスをコンパイルしたときに吐き出される、バイトコードが中心となる。

ラムダ式・メソッド参照とは

まずはじめに、Java8から、ラムダ式およびメソッド参照の機能が追加された。
この機能はどういう機能かというと
関数型インターフェースをラムダ式と呼ばれる記法で定義することができたり
インスタンスやクラスのメソッドを関数型インターフェースとして取り扱いやすくするための機能である。

初めに見せたコード中に存在する、以下のコードはメソッド参照を利用して
クラス・メソッドを関数型インターフェースに変換している(うまい言い方が思い浮かばないが、要は委譲である)。

    Consumer<String> consumer = LambdaTest::privateMethod;  

これをラムダ式で書くと以下のようになる。
JavaScriptだとarrow functionは=>になるので、よく間違える。

    Consumer<String> consumer = (str) -> privateMethod(str);  
    // 次のような形でも記述可能  
    //Consumer<String> consumer = (String str) -> privateMethod(str);  

ラムダ式・メソッド参照の簡単な概略については以上となる。

内部クラスとは何が違うのか

では、今回用意した3種類のコードは何が違うのか、というと。
javapしてみます。
まずは、InnerClassから

$ > javap -v LambdaTest$InnerClass.class  
...省略  
 public void accept(java.lang.String);  
  descriptor: (Ljava/lang/String;)V  
  flags: ACC_PUBLIC  
  Code:  
    stack=1, locals=2, args_size=2  
       0: aload_1  
       1: invokestatic  #23                 // Method com/github/wreulicke/lambda/LambdaTest.access$0:(Ljava/lang/String;)V  
       4: return  
    LineNumberTable:  
      line 42: 0  
      line 43: 4  
    LocalVariableTable:  
      Start  Length  Slot  Name   Signature  
          0       5     0  this   Lcom/github/wreulicke/lambda/LambdaTest$InnerClass;  
          0       5     1     t   Ljava/lang/String;  
...省略  

いくつか合成メソッドが生成されているが、一旦置いておく。
今回ソースで実装したacceptメソッドの記述を見ると
invokestaticでcom/github/wreulicke/lambda/LambdaTest.access$0:(Ljava/lang/String;)Vといったシグネチャのメソッドを呼び出している。

ソース上ではこんなものは定義していない。

では、LambdaTestのjavapをしてみよう。

$ > javap -v LambdaTest$InnerClass.class  
...省略  
  static void access$0(java.lang.String);  
    descriptor: (Ljava/lang/String;)V  
    flags: ACC_STATIC, ACC_SYNTHETIC  
    Code:  
      stack=1, locals=1, args_size=1  
         0: aload_0  
         1: invokestatic  #66                 // Method privateMethod:(Ljava/lang/String;)V  
         4: return  
      LineNumberTable:  
        line 8: 0  
      LocalVariableTable:  
        Start  Length  Slot  Name   Signature  
...省略  

コンパイラにより、バイトコード内に合成メソッドが定義されている。これを呼び出しているようだ。
中の処理はinvokestaticで実装で記述したprivateMethodの呼び出しをしているだけである。

じゃあStaticNestedなクラスでは何を呼び出しているかというと、一緒の合成メソッドを呼び出してあった。
なので飛ばす。

ではお待ちかねのメソッド参照で記述されたバイトコードを見てみよう。
ちなみに、ラムダ式を記述したときは、クラスファイルは生成されない。なので、ダンプした。
バイトコードのダンプの仕方は一番下にある。というかソースに書いてる。

...省略  
$ > javap -v LambdaTest$$Lambda$1.class  
  public void accept(java.lang.Object);  
    descriptor: (Ljava/lang/Object;)V  
    flags: ACC_PUBLIC  
    Code:  
      stack=1, locals=2, args_size=2  
         0: aload_1  
         1: checkcast     #14                 // class java/lang/String  
         4: invokestatic  #20                 // Method com/github/wreulicke/lambda/LambdaTest.privateMethod:(Ljava/lang/String;)V  
         7: return  
...省略  

おや?さっきと違ったコードが吐き出されている。
invokestaticで合成メソッド経由ではなく直接privateメソッドを呼び出している。

この点が内部クラスprivateMethodを呼び出した時に違う点だ。(他のアクセス修飾子の場合で網羅的に調べてない点についてはごめんなさい。)

自分はこの時にバイトコードにはアクセス修飾子は関係ないのかと勘違いしてしまった。
ちなみに、ASMでラムダ式が吐くようなバイトコードを吐いたところ、IllegalAccessErrorが発生したので
ちゃんとアクセス修飾子による制限は存在するようだ。

ラムダ式・メソッド参照で記述されたインスタンスはどこからやってくるのか

これについては既存の資料に大抵記述されている。

ラムダと invokedynamic の蜜月
Java8におけるindyとLambdaの絶妙な関係、もしくはSAMタイプを継承する内部クラスの.classファイルはどこへ行ったの? - uehaj's blog
Lambda が indy に出会ったら ... Java etc.../ウェブリブログ

invokedynamicとbootstrapメソッドに関して

バイトコードにinvokedynamic命令というものが存在しており
これはbootstrapメソッドと呼ばれるバイトコード内の情報を元に動的に呼び出し先を変更することができる機能である。
javapしてみると確認できる。
BootstrapMethodsという属性が存在しており
それに対して呼び出すメソッドとメソッドに渡される引数がバイトコード上に記述されている。

$ > javap -v LambdaTest.class  
...省略  
BootstrapMethods:  
  0: #77 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Metho  
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;  
    Method arguments:  
      #78 (Ljava/lang/Object;)V  
      #79 invokestatic com/github/wreulicke/lambda/LambdaTest.privateMethod:(Ljava/lang/String;)V  
      #80 (Ljava/lang/String;)V  
...省略  

LambdaMetafactory.metafactoryメソッドが呼び出される
第一引数のMethodHandles$LookupオブジェクトはJVMによって動的に解決される?。(詳しくは知らない。)

この時metafactoryメソッドによって生成されたCallSiteをJVMが実行して
ラムダ式で実装すべき型を実装したインスタンスが返却される。
今回のコードではConsumerである。

オマケ:ラムダ式を記述したクラスにInnerClassとしてLookupオブジェクトが定義される。

タイトル通りではあるが、ラムダ式を記述した際、JavaバイトコードにはInnerClasses属性が追記されている。アクセス権限を付与するためであろうか?

$ > javap -v LambdaTest.class  
...省略  
InnerClasses:  
     public static final #88= #84 of #86; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles  
...省略  

ラムダ式のクラスがロードされるまでのおおまかな流れ

大まかな流れとしては以下のようになる。
1. invokedynamicでLambdaMetafactory#metafactoryメソッドが呼ばれる
2. ラムダ式のクラスが生成される
3. CallSiteオブジェクトが返却される
4. 返却されたCallSiteオブジェクトがinvokedynamicの流れで実行される
5. ラムダ式で実装されたインスタンスが生成される。

どうやってクラスが生成されているか。

これは他のブログの方が書いてはいるが、ここにまとめておく。

ラムダ式が実際にどうやってロードされるのかを見てみよう。
invokedynamicで呼び出されたbootstrapメソッド内で
InnerClassLambdaMetafactoryインスタンス化された後、
細かいところを省略すると(というか読んでない)
buildCallSiteメソッドを呼び出し、CallSiteオブジェクトを返却している。

    public static CallSite metafactory(MethodHandles.Lookup caller,  
                                       String invokedName,  
                                       MethodType invokedType,  
                                       MethodType samMethodType,  
                                       MethodHandle implMethod,  
                                       MethodType instantiatedMethodType)  
            throws LambdaConversionException {  
        AbstractValidatingLambdaMetafactory mf;  
        mf = new InnerClassLambdaMetafactory(caller, invokedType,  
                                             invokedName, samMethodType,  
                                             implMethod, instantiatedMethodType,  
                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);  
        mf.validateMetafactoryArgs();  
        return mf.buildCallSite();  
    }  
  

では、buildCallSiteメソッドでは何をやっているかというと
spinInnerClassを呼ぶと、ラムダ式のクラスが生成されて返却されている。

    @Override  
    CallSite buildCallSite() throws LambdaConversionException {  
        final Class<?> innerClass = spinInnerClass();  
        ...省略  
    }  

spinInnerClassでは何をやっているかというと
JDK内部に持っているASM(バイトコード操作のライブラリ)を用いて、ラムダ式で実装するべきクラスのバイトコードを生成している

また、ラムダ式をダンプした時から気になっていた、
内部クラスを記述したときの合成メソッドを呼び出すのではなく
内部クラスでもないのにinvokestaticでprivateメソッドを直接読んでいる件に関しては
spinInnerClassの最後の行にある
このメソッドによって内部クラスになっているのであろう。

        return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);  

オマケ:ラムダ式で生成されたバイトコードをダンプするには?

ラムダ式が生成するクラスはシステムプロパティのキー「jdk.internal.lambda.dumpProxyClasses」に
ダンプ先を指定することでダンプすることが可能である。

ちなみに、このプロパティの読み取りはInnerClassLambdaMetafactoryのstatic初期化子に記述されているので
InnerClassLambdaMetafactoryクラスが読み込まれる前にダンプ先を設定しておかなければならない。

まとめ

今回はラムダ式というかメソッド参照がどんなバイトコードを生成するのかについて調べた。
楽しい。

ラムダ式が分からない?そんなあなたに朗報!!
ダンプしてバイトコードを調べよう!!Javaプログラマならわかるよね!!

雑な煽りをして今日の記事は終わりです。