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

なみひらブログ

学んだことを日々記録する。~ since 2012/06/24 ~

ラムダ式がどのように実現されているかを確認してみました。

背景

Java8で新仕様として「ラムダ式」が追加されました。
そのラムダ式がどのように実現されているか、確認してみました。

簡単な確認

Java7以前

比較のために、まずJava7以前のコードを以下のように書いてみます。
Java7まではメソッドの引数として、無名クラスを作って渡してあげます。
(他にもRunnableとかListenerとかにも多い記法。)

[ec2-user@xx java]$ cat jp/namihira/util/SortClass.java
/**
 * Copyright 2014 Kosuke Namihira All Rights Reserved.
 */

package jp.namihira.util;

import java.util.Arrays;
import java.util.Comparator;

public class SortClass {

    public static void main(String[] args) {
        // preapre
        String[] strs = new String[]{"a", "ccc", "bb"};

        // action
        Arrays.sort(strs, new Comparator<String>() {
            @Override
            public int compare(String first, String second) {
                return Integer.compare(first.length(), second.length());
            }
        });

        // check
        System.out.println(Arrays.asList(strs));
    }
}

で、上記コードをコンパイルして出来たクラスファイルを覗いてみます。
実装した通り、コンストラクタとmainメソッドがあることがわかります。

[ec2-user@xx java]$ javap -private jp/namihira/util/SortClass.class
Compiled from "SortClass.java"
public class jp.namihira.util.SortClass {
  public jp.namihira.util.SortClass();
  public static void main(java.lang.String[]);
}

Java8

次に、Java8でのラムダ式を使って同じロジックを書いてみます。
Arrays.sortメソッドの引数のComparetorは関数型インタフェースなので、ラムダ式で書くことができます。

[ec2-user@xx java]$ cat SortLambda.java
/**
 * Copyright 2014 Kosuke Namihira All Rights Reserved.
 */

package jp.namihira.util;

import java.util.Arrays;

public class SortLambda {

    public static void main(String[] args) {
        // preapre
        String[] strs = new String[]{"a", "ccc", "bb"};

        // action
        Arrays.sort(strs, (first, second) -> Integer.compare(first.length(), second.length()));

        // check
        System.out.println(Arrays.asList(strs));
    }
}

同じようにコンパイルしてクラスファイルを覗いてみます。

[ec2-user@xx java]$ javap -private jp/namihira/util/SortLambda.class
Compiled from "SortLambda.java"
public class jp.namihira.util.SortLambda {
  public jp.namihira.util.SortLambda();
  public static void main(java.lang.String[]);
  private static int lambda$main$0(java.lang.String, java.lang.String);
}

なんかメソッド増えとる( ゚д゚ )「private static int lambda$main$0(java.lang.String, java.lang.String);」

ここでもう結論ですが、

ラムダ式を書くとコンパイル時にそのラムダ式に書かれた処理のメソッドが新規で追加され、実行時にそのメソッドが呼ばれる

のようです。*1
ラムダ式」という新たな記法ですが、イメージ的にはこんな捉え方でいいと思う(´・ω・`)b

もう少し詳細に見てみる

先ほどのラムダ式のクラスファイルをデコンパイルして、もっと詳細に見てみます。
CFR - yet another java decompiler.を使ってデコンパイルしてみました。
以下のように出力されました。
※そのままデコンパイルするとラムダ記法までもどっちゃうので、オプション「decodelambdas」を付けてます。

[ec2-user@xx java]$ java -jar cfr_0_90.jar jp/namihira/util/SortLambda.class --decodelambdas false
/*
 * Decompiled with CFR 0_90.
 */
package jp.namihira.util;

import java.io.PrintStream;
import java.util.Arrays;

public class SortLambda {
    public static void main(String[] arrstring) {
        String[] arrstring2 = new String[]{"a", "ccc", "bb"};
        Arrays.sort(arrstring2, (java.util.Comparator)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;Ljava/lang/Object;)I, lambda$main$0(java.lang.String java.lang.String ), (Ljava/lang/String;Ljava/lang/String;)I)());
        System.out.println(Arrays.asList(arrstring2));
    }

    private static /* synthetic */ int lambda$main$0(String string, String string2) {
        return Integer.compare(string.length(), string2.length());
    }
}

まず分かるのは、確かにラムダ式の処理が書かれたメソッドが新規に定義されていることがわかります(lambda$main$0)。

問題なのは呼び出し元で、結構複雑です。。。
以下のクラスを使っています。
https://docs.oracle.com/javase/jp/8/api/java/lang/invoke/LambdaMetafactory.htm

ラムダ式が呼ばれるまでの流れ

LambdaMetafactory.metafactoryメソッドの引数の雰囲気としては以下のような感じです。(一部のみ)

metafactoryメソッドの引数 コンパイル後の表現
呼び出すメソッドラムダ式)の実態 lambda$main$0
メソッドラムダ式)の引数と戻り値(MethodType) (String、String、Integer)
作成予定の関数インタフェースのための引数と戻り値(MethodType) Object、Object、Integer・・・Comparatorクラスのint compare(T o1, T o2)に相当

LambdaMetafactory.metafactoryメソッドを簡単に言うと*2、関数型インタフェースのインスタンスを生成してくれるオブジェクトを作成してくれます。
インスタンスを生成するまでには、Java7で追加された「invokedynamic(動的メソッド呼び出し)」と「メソッドハンドル(Java7版リフレクション)」を使っています。

全体的な流れ(コンパイルから実行まで)は以下の通りです。

  1. ソースコードコンパイル時に、ラムダ記法の部分を「LambdaMetafactory.metafactory」と「内部メソッド(lambda$XX$0)」で置き換える。
  2. コードを実行すると、LambdaMetafactory.metafactoryに対して、「内部メソッド名」「内部メソッドの型情報(引数や戻り値)」「作成する関数型インターフェースのメソッドの型情報(引数や戻り値)」を渡すことで、「関数型インタフェースのインスタンスを作ることのできるメソッドハンドル」をもつオブジェクト(CallSite)が動的に取得される。※invokedynamic
  3. そのオブジェクトが保持しているメソッドハンドルを呼ぶことで、関数型インタフェースのインスタンスが生成される。※メソッドハンドル
  4. その関数型インタフェースのインスタンスが、呼び出し先のメソッド(Arrays.sort)に渡される。

#ということは実際の動作は先ほどのラムダ式のところに来たら新規に定義されたメソッドが呼ばれるというのではなくて、そのメソッド自体はインスタンス生成のための情報に過ぎないということだろう(´Д`)結局。

まとめ

ラムダ式の動きが、な、なんとなく分かった気がする(;´Д`)

参考

CFR で Java 8 のラムダ式をデコンパイルする - なんとなくな Developer のメモ

Java 8を可能にしたJava 7の機能

LambdaMetafactory (Java Platform SE 8)

Comparator (Java Platform SE 8)

Javaプログラマーなら習得しておきたい Java SE 8 実践プログラミング

Javaプログラマーなら習得しておきたい Java SE 8 実践プログラミング

*1:Java 8の初期プロトタイプでは,ラムダ式をそれぞれ,コンパイル時に無名の内部クラスに変換していたらしい

*2:内容が難しくて簡単にしか理解できない。。。