1. 程式人生 > >理解和運用Java中的Lambda

理解和運用Java中的Lambda

前提

回想一下,JDK8是2014年釋出正式版的,到現在為(2020-02-08)止已經過去了5年多。JDK8引入的兩個比較強大的新特性是Lambda表示式(下文的Lambda特指JDK提供的Lambda)和Stream,這兩個強大的特性讓函數語言程式設計在Java開發中發揚光大。這篇文章會從基本概念、使用方式、實現原理和實戰場景等角度介紹Lambda的全貌,其中還會涉及一些函數語言程式設計概念、JVM一些知識等等。

基本概念

下面介紹一些基本概念,一步一步引出Lambda的概念。

函式式介面

函式式介面和介面預設方法都是JDK8引入的新特性。函式式介面的概念可以從java.lang.FunctionalInterface

註解的API註釋中得知:

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 {@linkplain java.lang.reflect.Method#isDefault() default methods} have an implementation, they are not abstract.

簡單來說就是:@FunctionalInterface是一個提供資訊的介面(其實就是標識介面),用於表明對應的介面型別宣告是一個Java語言規範定義的函式式介面。從概念上說,一個函式式介面有且僅有一個抽象方法,因為介面預設方法必須予以實現,它們不是抽象方法。

所以可以這樣給函式式介面定義:如果一個介面宣告的時候有且僅有一個抽象方法,那麼它就是函式式介面,可以使用@FunctionalInterface註解標識。

JDK中已經定義了很多內建的函式式介面,例如:

// java.lang.Runnable
@FunctionalInterface
public interface Runnable {

    public abstract void run();
}  

// java.util.function.Supplier
@FunctionalInterface
public interface Supplier<T> {

    T get();
}

也可以自定義函式式介面,例如:

@FunctionalInterface
public interface CustomFunctionalInterface {
    
    // 可以縮寫為void process();  介面方法定義的時候,預設使用public abstract修飾
    public abstract void process();
}

介面預設方法

介面預設方法的含義可以見Java官方教程中對應的章節,在文末的參考資料可以檢視具體的連結:

Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.

簡單來說就是:預設方法允許你在你的類庫中向介面新增新的功能,並確保新增的預設方法與這些介面的較早版本編寫的程式碼二進位制相容。

介面預設方法(下稱預設方法)通過default關鍵字宣告,可以直接在介面中編寫方法體。也就是預設方法既聲明瞭方法,也實現了方法。這一點很重要,在預設方法特性出現之前,Java程式設計語言規範中,介面的本質就是方法宣告的集合體,而自預設方法特性出現之後,介面的本質也改變了。預設方法的一個例子如下:

public interface DefaultMethod {

    default void defaultVoidMethod() {

    }

    default String sayHello(String name) {
        return String.format("%s say hello!", name);
    }

    static void main(String[] args) throws Exception {
        class Impl implements DefaultMethod {

        }
        DefaultMethod defaultMethod = new Impl();
        System.out.println(defaultMethod.sayHello("throwable"));  // throwable say hello!
    }
}

如果繼承一個定義了預設方法的介面,那麼可以有如下的做法:

  • 完全忽略父介面的預設方法,那麼相當於直接繼承父介面的預設方法的實現(方法繼承)。
  • 重新宣告預設方法,這裡特指去掉default關鍵字,用public abstract關鍵字重新宣告對應的方法,相當於讓預設方法轉變為抽象方法,子類需要進行實現(方法抽象)。
  • 重新定義預設方法,也就是直接覆蓋父介面中的實現(方法覆蓋)。

結合前面一節提到的函式式介面,這裡可以綜合得出一個結論:函式式介面,也就是有且僅有一個抽象方法的介面,可以定義0個或者N(N >= 1)個預設方法。這一點正是Stream特性引入的理論基礎。舉個例子:

@FunctionalInterface
public interface CustomFunctionalInterface {

    public abstract void process();

    default void defaultVoidMethod() {

    }

    default String sayHello(String name) {
        return String.format("%s say hello!", name);
    }
}

這裡說點題外話。

在寫這篇文章的時候,筆者想起了一個前同事說過的話,大意如下:在軟體工程中,如果從零做起,任何新功能的開發都是十分簡單的,困難的是在相容所有歷史功能的前提下進行新功能的迭代。試想一下,Java迭代到今天已經過去十多年了,Hotspot VM原始碼工程已經十分龐大(手動編譯過OpenJDK Hotspot VM原始碼的人都知道過程的痛苦),任何新增的特性都要向前相容,否則很多用了歷史版本的Java應用會無法升級新的JDK版本。既要二進位制向前相容,又要迭代出新的特性,Java需要進行舍奪,預設方法就是一個例子,必須捨去介面只能定義抽象方法這個延續了多年在Java開發者中根深蒂固的概念,奪取了基於預設方法實現構築出來的流式程式設計體系。筆者有時候也在思考:如果要我去開發Stream這個新特性,我會怎麼做或者我能怎麼做?

巢狀類(Nested Classes)

巢狀類(Nested Classes),簡單來說就是:在一個類中定義另一個類,那麼在類內被定義的那個類就是巢狀類,最外層的類一般稱為封閉類(Enclosing Class)。巢狀類主要分為兩種:靜態巢狀類和非靜態巢狀類,而非靜態巢狀類又稱為內部類(Inner Classes)。

// 封閉類
class OuterClass {
    ...
    // 靜態巢狀類
    static class StaticNestedClass {
        ...
    }
    
    // 內部類
    class InnerClass {
        ...
    }
}

靜態巢狀類可以直接使用封閉的類名稱去訪問例如:OuterClass.StaticNestedClass x = new OuterClass.StaticNestedClass();,這種使用形式和一般類例項化基本沒有區別。

內部類例項的存在必須依賴於封閉類例項的存在,並且內部類可以直接訪問封閉類的任意屬性和方法,簡單來說就是內部類的例項化必須在封閉類例項化之後,並且依賴於封閉類的例項,宣告的語法有點奇特:

public class OuterClass {

    int x = 1;

    static class StaticNestedClass {

    }

    class InnerClass {
        // 內部類可以訪問封閉類的屬性
        int y = x;
    }

    public static void main(String[] args) throws Exception {
        OuterClass outerClass = new OuterClass();

        // 必須這樣例項化內部類 - 宣告的語法相對奇特
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();

        // 靜態巢狀類可以一般例項化,形式為:封閉類.靜態巢狀類
        OuterClass.StaticNestedClass staticNestedClass = new OuterClass.StaticNestedClass();

        // 如果main方法在封閉類內,可以直接使用靜態巢狀類進行例項化
        StaticNestedClass x = new StaticNestedClass();
    }
}

內部類中有兩種特殊的型別:本地類(Local Classes)和匿名類(Anonymous Classes)。

本地類是一種宣告在任意塊(block)的類,例如宣告在程式碼塊、靜態程式碼塊、例項方法或者靜態方法中,它可以訪問封閉類的所有成員屬性和方法,它的作用域就是塊內,不能在塊外使用。例如:

public class OuterClass {

    static int y = 1;
    
    {    
        // 本地類A
        class A{
            int z = y;
        }
        A a = new A();
    }

    static {
        // 本地類B
        class B{
            int z = y;
        }
        B b = new B();
    }

    private void method(){
        // 本地類C
        class C{
            int z = y;
        }
        C c = new C();
    }
}

匿名類可以讓程式碼更加簡明,允許使用者在定義類的同時予以實現,匿名類和其他內部類不同的地方是:它是一種表示式,而不是類宣告。例如:

public class OuterClass {

    interface In {

        void method(String value);
    }
    
    public void sayHello(){
        // 本地類 - 類宣告
        class LocalClass{
            
        }
        
        // 匿名類 - 是一個表示式
        In in = new In() {
            
            @Override
            public void method(String value) {
                
            }
        };
    }
}

如果用Java做過GUI開發,匿名類在Swing或者JavaFx的事件回撥中大量使用,經常會看到類似這樣的程式碼:

JButton button = new JButton();
button.addActionListener(new AbstractAction() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("按鈕事件被觸發...");
    }
});

巢狀類的型別關係圖如下:

Nested Classes
  - Static Nested Classes
  - None Nested Classes
    - Local Classes
    - Anonymous Classes
    - Other Inner Classes

Lambda表示式

下面是來自某搜尋引擎百科關於Lambda表示式的定義:

Lambda表示式(Lambda Expression)是一個匿名函式,Lambda表示式基於數學中的λ演算得名,直接對應於其中的Lambda抽象(Lambda Abstraction),是一個匿名函式,即沒有函式名的函式。Lambda表示式可以表示閉包(注意和數學傳統意義上的不同)。

Java中的Lambda表示式(下面稱Lambda)表面上和上面的定義類似,本質也是匿名函式,但其實現原理區別於一般的匿名類中的匿名函式實現,她是JDK8引入的一顆新的語法糖。

引入Lambda表示式的初衷

如果一個介面只包含一個方法,那麼匿名類的語法會變得十分笨拙和不清楚,產生大量的模板程式碼,歸結一下就是:程式碼冗餘是匿名類的最大弊端。在程式設計的時候,我們很多時候希望把功能作為引數傳遞到另一個方法,Lambda就是為此而生,Lambda允許使用者將功能視為方法引數,將程式碼視為資料。引入Lambda帶來了如下優勢:

  • 簡化程式碼,引入了強大的型別推斷和方法引用特性,簡單的功能甚至可以一行程式碼解決,解放匿名類的束縛。
  • 把功能作為引數向下傳遞,為函數語言程式設計提供了支援。

至此還得出一個結論:Lambda只適用於函式式介面對應唯一抽象方法的實現。

Lambda表示式的語法定義

Lambda語法的詳細定義如下:

// en_US
InterfaceType interfaceObject = [Method Argument List] -> Method Body

// zh_CN
介面型別 介面例項 = [方法引數列表] -> 方法體

更具體的描述應該是:

介面型別 介面例項臨時變數 = (方法引數型別X 方法引數型別X臨時變數 , 方法引數型別Y 方法引數型別Y臨時變數...) -> { 方法體... return 介面抽象方法返回值對應型別型別例項;}

一個Lambda表示式由五個部分組成:

  • 返回值:介面型別以及介面型別對應的臨時例項變數。
  • 等號:=
  • 方法引數列表:一般由中括號()包裹,格式是(型別1 型別1的臨時變數,...,型別N 型別N的臨時變數),在方法沒有過載可以明確推斷引數型別的時候,引數型別可以省略,只留下臨時變數列表。特殊地,空引數列表用()表示,如果引數只有一個,可以省略()
  • 箭頭:->
  • 方法體:一般由花括號{}包裹,格式是{方法邏輯... return 函式式介面方法返回值型別的值;},有幾點需要注意:
    • 如果方法體是空實現,用{}表示,如Runnable runnable = () -> {};
    • 如果函式式介面抽象方法的返回值為void型別,則不需要return關鍵字語句,如Runnable runnable = () -> {int i=0; i++;};
    • 如果函式式介面抽象方法的方法體僅僅包含一個表示式,則不需要使用{}包裹,如Runnable runnable = () -> System.out.println("Hello World!");

舉一些例子:

// Function - 具體
java.util.function.Function<String, Integer> functionY = (String string) -> {
    return Integer.parseInt(string);
};
// Function - 簡化
java.util.function.Function<String, Integer> functionX = string -> Integer.parseInt(string);

// Runnable - 具體
Runnable runnableX = () -> {
    System.out.println("Hello World!");
};
// Runnable - 簡化
Runnable runnableY = () -> System.out.println("Hello World!");

// 整數1-100的和 - 具體
int reduceX = IntStream.range(1, 101).reduce(0, (int addend, int augend) -> {
    return addend + augend;
});
// 整數1-100的和 - 簡化
int reduceY = IntStream.range(1, 101).reduce(0, Integer::sum);

目標型別與型別推斷

先引入下面的一個場景:

// club.throwable.Runnable
@FunctionalInterface
public interface Runnable {

    void run();

    static void main(String[] args) throws Exception {
        java.lang.Runnable langRunnable = () -> {};
        club.throwable.Runnable customRunnable = () -> {};
        langRunnable.run();
        customRunnable.run();
    }
}

筆者定義了一個和java.lang.Runnable完全一致的函式式介面club.throwable.Runnable,上面main()方法中,可以看到兩個介面對應的Lambda表示式的方法體實現也是完全一致,但是很明顯最終可以使用不同型別的介面去接收返回值,也就是這兩個Lambda的型別是不相同的。而這兩個Lambda表示式返回值的型別是我們最終期待的返回值型別(expecting a data type of XX),那麼Lambda表示式就是對應的被期待的型別,這個被期待的型別就是Lambda表示式的目標型別。

為了確定Lambda表示式的目標型別,Java編譯器會基於對應的Lambda表示式,使用上下文或者場景進行綜合推導,判斷的一個因素就是上下文中對該Lambda表示式所期待的型別。因此,只能在Java編譯器能夠正確推斷Lambda表示式目標型別的場景下才能使用Lambda表示式,這些場景包括:

  • 變數宣告。
  • 賦值。
  • 返回語句。
  • 陣列初始化器。
  • Lambda表示式函式體。
  • 條件表示式(condition ? processIfTrue() : processIfFalse())。
  • 型別轉換(Cast)表示式。

Lambda表示式除了目標型別,還包含引數列表和方法體,而方法體需要依賴於引數列表進行實現,所以方法引數也是決定目標型別的一個因素。

方法引數的型別推導的過程主要依賴於兩個語言特性:過載解析(Overload Resolution)和引數型別推導(Type Argument Inference)。

原文:For method arguments, the Java compiler determines the target type with two other language features: overload resolution and type argument inference

過載解析會為一個給定的方法呼叫(Method Invocation)尋找最合適的方法宣告(Method Declaration)。由於不同的宣告具有不同的簽名,當Lambda表示式作為方法引數時,過載解析就會影響到Lambda表示式的目標型別。編譯器會根據它對該Lambda表示式的所提供的資訊的理解做出決定。如果Lambda表示式具有顯式型別(引數型別被顯式指定),編譯器就可以直接使用Lambda表示式的返回型別;如果Lambda表示式具有隱式型別(引數型別被推導而知),過載解析則會忽略Lambda表示式函式體而只依賴Lambda表示式引數的數量。

舉個例子:

// 顯式型別
Function<String, String> functionX = (String x) -> x;

// 隱式型別
Function<String, Integer> functionY = x -> Integer.parseInt(x);

如果依賴於方法引數的型別推導最佳方法宣告時存在二義性(Ambiguous),我們就需要利用轉型(Cast)或顯式Lambda表示式來提供更多的型別資訊,從而Lambda表示式的目標型別。舉個例子:

// 編譯不通過
Object runnableX = () -> {};

// 編譯通過 - Cast
Object runnableY = (Runnable) () -> {};


// 靜態方法入參型別是函式式介面
public static void function(java.util.function.Function function) {

}

function((Function<String, Long>) (x) -> Long.parseLong(x));

作用域

關於作用域的問題記住幾點即可:

  • <1>Lambda表示式內的this引用和封閉類的this引用相同。
  • <2>Lambda表示式基於詞法作用域,它不會從超類中繼承任何變數,方法體裡面的變數和它外部環境的變數具有相同的語義。
  • <3>Lambda expressions close over values, not variables,也就是Lambda表示式對值型別封閉,對變數(引用)型別開放(這一點正好解釋了Lambda表示式內部引用外部的屬性的時候,該屬性必須定義為final)。

對於第<1>點舉個例子:

public class LambdaThis {

    int x = 1;

    public void method() {
        Runnable runnable = () -> {
            int y = this.x;
            y++;
            System.out.println(y);
        };
        runnable.run();
    }

    public static void main(String[] args) throws Exception {
        LambdaThis lambdaThis = new LambdaThis();
        lambdaThis.method();   // 2
    }
}

對於第<2>點舉個例子:

public class LambdaScope {
    
    public void method() {
        int x = 1;
        Runnable runnable = () -> {
            // 編譯不通過 - Lambda方法體外部已經定義了同名變數
            int x = 2;
        };
        runnable.run();
    }
}

對於第<3>點舉個例子:

public class LambdaValue {

    public void method() {
        (final) int x = 1;
        Runnable runnable = () -> {
            // 編譯不通過 - 外部值型別使用了final
            x ++;
        };
        runnable.run();
    }
}

public class LambdaValue {

    public void method() {
        (final) IntHolder holder = new IntHolder();
        Runnable runnable = () -> {
            // 編譯通過 - 使用了引用型別
            holder.x++;
        };
        runnable.run();
    }

    private static class IntHolder {

        int x = 1;
    }
}

方法引用

方法引用(Method Reference)是一種功能和Lambda表示式類似的表示式,需要目標型別和實現函式式介面,但是這個實現形式並不是通過方法體,而是通過方法名稱(或者關鍵字)關聯到一個已經存在的方法,本質是編譯層面的技術,旨在進一步簡化Lambda表示式方法體和一些特定表示式的實現。方法引用的型別歸結如下:

型別 例子
靜態方法引用 ClassName::methodName
指定物件例項方法引用 instanceRef::methodName
特定型別任意物件方法引用 ContainingType::methodName
超類方法引用 supper::methodName
構造器方法引用 ClassName::new
陣列構造器方法引用 TypeName[]::new

可見其基本形式是:方法容器::方法名稱或者關鍵字

舉一些基本的使用例子:

// 靜態方法引用
public class StaticMethodRef {

    public static void main(String[] args) throws Exception {
        Function<String, Integer> function = StaticMethodRef::staticMethod;
        Integer result = function.apply("10086");
        System.out.println(result);  // 10086
    }

    public static Integer staticMethod(String value) {
        return Integer.parseInt(value);
    }
}

// 指定物件例項方法引用
public class ParticularInstanceRef {

    public Integer refMethod(String value) {
        return Integer.parseInt(value);
    }

    public static void main(String[] args) throws Exception{
        ParticularInstanceRef ref = new ParticularInstanceRef();
        Function<String, Integer> function = ref::refMethod;
        Integer result = function.apply("10086");
        System.out.println(result);  // 10086
    }
}

// 特定型別任意物件方法引用
String[] stringArray = {"C", "a", "B"};
Arrays.sort(stringArray, String::compareToIgnoreCase);
System.out.println(Arrays.toString(stringArray)); // [a, B, C]

// 超類方法引用
public class SupperRef {

    public static void main(String[] args) throws Exception {
        Sub sub = new Sub();
        System.out.println(sub.refMethod("10086")); // 10086
    }

    private static class Supper {

        private Integer supperRefMethod(String value) {
            return Integer.parseInt(value);
        }
    }

    private static class Sub extends Supper {

        private Integer refMethod(String value) {
            Function<String, Integer> function = super::supperRefMethod;
            return function.apply(value);
        }
    }
}

// 構造器方法引用
public class ConstructorRef {

    public static void main(String[] args) throws Exception {
        Function<String, Person> function = Person::new;
        Person person = function.apply("doge");
        System.out.println(person.getName()); // doge
    }

    private static class Person {

        private final String name;

        public Person(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }
}

// 陣列構造器方法引用
Function<Integer, Integer[]> function = Integer[]::new;
Integer[] array = function.apply(10);
System.out.println(array.length); // 10

Java中Lambda的底層實現原理

重點要說三次:

  • Lambda表示式底層不是匿名類實現。
  • Lambda表示式底層不是匿名類實現。
  • Lambda表示式底層不是匿名類實現。

在深入學習Lambda表示式之前,筆者也曾經認為Lambda就是匿名類的語法糖:

// Lambda
Function<String, String> functionX = (String x) -> x;

// 錯誤認知
Function<String, String> functionX = new Function<String, String>() {
    @Override public Void apply(String x) {
        return x;
    }
}

Lambda就是匿名類的語法糖這個認知是錯誤的。下面舉一個例子,從原始碼和位元組碼的角度分析一下Lambda表示式編譯和執行的整個流程。

public class Sample {

    public static void main(String[] args) throws Exception {
        Runnable runnable = () -> {
            System.out.println("Hello World!");
        };
        runnable.run();
        String hello = "Hello ";
        Function<String, String> function = string -> hello + string;
        function.apply("Doge");
    }
}

新增VM引數-Djdk.internal.lambda.dumpProxyClasses=.執行上面的Sample#main()方法,專案根目錄動態生成了兩個類如下:

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class Sample$$Lambda$14 implements Runnable {
    private Sample$$Lambda$14() {
    }

    @Hidden
    public void run() {
        Sample.lambda$main$0();
    }
}

import java.lang.invoke.LambdaForm.Hidden;
import java.util.function.Function;

// $FF: synthetic class
final class Sample$$Lambda$15 implements Function {
    private final String arg$1;

    private Sample$$Lambda$15(String var1) {
        this.arg$1 = var1;
    }

    private static Function get$Lambda(String var0) {
        return new Sample$$Lambda$15(var0);
    }

    @Hidden
    public Object apply(Object var1) {
        return Sample.lambda$main$1(this.arg$1, (String)var1);
    }
}

反查兩個類的位元組碼,發現了類修飾符為final synthetic。接著直接看封閉類Sample的位元組碼:

public class club/throwable/Sample {
     <ClassVersion=52>
     <SourceFile=Sample.java>

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

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

