天天看點

了解和運用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中已經定義了很多内置的函數式接口,例如:

Copy

// java.lang.Runnable

@FunctionalInterface

public interface Runnable {

public abstract void run();           

}

// java.util.function.Supplier

public interface Supplier {

T get();           

}

也可以自定義函數式接口,例如:

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特性引入的理論基礎。舉個例子:

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)的類,例如聲明在代碼塊、靜态代碼塊、執行個體方法或者靜态方法中,它可以通路封閉類的所有成員屬性和方法,它的作用域就是塊内,不能在塊外使用。例如:

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();
}           

匿名類可以讓代碼更加簡明,允許使用者在定義類的同時予以實作,匿名類和其他内部類不同的地方是:它是一種表達式,而不是類聲明。例如:

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 functionY = (String string) -> {

return Integer.parseInt(string);           

};

// Function - 簡化

java.util.function.Function 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

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 functionX = (String x) -> x;

// 隐式類型

Function 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) (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 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 function = Integer[]::new;

Integer[] array = function.apply(10);

System.out.println(array.length); // 10

Java中Lambda的底層實作原理#

重點要說三次:

Lambda表達式底層不是匿名類實作。

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

// Lambda

// 錯誤認知

Function functionX = new Function() {

@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.util.function.Function;

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.invoke和Unsafe#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:

// 輔助接口

public interface PreparedStatementProcessor {

void process(PreparedStatement ps) throws SQLException;           

public interface ResultSetConverter {

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

Optional.ofNullable(o).map(OrderInfoVo::getUserInfoVo).map(UserInfoVo::getAddressInfoVo).ifPresent(a -> a.setAddress(address));

小結#

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

參考資料:

Lambda Expressions

Default Methods

State of the Lambda

JDK11部分源碼

個人部落格#

Throwable's Blog

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

作者: throwable

出處:

https://www.cnblogs.com/throwable/p/12288004.html