「Java8系列」神祕的Lambda

雙哥發表於2019-07-16

接觸背景

第一次接觸lambda表示式時,感覺這個東西挺神奇的(高逼格),一個()加->就能傳遞一段程式碼,當時公司專案中接手同事的程式碼,自己也對java8的特性不瞭解,看的也是一頭霧水,之後就趕快看了下《java8實戰》這本書,決定寫一個java8特性系列的部落格,既加深自己的印象,還能跟大家分享一下,希望大家多多指教?。

什麼是Lambda?

Lambda是一個匿名函式,我們可以把Lambda表示式理解為是一段可以傳遞的程式碼(將程式碼像引數一樣進行傳遞,稱為行為引數化)。Lambda允許把函式作為一個方法的引數(函式作為引數傳遞進方法中),要做到這一點就需要了解,什麼是函式式介面,這裡先不做介紹,等下一篇在講解。

首先先看一下lambda長什麼樣? 正常寫法:

new Thread(new Runnable() {
    @Override
    public void run() {
       System.out.println("hello lambda");
    }
}).start();
複製程式碼

lambda寫法:

new Thread(
    () -> System.out.println("hello lambda")
).start();
複製程式碼

怎麼樣?是不是感覺很簡潔,沒錯,這就是lambda的魅力,他可以讓你寫出來的程式碼更簡單、更靈活。

Lambda怎麼寫?

在這裡插入圖片描述
大家看一些上面的這個圖,這就是lambda的語法,一個lambda分為三部分:引數列表、操作符、lambda體。以下是lambda表示式的重要特徵:

  • 可選型別宣告: 不需要宣告引數型別,編譯器可以統一識別引數值。也就說(s) -> System.out.println(s)和 (String s) -> System.out.println(s)是一樣的編譯器會進行型別推斷所以不需要新增引數型別。
  • 可選的引數圓括號: 一個引數無需定義圓括號,但多個引數需要定義圓括號。例如:
  1. s -> System.out.println(s) 一個引數不需要新增圓括號。
  2. (x, y) -> Integer.compare(y, x) 兩個引數新增了圓括號,否則編譯器報錯。
  • 可選的大括號: 如果主體包含了一個語句,就不需要使用大括號。
    1. s -> System.out.println(s) , 不需要大括號.
    2. (s) -> { if (s.equals("s")){ System.out.println(s); } }; 需要大括號
  • 可選的返回關鍵字: 如果主體只有一個表示式返回值則編譯器會自動返回值,大括號需要指定明表示式返回了一個數值。

Lambda體不加{ }就不用寫return:

 Comparator<Integer> com = (x, y) -> Integer.compare(y, x); 
複製程式碼

Lambda體加上{ }就需要新增return:

  Comparator<Integer> com = (x, y) -> {
            int compare = Integer.compare(y, x);
            return compare;
        }; 
複製程式碼

型別推斷

上面我們看到了一個lambda表示式應該怎麼寫,但lambda中有一個重要特徵是可選引數型別宣告,就是說不用寫引數的型別,那麼為什麼不用寫呢?它是怎麼知道的引數型別呢?這就涉及到型別推斷了。

java8的泛型型別推斷改進:

  • 支援通過方法上下文推斷泛型目標型別
  • 支援在方法呼叫鏈路中,泛型型別推斷傳遞到最後一個方法
List<Person> ps = ...
Stream<String> names = ps.stream().map(p -> p.getName());
複製程式碼

在上面的程式碼中,ps的型別是List<Person>,所以ps.stream()的返回型別是Stream<Person>。map()方法接收一個型別為Function<T, R>的函式式介面,這裡T的型別即是Stream元素的型別,也就是Person,而R的型別未知。由於在過載解析之後lambda表示式的目標型別仍然未知,我們就需要推導R的型別:通過對lambda表示式lambda進行型別檢查,我們發現lambda體返回String,因此R的型別是String,因而map()返回Stream<String>。絕大多數情況下編譯器都能解析出正確的型別,但如果碰到無法解析的情況,我們則需要:

  • 使用顯式lambda表示式(為引數p提供顯式型別)以提供額外的型別資訊
  • 把lambda表示式轉型為Function<Person, String>
  • 為泛型引數R提供一個實際型別。( <String>map(p -> p.getName()))

方法引用

方法引用是用來直接訪問類或者例項已經存在的方法或構造方法,提供了一種引用而不執行方法的方式。是一種更簡潔更易懂的Lambda表示式,當Lambda表示式中只是執行一個方法呼叫時,直接使用方法引用的形式可讀性更高一些。 方法引用使用 “ :: ” 操作符來表示,左邊是類名或例項名,右邊是方法名。 (注意:方法引用::右邊的方法名是不需要加()的,例:User::getName)

方法引用的幾種形式:

  • 類 :: 靜態方法
  • 類 :: 例項方法
  • 物件 :: 例項方法
例如:
    Consumer<String> consumer = (s) -> System.out.println(s);
等同於:
    Consumer<String> consumer = System.out::println;

例如:
    Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);
等同於:
    Function<String, Integer> stringToInteger = Integer::parseInt;

例如:
    BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element);
等同於:
    BiPredicate<List<String>, String> contains = List::contains;
複製程式碼

注意:

  • Lambda體中呼叫方法的引數列表與返回值型別,要與函式式介面中抽象方法的函式列表和返回值型別儲存一致
  • 若Lambda引數列表中的第一個引數是例項方法的呼叫者,而第二個引數是例項方法的引數時,可以使用ClassName::method

構造器引用

語法格式:類名::new

例如:
    Supplier<User> supplier = ()->new User();

等同於:
    Supplier<User> supplier = User::new;
複製程式碼

注意: 需要呼叫的構造器方法與函式式介面中抽象方法的引數列表保持一致。

Lambda是怎麼實現的?

研究了半天Lambda怎麼寫,可是它的原理是什麼?我們簡單看個例子,看看真相到底是什麼:

public class StreamTest {

    public static void main(String[] args) {
        printString("hello lambda", (String s) -> System.out.println(s));

    }

    public static void printString(String s, Print<String> print) {
        print.print(s);
    }
}

@FunctionalInterface
interface Print<T> {
    public void print(T t);
}
複製程式碼

上面的程式碼自定義了一個函式式介面,定義一個靜態方法然後用這個函式式介面來接收引數。編寫完這個類以後,我們到終端介面javac進行編譯,然後用javap(javap是jdk自帶的反解析工具。它的作用就是根據class位元組碼檔案,反解析出當前類對應的code區(彙編指令)、本地變數表、異常表和程式碼行偏移量對映表、常量池等等資訊。)進行解析,如下圖:

  • 執行javap -p 命令 ( -p -private 顯示所有類和成員)
    \[外鏈圖片轉存失敗(img-WRQorife-1563206705415)(./1563196007654.png)\]
    看上圖發現在編譯Lambda表示式生成了一個lambda$main$0靜態方法,這個靜態方法實現了Lambda表示式的邏輯,現在我們知道原來Lambda表示式被編譯成了一個靜態方法,那麼這個靜態方式是怎麼呼叫的呢?我們繼續進行
  • 執行javap -v -p 命令 ( -v -verbose 輸出附加資訊)
  public com.lxs.stream.StreamTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #2                  // String hello lambda
         2: invokedynamic #3,  0              // InvokeDynamic #0:print:()Lcom/lxs/stream/Print;
         7: invokestatic  #4                  // Method printString:(Ljava/lang/String;Lcom/lxs/stream/Print;)V
        10: return
      LineNumberTable:
        line 10: 0
        line 12: 10

  public static void printString(java.lang.String, com.lxs.stream.Print<java.lang.String>);
    descriptor: (Ljava/lang/String;Lcom/lxs/stream/Print;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_1
         1: aload_0
         2: invokeinterface #5,  2            // InterfaceMethod com/lxs/stream/Print.print:(Ljava/lang/Object;)V
         7: return
      LineNumberTable:
        line 15: 0
        line 16: 7
    Signature: #19                          // (Ljava/lang/String;Lcom/lxs/stream/Print<Ljava/lang/String;>;)V

  private static void lambda$main$0(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         7: return
      LineNumberTable:
        line 10: 0
}
SourceFile: "StreamTest.java"
InnerClasses:
     public static final #58= #57 of #61; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #27 invokestatic 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;
    Method arguments:
      #28 (Ljava/lang/Object;)V
      #29 invokestatic com/lxs/stream/StreamTest.lambda$main$0:(Ljava/lang/String;)V
      #30 (Ljava/lang/String;)V
複製程式碼

這裡只貼出了一部分的位元組碼結構,由於常量池定義太長了,就沒有貼上。

InnerClasses:
     public static final #58= #57 of #61; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #27 invokestatic 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;
    Method arguments:
      #28 (Ljava/lang/Object;)V
      #29 invokestatic com/lxs/stream/StreamTest.lambda$main$0:(Ljava/lang/String;)V
      #30 (Ljava/lang/String;)V
複製程式碼

通過這段位元組碼結構發現是要生成一個內部類,使用invokestatic呼叫了一個LambdaMetafactory.metafactory方法,並把lambda$main$0作為引數傳了進去,我們來看metafactory 的方法裡的實現程式碼:

    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 構建了這個內部類。也就是生成了一個StreamTest$$Lambda$1.class這樣的內部類,這個類是在執行的時候構建的,並不會儲存在磁碟中。

    @Override
    CallSite buildCallSite() throws LambdaConversionException {
        final Class<?> innerClass = spinInnerClass();
        以下省略。。。
    }
複製程式碼

如果想看到這個構建的類,可以通過設定環境引數 System.setProperty("jdk.internal.lambda.dumpProxyClasses", " . "); 會在你指定的路徑 . 當前執行路徑上生成這個內部類。我們看下一下生成的類長什麼樣

在這裡插入圖片描述
從圖中可以看出動態生成的內部類實現了我自定義的函式式介面,並且重寫了函式式介面中的方法。

我們在javap -v -p StreamTest$$Lambda$1.class看下:

{
  private com.lxs.stream.StreamTest$$Lambda$1();
    descriptor: ()V
    flags: ACC_PRIVATE
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #10                 // Method java/lang/Object."<init>":()V
         4: return

  public void print(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: checkcast     #15                 // class java/lang/String
         4: invokestatic  #21                 // Method com/lxs/stream/StreamTest.lambda$main$0:(Ljava/lang/String;)V
         7: return
    RuntimeVisibleAnnotations:
      0: #13()
}
複製程式碼

發現在重寫的parint方法中使用invokestatic指令呼叫了lambda$main$0方法。

總結: 這樣實現了Lambda表示式,使用invokedynamic指令,執行時呼叫LambdaMetafactory.metafactory動態的生成內部類,實現了函式式介面,並在重寫函式式介面中的方法,在方法內呼叫lambda$main$0,內部類裡的呼叫方法塊並不是動態生成的,只是在原class裡已經編譯生成了一個靜態的方法,內部類只需要呼叫該靜態方法。

大家看后辛苦點個贊點關注哦!後續還會後更多的部落格。 如有錯誤,煩請指正。

相關文章