     public static main(java.lang.String[] arg0) throws java/lang/Exception { //([Ljava/lang/String;)V
         <localVar:index=0 , name=args , desc=[Ljava/lang/String;, sig=null, start=L1, end=L2>
         <localVar:index=1 , name=runnable , desc=Lclub/throwable/Runnable;, sig=null, start=L3, end=L2>
         <localVar:index=2 , name=hello , desc=Ljava/lang/String;, sig=null, start=L4, end=L2>
         <localVar:index=3 , name=function , desc=Ljava/util/function/Function;, sig=Ljava/util/function/Function<Ljava/lang/String;Ljava/lang/String;>;, start=L5, end=L2>

         L1 {
             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()Lclub/throwable/Runnable; ()V club/throwable/Sample.lambda$main$0()V (6) ()V
             astore1
         }
         L3 {
             aload1
             invokeinterface club/throwable/Runnable.run()V
         }
         L6 {
             ldc "Hello " (java.lang.String)
             astore2
         }
         L4 {
             aload2
             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; : apply(Ljava/lang/String;)Ljava/util/function/Function; (Ljava/lang/Object;)Ljava/lang/Object; club/throwable/Sample.lambda$main$1(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; (6) (Ljava/lang/String;)Ljava/lang/String;
             astore3
         }
         L5 {
             aload3
             ldc "Doge" (java.lang.String)
             invokeinterface java/util/function/Function.apply(Ljava/lang/Object;)Ljava/lang/Object;
             pop
         }
         L7 {
             return
         }
         L2 {
         }
     }

     private static synthetic lambda$main$1(java.lang.String arg0, java.lang.String arg1) { //(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
         <localVar:index=0 , name=hello , desc=Ljava/lang/String;, sig=null, start=L1, end=L2>
         <localVar:index=1 , name=string , desc=Ljava/lang/String;, sig=null, start=L1, end=L2>

         L1 {
             new java/lang/StringBuilder
             dup
             invokespecial java/lang/StringBuilder.<init>()V
             aload0 // reference to arg0
             invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
             aload1
             invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
             invokevirtual java/lang/StringBuilder.toString()Ljava/lang/String;
             areturn
         }
         L2 {
         }
     }

     private static synthetic lambda$main$0() { //()V
         L1 {
             getstatic java/lang/System.out:java.io.PrintStream
             ldc "Hello World!" (java.lang.String)
             invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
         }
         L2 {
             return
         }
     }
// The following inner classes couldn't be decompiled: java/lang/invoke/MethodHandles$Lookup 
}

上面的位元組碼已經被Bytecode-Viewer工具格式化過,符合於人的閱讀習慣,從位元組碼的閱讀,結合前面的分析大概可以得出下面的結論:

  • <1>Lambda表示式在編譯期通過位元組碼增強技術新增一個模板類實現對應的介面型別,這個模板類的所有屬性都使用final修飾,模板類由關鍵字final synthetic修飾。
  • <2>:封閉類會基於類內的Lambda表示式型別生成private static synthetic修飾的靜態方法,該靜態方法的方法體就是來源於Lambda方法體,這些靜態方法的名稱是lambda$封閉類方法名$遞增數字
  • <3>Lambda表示式呼叫最終通過位元組碼指令invokedynamic,忽略中間過程,最後呼叫到第<2>步中對應的方法。

限於篇幅問題,這裡把Lambda表示式的底層原理做了簡單的梳理(這個推導過程僅限於個人理解,依據尚未充分):

  • <1>:封閉類會基於類內的Lambda表示式型別生成private static synthetic修飾的靜態方法,該靜態方法的方法體就是來源於Lambda方法體,這些靜態方法的名稱是lambda$封閉類方法名$遞增數字
  • <2>Lambda表示式會通過LambdaMetafactory#metafactory()方法,生成一個對應函式式介面的模板類,模板類的介面方法實現引用了第<1>步中定義的靜態方法,同時建立一個呼叫點ConstantCallSite例項,後面會通過Unsafe#defineAnonymousClass()例項化模板類。。
  • <3>:呼叫點ConstantCallSite例項中的方法控制代碼MethodHandle會根據不同場景選取不同的實現,MethodHandle的子類很多,這裡無法一一展開。
  • <4>:通過invokedynamice指令,基於第<1>步中的模板類例項、第<3>步中的方法控制代碼以及方法入參進行方法控制代碼的呼叫,實際上最終委託到第<1>步中定義的靜態方法中執行。

如果想要跟蹤Lambda表示式的整個呼叫生命週期,可以以LambdaMetafactory#metafactory()方法為入口開始DEBUG,呼叫鏈路十分龐大,需要有足夠的耐心。總的來說就是:Lambda表示式是基於JSR-292引入的動態語言呼叫包java.lang.invokeUnsafe#defineAnonymousClass()定義的輕量級模板類實現的,主要用到了invokedynamice位元組碼指令,關聯到方法控制代碼MethodHandle、呼叫點CallSite等相對複雜的知識點,這裡不再詳細展開。

實戰

基於JdbcTemplate進行輕量級DAO封裝

假設訂單表的DDL如下:

CREATE TABLE `t_order`
(
    id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    create_time DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
    edit_time   DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    user_id     BIGINT UNSIGNED NOT NULL COMMENT '使用者ID',
    order_id    VARCHAR(64)     NOT NULL COMMENT '訂單ID',
    amount      DECIMAL(12, 2)  NOT NULL DEFAULT 0 COMMENT '訂單金額',
    INDEX idx_user_id (user_id),
    UNIQUE uniq_order_id (order_id)
) COMMENT '訂單表';

下面基於JdbcTemplate封裝一個輕量級的OrderDao

// 輔助介面
@FunctionalInterface
public interface PreparedStatementProcessor {

    void process(PreparedStatement ps) throws SQLException;
}

@FunctionalInterface
public interface ResultSetConverter<T> {

    T convert(ResultSet resultSet) throws SQLException;
}

// OrderDao介面
public interface OrderDao {

    int insertSelective(Order record);

    int updateSelective(Order record);

    Order selectOneByOrderId(String orderId);

    List<Order> selectByUserId(Long userId);
}

// OrderDao實現
@Repository
@RequiredArgsConstructor
public class MySqlOrderDao implements OrderDao {

    private final JdbcTemplate jdbcTemplate;

    private static final ResultSetConverter<Order> CONVERTER = r -> {
        Order order = new Order();
        order.setId(r.getLong("id"));
        order.setCreateTime(r.getTimestamp("create_time").toLocalDateTime());
        order.setEditTime(r.getTimestamp("edit_time").toLocalDateTime());
        order.setUserId(r.getLong("user_id"));
        order.setAmount(r.getBigDecimal("amount"));
        order.setOrderId(r.getString("order_id"));
        return order;
    };

    private static final ResultSetExtractor<List<Order>> MULTI = r -> {
        List<Order> list = new ArrayList<>();
        while (r.next()) {
            list.add(CONVERTER.convert(r));
        }
        return list;
    };

    private static final ResultSetExtractor<Order> SINGLE = r -> {
        if (r.next()) {
            return CONVERTER.convert(r);
        }
        return null;
    };

    @Override
    public int insertSelective(Order record) {
        List<PreparedStatementProcessor> processors = new ArrayList<>();
        StringBuilder sql = new StringBuilder("INSERT INTO t_order(");
        Cursor cursor = new Cursor();
        if (null != record.getId()) {
            int idx = cursor.add();
            sql.append("id,");
            processors.add(p -> p.setLong(idx, record.getId()));
        }
        if (null != record.getOrderId()) {
            int idx = cursor.add();
            sql.append("order_id,");
            processors.add(p -> p.setString(idx, record.getOrderId()));
        }
        if (null != record.getUserId()) {
            int idx = cursor.add();
            sql.append("user_id,");
            processors.add(p -> p.setLong(idx, record.getUserId()));
        }
        if (null != record.getAmount()) {
            int idx = cursor.add();
            sql.append("amount,");
            processors.add(p -> p.setBigDecimal(idx, record.getAmount()));
        }
        if (null != record.getCreateTime()) {
            int idx = cursor.add();
            sql.append("create_time,");
            processors.add(p -> p.setTimestamp(idx, Timestamp.valueOf(record.getCreateTime())));
        }
        if (null != record.getEditTime()) {
            int idx = cursor.add();
            sql.append("edit_time,");
            processors.add(p -> p.setTimestamp(idx, Timestamp.valueOf(record.getEditTime())));
        }
        StringBuilder realSql = new StringBuilder(sql.substring(0, sql.lastIndexOf(",")));
        realSql.append(") VALUES (");
        int idx = cursor.idx();
        for (int i = 0; i < idx; i++) {
            if (i != idx - 1) {
                realSql.append("?,");
            } else {
                realSql.append("?");
            }
        }
        realSql.append(")");
        // 傳入主鍵的情況
        if (null != record.getId()) {
            return jdbcTemplate.update(realSql.toString(), p -> {
                for (PreparedStatementProcessor processor : processors) {
                    processor.process(p);
                }
            });
        } else {
            // 自增主鍵的情況
            KeyHolder keyHolder = new GeneratedKeyHolder();
            int count = jdbcTemplate.update(p -> {
                PreparedStatement ps = p.prepareStatement(realSql.toString(), Statement.RETURN_GENERATED_KEYS);
                for (PreparedStatementProcessor processor : processors) {
                    processor.process(ps);
                }
                return ps;
            }, keyHolder);
            record.setId(Objects.requireNonNull(keyHolder.getKey()).longValue());
            return count;
        }
    }

    @Override
    public int updateSelective(Order record) {
        List<PreparedStatementProcessor> processors = new ArrayList<>();
        StringBuilder sql = new StringBuilder("UPDATE t_order SET ");
        Cursor cursor = new Cursor();
        if (null != record.getId()) {
            int idx = cursor.add();
            sql.append("id = ?,");
            processors.add(p -> p.setLong(idx, record.getId()));
        }
        if (null != record.getOrderId()) {
            int idx = cursor.add();
            sql.append("order_id = ?,");
            processors.add(p -> p.setString(idx, record.getOrderId()));
        }
        if (null != record.getUserId()) {
            int idx = cursor.add();
            sql.append("user_id = ?,");
            processors.add(p -> p.setLong(idx, record.getUserId()));
        }
        if (null != record.getAmount()) {
            int idx = cursor.add();
            sql.append("amount = ?,");
            processors.add(p -> p.setBigDecimal(idx, record.getAmount()));
        }
        if (null != record.getCreateTime()) {
            int idx = cursor.add();
            sql.append("create_time = ?,");
            processors.add(p -> p.setTimestamp(idx, Timestamp.valueOf(record.getCreateTime())));
        }
        if (null != record.getEditTime()) {
            int idx = cursor.add();
            sql.append("edit_time = ?,");
            processors.add(p -> p.setTimestamp(idx, Timestamp.valueOf(record.getEditTime())));
        }
        StringBuilder realSql = new StringBuilder(sql.substring(0, sql.lastIndexOf(",")));
        int idx = cursor.add();
        processors.add(p -> p.setLong(idx, record.getId()));
        realSql.append(" WHERE id = ?");
        return jdbcTemplate.update(realSql.toString(), p -> {
            for (PreparedStatementProcessor processor : processors) {
                processor.process(p);
            }
        });
    }

    @Override
    public Order selectOneByOrderId(String orderId) {
        return jdbcTemplate.query("SELECT * FROM t_order WHERE order_id = ?", p -> p.setString(1, orderId), SINGLE);
    }

    @Override
    public List<Order> selectByUserId(Long userId) {
        return jdbcTemplate.query("SELECT * FROM t_order WHERE order_id = ?", p -> p.setLong(1, userId), MULTI);
    }

    private static class Cursor {

        private int idx;

        public int add() {
            idx++;
            return idx;
        }

        public int idx() {
            return idx;
        }
    }
}

類似於Mybatis Generator,上面的DAO實現筆者已經做了一個簡單的生成器,只要配置好資料來源的連線屬性和表過濾規則就可以生成對應的實體類和DAO類。

基於Optional進行VO設定值

// 假設VO有多個層級,每個層級都不知道父節點是否為NULL,如下
// - OrderInfoVo
//   - UserInfoVo
//     - AddressInfoVo
//        - address(屬性)
// 假設我要為address屬性賦值,那麼就會產生箭頭型程式碼。

// 常規方法
String address = "xxx";
OrderInfoVo o = ...;
if(null != o){
    UserInfoVo uiv = o.getUserInfoVo();
    if (null != uiv){
        AddressInfoVo aiv = uiv.getAddressInfoVo();
        if (null != aiv){
            aiv.setAddress(address);
        }
    }
}

// 使用Optional和Lambda
String address = "xxx";
OrderInfoVo o = ...;
Optional.ofNullable(o).map(OrderInfoVo::getUserInfoVo).map(UserInfoVo::getAddressInfoVo).ifPresent(a -> a.setAddress(address));

小結

LambdaJava中一個香甜的語法糖,擁抱Lambda,擁抱函數語言程式設計,筆者也經歷過抗拒、不看好、上手和真香的過程,目前也大量使用StreamLambda,能在保證效能的前提下,儘可能簡化程式碼,解放勞動力。時代在進步,Java也在進步,這是很多人活著和堅持程式設計事業的信念。

參考資料:

  • Lambda Expressions
  • Default Methods
  • State of the Lambda
  • JDK11部分原始碼

個人部落格

  • Throwable's Blog

(本文完 e-a-20200208 c-5-d