Java Lambda 表達式源碼分析

Lambda 表達式是什么拔鹰?JVM 內(nèi)部究竟是如何實現(xiàn) Lambda 表達式的仪缸?為什么要這樣實現(xiàn)?

基本概念

Lambda 表達式

下面的例子中格郁,() -> System.out.println("1") 就是一個 Lambda 表達式腹殿。Java 8 中每一個 Lambda 表達式必須有一個函數(shù)式接口與之對應(yīng)。Lambda 表達式就是函數(shù)式接口的一個實現(xiàn)例书。

@Test
public void test0() {
    Runnable runnable = () -> System.out.println("1");
    runnable.run();

    ToIntBiFunction<Integer, Integer> function = (n1, n2) -> n1 + n2;
    System.out.println(function.applyAsInt(1, 2));

    ToIntBiFunction<Integer, Integer> function2 = Integer::sum;
    System.out.println(function2.applyAsInt(1, 2));
}

大致形式就是 (param1, param2, param3, param4…) -> { doing…… };

函數(shù)式接口

首先要從 FunctionalInterface 注解講起

An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification. Conceptually, a functional interface has exactly one abstract method. Since default methods have an implementation, they are not abstract. If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface's abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere.

簡單總結(jié)一下函數(shù)式接口的特征:

  • FunctionalInterface 注解標注一個函數(shù)式接口锣尉,不能標注類,方法决采,枚舉自沧,屬性這些。
  • 如果接口被標注了 @FunctionalInterface树瞭,這個類就必須符合函數(shù)式接口的規(guī)范拇厢。
  • 即使一個接口沒有標注 @FunctionalInterface,如果這個接口滿足函數(shù)式接口規(guī)則晒喷,依舊可以被當(dāng)作函數(shù)式接口孝偎。

注意:interface 中重寫 Object 類中的抽象方法,不會增加接口的方法數(shù)凉敲,因為接口的實現(xiàn)類都是 Object 的子類衣盾。

我們可以看到 Runnable 接口爷抓,里面只有一個抽象方法 run()势决,則這個接口就是一個函數(shù)式接口。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

方法引用

所謂方法引用蓝撇,是指如果某個方法簽名和接口恰好一致果复,就可以直接傳入方法引用。文章開頭的示例中渤昌,下面這塊代碼就是方法引用虽抄。

ToIntBiFunction<Integer, Integer> function2 = Integer::sum;

java.lang.Integer#sum 的實現(xiàn)如下:

public static int sum(int a, int b) {
    return a + b;
}

比如我們計算一個 Stream 的和走搁,可以直接傳入 Integer::sum 這個方法引用。

@Test
public void test1() {
    Integer sum = IntStream.range(0, 10).boxed().reduce(Integer::sum).get();
    System.out.println(sum);
}

上面的代碼中迈窟,為什么可以直接在 reduce 方法中傳入 Integer::sum 這個方法引用呢朱盐?這是因為 reduce 方法的入?yún)⒕褪?BinaryOperator 的函數(shù)式接口。

Optional<T> reduce(BinaryOperator<T> accumulator);

BinaryOperator 是繼承自 BiFunction菠隆,定義如下:

@FunctionalInterface
public interface BiFunction<T, U, R> {

    R apply(T t, U u);

    default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t, U u) -> after.apply(apply(t, u));
    }
}

可以看到,只要是符合 R apply(T t, U u); 的方法引用狂秘,都可以傳入 reduce 中骇径。可以是上面代碼中的 Integer::sum者春,也可以是 Integer::max破衔。

深入實現(xiàn)原理

字節(jié)碼

首先寫 2 個 Lambda 方法:

public class LambdaMain {

    public static void main(String[] args) {
        new Thread(() -> System.out.println("1")).start();
        IntStream.range(0, 5).boxed().filter(i -> i < 3).map(i -> i + "").collect(Collectors.toList());
    }
}

之后 javac LambdaMain.java 編譯成字節(jié)碼文件,再通過 javap -p LambdaMain 輸出 class 文件的所有類和成員钱烟,得到輸出結(jié)果:

Compiled from "LambdaMain.java"
public class test.jdk.LambdaMain {
  public test.jdk.LambdaMain();
  public static void main(java.lang.String[]);
  private static java.lang.String lambda$main$2(java.lang.Integer);
  private static boolean lambda$main$1(java.lang.Integer);
  private static void lambda$main$0();
}

  • 輸出的 void lambda$main$0() 對應(yīng)的是 () -> System.out.println("1")
  • 輸出的 boolean lambda$main$1(java.lang.Integer) 對應(yīng)的是 i -> i < 3
  • 輸出的 java.lang.String lambda$main$2(java.lang.Integer) 對應(yīng)的是 i -> i + ""

