理解和運用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.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也在進步,這是很多人活着和堅持編程事業的信念。

參考資料:

個人博客

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

技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM