秒懂Java之深入理解Lambda表達式

【版權申明】非商業目的註明出處可自由轉載
博文地址:
出自:shusheng007

概述

今天在Pluralsight看了一個講Java Lambda 表達式的視頻教程,覺得很好,自己研究並記錄分享一下以饗讀者。

因爲Java8已經出來好久了,Lambda已經被大量使用了,所以這裏只是分享一下對其的思考和總結,不準備過多講解其用法,目的是使我們對其有更加深刻的理解。

匿名類到Lambda表達式

我們知道,只有函數接口才可以使用Lambda表達式。

函數接口:只有一個abstract的方法的接口

那我們怎麼將實現了函數接口的匿名類轉換成Lambda表達式呢?我們以code來說話:

示例1,抽象方法無入參,無返回值

我們以Runnable函數接口爲例

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

匿名類:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("hello thread");
    }
};
new Thread(runable).start();

Lambda 表達式

  • 將方抽象方法括號及其中參數拷貝出來並加上->
    ()->
  • 將抽象方法方法體拷貝出來,如果其中只有一句代碼則{} 可以省略
	()->{ System.out.println("hello thread");}
  
    省略括號後
    ()->System.out.println("hello thread")

    new Thread(() -> System.out.println("hello thread")).start();    

示例2,抽象方法帶入參,帶返回值的情形

例如用於比較的Comparator函數接口

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
    ...
 }

匿名類

 Comparator<Integer> comparator = new Comparator<Integer>() {
     @Override
     public int compare(Integer o1, Integer o2) {
         return Integer.compare(o2, o1);
     }
 };

轉換爲Lambda表達式

  • 將方抽象方法括號及其中參數拷貝出來並加上->
    (Integer o1, Integer o2) ->
  • 將抽象方法方法體拷貝出來放在->後面
    (Integer o1, Integer o2) ->`{
        return Integer.compare(o2, o1);
    }
    
  • 簡化
    參數類型可以省略,如果方法體只有一句代碼則{}return 關鍵字可以省略
    (o1, o2)-> Integer.compare(o2, o1)
    

不是說越簡單越好,如果你發現帶上類型可讀性更好,那就帶上,完全沒有問題。

不知道你們平時在開發中是否遇到過特別複雜函數接口,手寫lambda表達式變得很困難,不好理解。此時反而利用IDE的提示功能直接new匿名類對象反而更方便,然後寫完了再使用IDE一鍵將其轉換爲Lambda表達式。

Method reference(方法引用)

Lambda的一種簡寫方式,無他。在可以使用Lambda的地方都可以使用Method Reference, 反之亦然。唯一決定你使用哪種方式的是可讀性,哪個可讀性高就使用哪個。

其語法爲

類名::方法名

其中方法既可以是類方法,也可以是實例方法。

 //方法引用
 stream.forEach(System.out::println);
 //Lambda表達式
 stream.forEach(x->System.out.println(x));

靈魂拷問

Lambda 有類型嗎?有的話是什麼類型?

Lambda可以賦值給變量嗎?可以當方法的入參和返回值嗎?

Lambda是對象嗎?有的話我們可以在代碼中引用它嗎?

第一問:
Java是強類型語言,在Java中任何事物都有類型,Lambda也不例外,它的類型就是其對應的函數接口。

第二問:
Lambda可以賦值給變量,而且可以作爲方法的參數及返回值

第三問:
Lambda是個對象嗎?這塊情況比較就比較複雜了,我只能將自己的調查和理解呈上,至於更深刻的機制需要你去研究。不過我這裏說的,對於普通程序員已經足夠了,我個人覺得這部分很有意思,如果你也想知道Java編譯器到底如何處理Lambda表達式這種語法糖的話,應該接着往下看。

首先我們來看一段代碼

package top.ss007;
...
public class Student {
    Runnable runnable = () -> {
    };
    Comparator<Integer> comparator = (o1, o2) -> 1;

    Runnable runnableAnon = new Runnable() {
        @Override
        public void run() {
        }
    };
}

定義了一個Student類,裏面聲明瞭3個field,兩個賦值爲Lambda表達式,一個賦值爲匿名內部類對象。

我們使用Java編譯器javac將其編譯爲class文件

javac -g  Student.java

-g 保留所有調試信息

執行上述命令後生成了Student.classStudent$1.class兩個文件

然後我們使用字節碼查看器ByteCodeViewer查看一下這兩個文件,從命名上就可以看出Student$1.class是Student類的內部類.

我們先看使用匿名內部類實現函數接口的部分:

Runnable runnableAnon = new Runnable() {
    @Override
    public void run() {
    }
};

我們知道Javac 會爲其生成一個實現了Runnable接口的Student的內部類,這塊我們就不看字節碼了直接看生成的代碼類

class Student$1 implements Runnable {
   // $FF: synthetic field
   final Student this$0;

   Student$1(Student this$0) {
      this.this$0 = this$0;
   }

   public void run() {
   }
}

可見,非靜態的內部類是會持有其包含類的一個引用的。

接下來看一下其具體的賦值字節碼

public Student() { // <init> //()V
    <localVar:index=0 , name=this , desc=Ltop/ss007/Student;, sig=null, start=L1, end=L2>

    L1 {
        aload0 // reference to self
        invokespecial java/lang/Object.<init>()V
    }
    
...

    L5 {
        aload0 // reference to self
        new top/ss007/Student$1
        dup
        aload0 // reference to self
        invokespecial top/ss007/Student$1.<init>(Ltop/ss007/Student;)V
        putfield top/ss007/Student.runnableAnon:java.lang.Runnable
        return
    }
}

上面是Student類的構造函數的字節碼

L1{} 裏面是調用Object的構造函數的代碼,又一次印證了Object是所有類型的基類。

L5{} 的代碼是我們要關注的,通過new top/ss007/Student$1 new 一個Student$1的對象,初始化後賦值給field runableAnon.

通過上面的調查我們已經清楚了javac是如何處理使用匿名內部類實現函數接口的方式,接下來讓我看一下Lambda表達式是如何處理的。

查看Student的字節碼文件可以發現如下兩段代碼

 private static synthetic lambda$new$1(java.lang.Integer arg0, java.lang.Integer arg1) { //(Ljava/lang/Integer;Ljava/lang/Integer;)I
     <localVar:index=0 , name=o1 , desc=Ljava/lang/Integer;, sig=null, start=L1, end=L2>
     <localVar:index=1 , name=o2 , desc=Ljava/lang/Integer;, sig=null, start=L1, end=L2>

     L1 {
         iconst_1
         ireturn
     }
     L2 {
     }
 }

 private static synthetic lambda$new$0() { //()V
     L1 {
         return
     }
 }

其中有兩個使用了synthetic的方法,說明這兩個方法是編譯器幫我們生成的。
lambda$new$0 對應的是

()->{}

lambda$new$1 對應的是

(o1, o2) -> 1;

賦值語句位於Student的構造函數中,如下所示:

public Student() { // <init> //()V
    <localVar:index=0 , name=this , desc=Ltop/ss007/Student;, sig=null, start=L1, end=L2>
...
    L3 {
        aload0 // reference to self
        invokedynamic java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; : run()Ljava/lang/Runnable; ()V top/ss007/Student.lambda$new$0()V (6) ()V
        putfield top/ss007/Student.runnable:java.lang.Runnable
    }
    L4 {
        aload0 // reference to self
        invokedynamic java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; : compare()Ljava/util/Comparator; (Ljava/lang/Object;Ljava/lang/Object;)I top/ss007/Student.lambda$new$1(Ljava/lang/Integer;Ljava/lang/Integer;)I (6) (Ljava/lang/Integer;Ljava/lang/Integer;)I
        putfield top/ss007/Student.comparator:java.util.Comparator
    }
...
}

L3{} 塊內是

Runnable runnable = () -> {   };

的處理代碼

L4{} 塊內是

Comparator<Integer> comparator = (o1, o2) -> 1;

的處理代碼

其中最爲關鍵的就是

invokedynamic java/lang/invoke/LambdaMetafactory.metafactory(
		Ljava/lang/invoke/MethodHandles$Lookup;
		Ljava/lang/String;
		Ljava/lang/invoke/MethodType;
		Ljava/lang/invoke/MethodType;
		Ljava/lang/invoke/MethodHandle;
		Ljava/lang/invoke/MethodType;)
Ljava/lang/invoke/CallSite; : run()Ljava/lang/Runnable; ()V top/ss007/Student.lambda$new$0()V (6) ()V

上面的代碼的作用是什麼呢?
總的來說,其是JVM在Runtime時創建() -> { } 對應的類,即在運行時創建一個實現了Runable接口的類(字節碼)。

我們看到此處使用了invokedynamic ,只是爲了動態的生成其對應類的字節碼,但是Lambda方法的執行仍然使用的是 invokevirtual或者invokeinterface, 在首次調用時,生成對應類的字節碼,然後就緩存起來,下次使用緩存。

那麼是時候回答一下開頭的問題了:每一個Lambda表達式是否對應一個對象?
答案是:是的!但是,注意但是,每個Lambda是對應一個對象,但是有可能出現多對一的情況。
例如Lambda1,Lambda2 結構相同,那麼他們在JVM中就有可能對應同一個對象

總結

寫一篇誤人子弟的文章實在是太費時間了,因爲你要不斷的確認自己寫的是不是真的可以實現,而不是自己的想當然。

技術真的是沒有止境,應儘早確定自己的方向,少年加油。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章