我們可以看出 Lambda 表達式在 Java 8 中首先會生成一個私有的靜態(tài)函數(shù)晰筛。

為什么不使用匿名內(nèi)部類?

如果要在 Java 語言中實現(xiàn) lambda 表達式拴袭,生成匿名內(nèi)部類就可以輕松實現(xiàn)读第。但是 JDK 為什么沒有這么實現(xiàn)呢?這是因為匿名內(nèi)部類有一些缺點拥刻。

  1. 每個匿名內(nèi)部類都會在編譯時創(chuàng)建一個對應(yīng)的class 文件怜瞒,在運行時不可避免的會有加載、驗證般哼、準備吴汪、解析、初始化等類加載過程蒸眠。
  2. 每次調(diào)用都會創(chuàng)建一個這個匿名內(nèi)部類 class 的實例對象漾橙,無論是有狀態(tài)的(使用到了外部的變量)還是無狀態(tài)(沒有使用外部變量)的內(nèi)部類。

invokedynamic

本來要寫文字的楞卡,但是俺發(fā)現(xiàn)俺總結(jié)的思維導(dǎo)圖還挺清晰的霜运,直接提出來吧,囧臀晃。

詳情見 Class LambdaMetafactory 官方文檔觉渴,java.lang.invoke.LambdaMetafactory#metafactory 的實現(xiàn)。

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();
}

其主要的概念有如下幾個:

  • invokedynamic 字節(jié)碼指令:運行時 JVM 第一次到某個地方的這個指令的時候會進行 linkage徽惋,會調(diào)用用戶指定的 Bootstrap Method 來決定要執(zhí)行什么方法案淋,之后便不需要這個步驟。
  • Bootstrap Method: 用戶可以自己編寫的方法险绘,最終需要返回一個 CallSite 對象踢京。
  • CallSite: 保存 MethodHandle 的容器誉碴,里面有一個 target MethodHandle。
    MethodHandle: 真正要執(zhí)行的方法的指針瓣距。

測試一下 Lambda 函數(shù)生成的字節(jié)碼黔帕,為了方便起見,java 代碼改成如下:

public class LambdaMain {

    public static void main(String[] args) {
        new Thread(() -> System.out.println("1")).start();
    }
}

先編譯成 class 文件蹈丸,之后再反匯編 javap -c -p LambdaMain 看下輸出:

Compiled from "LambdaMain.java"
public class test.jdk.LambdaMain {
  public test.jdk.LambdaMain();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/Thread
       3: dup
       4: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
       9: invokespecial #4                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      12: invokevirtual #5                  // Method java/lang/Thread.start:()V
      15: return

  private static void lambda$main$0();
    Code:
       0: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #7                  // String 1
       5: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

可以看到 Thread 里的 Runnable 實現(xiàn)是通過 invokedynamic 調(diào)用的成黄。詳細情況 JVM 虛擬機規(guī)范,等有時間再補充吧~~~

總結(jié)

  • Lambda 表達式在 Java 中最終編譯成私有的靜態(tài)函數(shù)逻杖,JDK 最終使用 invokedynamic 字節(jié)碼指令調(diào)用奋岁。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市荸百,隨后出現(xiàn)的幾起案子闻伶,更是在濱河造成了極大的恐慌,老刑警劉巖够话,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蓝翰,死亡現(xiàn)場離奇詭異,居然都是意外死亡女嘲,警方通過查閱死者的電腦和手機畜份,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澡为,“玉大人漂坏,你說我怎么就攤上這事∶街粒” “怎么了剥险?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵精刷,是天一觀的道長舀寓。 經(jīng)常有香客問我胯舷,道長,這世上最難降的妖魔是什么谋旦? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任剩失,我火速辦了婚禮,結(jié)果婚禮上册着,老公的妹妹穿的比我還像新娘拴孤。我一直安慰自己,他們只是感情好甲捏,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布演熟。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪芒粹。 梳的紋絲不亂的頭發(fā)上兄纺,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機與錄音化漆,去河邊找鬼估脆。 笑死,一個胖子當(dāng)著我的面吹牛座云,可吹牛的內(nèi)容都是我干的疙赠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼朦拖,長吁一口氣:“原來是場噩夢啊……” “哼棺聊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起贞谓,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎葵诈,沒想到半個月后裸弦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡作喘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年理疙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泞坦。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡窖贤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贰锁,到底是詐尸還是另有隱情赃梧,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布豌熄,位于F島的核電站授嘀,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏锣险。R本人自食惡果不足惜蹄皱,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芯肤。 院中可真熱鬧巷折,春花似錦、人聲如沸崖咨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掩幢。三九已至逊拍,卻和暖如春上鞠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背芯丧。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工芍阎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缨恒。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓谴咸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親骗露。 傳聞我的和親對象是個殘疾皇子岭佳,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容