JDK8 的 Lambda 表達式原理

JDK8 使用一行 Lambda 表達式能夠代替先前用匿名類五六行代碼所作的事情,那麼它是怎麼實現的呢?從所周知,匿名類會在編譯的時候生成與宿主類帶上 $1, $2 的類文件,如寫在 TestLambda 中的匿名類產生成類文件是 TestLambda$1.class, TestLambda$2.class 等。java

我試驗了一下,若是使用的是 Lambda 表達式並不會生成額外的類文件,那麼字節碼裏是什麼樣子的?來看下用  javap -c 反編譯出下面文件產生的 TestLambda.class,兩個方法,一個是  byAnonymousClass() 使用匿名類,另外一個是 byLambda 使用 Lambda 的方式:編程

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package cc.unmi.testjdk8;
 
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
 
import javax.swing.JButton;
 
public class TestLambda{
     private JButton button = new JButton();
     
     public void byLambda() {
         button.addActionListener((ActionEvent e) -> System.out.println( "Lambda" ));
     }
     
     public void byAnonymousClass(){
         
         button.addActionListener( new ActionListener() {
             @Override
             public void actionPerformed(ActionEvent e) {
                 System.out.println( "Anonymous class" );
             }
         });
     }
}

相應的字節碼以下:ide

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class cc.unmi.testjdk8.TestLambda {
   public cc.unmi.testjdk8.TestLambda();
     Code:
        0 : aload_0       
        1 : invokespecial # 10                 // Method java/lang/Object."<init>":()V
        4 : aload_0       
        5 : new           # 12                 // class javax/swing/JButton
        8 : dup           
        9 : invokespecial # 14                 // Method javax/swing/JButton."<init>":()V
       12 : putfield      # 15                 // Field button:Ljavax/swing/JButton;
       15 : return        
 
   public void byLambda();
     Code:
        0 : aload_0     
        1 : getfield      # 15                 // Field button:Ljavax/swing/JButton;
        4 : invokedynamic # 25 0             // InvokeDynamic #0:actionPerformed:()Ljava/awt/event/ActionListener;
        9 : invokevirtual # 26                 // Method javax/swing/JButton.addActionListener:(Ljava/awt/event/ActionListener;)V
       12 : return        
 
   public void byAnonymousClass();
     Code:
        0 : aload_0       
        1 : getfield      # 15                 // Field button:Ljavax/swing/JButton;
        4 : new           # 31                 // class cc/unmi/testjdk8/TestLambda$1
        7 : dup           
        8 : aload_0       
        9 : invokespecial # 33                 // Method cc/unmi/testjdk8/TestLambda$1."<init>":(Lcc/unmi/testjdk8/TestLambda;)V
       12 : invokevirtual # 26                 // Method javax/swing/JButton.addActionListener:(Ljava/awt/event/ActionListener;)V
       15 : return        
}

對比後咱們發現,匿名類的方式會建立一個匿名類(這是廢話),如編譯出的的 TestLambda$1.class 文件,在磁盤上能看到 TestLambda$1.class 文件函數式編程

而 Lambda 的方式則不會產生額外的類文件,咱們可讓 TestLambda 只保留 byLambda() 方法,就會發現編譯後只會有 TestLambda.class 文件。函數

對比方法調用指令,byLambda 中使用了一個 JDK7 新加的 invokedynamic 虛擬機指令。invokedynamic 就是個關鍵,這裏不去深挖,只簡單說明,總之它對於 Java 進行函數式編程,加強了語言的動態性意義匪淺,它從新引入了像 C 裏函數指針相似的方法句柄的概念。JDK7 以前的方法調用指令有 invokestatic, invokespecial, invokevirtual 和 invokeinterface 四個,它們都是在編譯時就肯定了實際調用哪一個方法。而 invokedynamic 能讓虛擬機在執行到該指令時纔去動態的連接,調用實際的方法,因此每一個 invokedynamic 就是一個動態調用點。工具

具體到咱們的例子,也就是說對於這個例子虛擬機會在執行 byLambda 的 invokedynamic #25,  0  指令時動態的在內存中建立一個相似與 TestLambda$1 的類,名字多是 TestLambda$1$$Lambda$1。spa

爲此,我專門作了個實驗,下面的代碼編譯會生成兩個類文件,TestLambda.class,TestLambda$1.class, 第二個類文件是由 new ActionListener() 時建立的匿名類。 而後執行下面的代碼:指針

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package cc.unmi.testjdk8;
 
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
 
import javax.swing.JButton;
import javax.swing.JFrame;
 
public class TestLambda{
     private static final JButton button = new JButton( "Click" );
     
     public static void main(String[] args) {
         JFrame frame = new JFrame();
         frame.setSize( 600 , 480 );
         frame.add(button);
         button.addActionListener( new ActionListener() {
             @Override
             public void actionPerformed(ActionEvent e) {
                 button.addActionListener((ActionEvent ae) -> {
                     System.out.println( "button clicked." );
                 });                
             }
         });
         
         frame.setVisible( true );
     }
}

JDK 給咱們提供了很多分析 JVM 的工具,如 jps, jinfo, jstack, jmap, jhat, jconsole, jvisualvm 等。code

命令:orm

jps         #可看到 Java Process ID
jmap -dump:file=before_click.dump <pid>   #click 前堆轉儲爲文件
jmap -dump:file=after_click.dump <pid>   #click 後堆轉儲爲文件
jhat before_click.dump         #默認在  7000 端口打開 Web 服務瀏覽 open http://localhost:7000
jhat -port 7001 after_click.dump  #open http://localhost:7001

咱們可分別在點擊按鈕的先後用 jmap 生成快照文件 before_click.dump 和 after_click.dump。在點擊按鈕以前虛擬機還未真正執行到 Lambda 表達式。

用 jhat 瀏覽 before_click.dump,即點擊按鈕以前的快照,看到的是:

java_anonymous_class_1

只加載了兩個類,TestLambda 和匿名類 TestLambda$1。點擊 class cc.unmi.testjdk8.TestLambda$1, 看到 TestLambda$1 中是說繼承自 Object, 並未告知它與 ActionListner 有何實現上的關係。

java_anonymous_class_2

再用 jhat 打開點擊按鈕執行了 Lambda 表達式後的快照 after_click.dump,這時候就會發現多一個類 cc.unmi.testjdk8.TestLambda$1$$Lambda$1

java_lambda_1

在磁盤上並不存在這個文件,這是在執行 Lambda 表達式時內存中動態生成的,點擊它看看它的父類是什麼

java_lambda_2

在 JDK8 正式版中,它的父類變成了 java.lang.Object。

在早先的 JDK8 Beta 版中,它的父類是 MagicLambdaImpl,確實有點名符其實,Magic,以圖爲證:
jdk8_beta_lambda_class

上面使用的 JDK 工具是 jmap 和 jhat,也能夠用 Java VisualVM -- 即 jvisualvm 指令來查看執行 lambda 先後的內存快照。截了兩個圖:

lambda_jvisualvm_1

上圖爲點擊按鈕前的快照,能夠看到只有兩個類

lambda_jvisualvm_2

上圖是點擊按鈕後的快照,又多了 cc.unmi.testjdk8.TestLambda$1$$Lambda$1  這個類。

小結:JDK8 在實現 Lambda 時使用了 JDK7 虛擬機開始有的 invokedynamic  方法調用指令,該知使得虛擬機執行到 Lambda 表達式時才動態的去建立相應的實現類,並加載執行。

相關文章
相關標籤/搜索