Java lambda表示式基本使用

Xianuii發表於2022-11-26

程式碼示例:java.lambda.LambdaExpression

1 本質

  • lambda表示式本質上是對匿名內部類例項的一種簡化寫法。

1.1 案例

有以下List<Integer>物件:

List<Integer> list = Arrays.asList(1, 3, 5, 7, 9, 2, 4, 6, 8, 10);

在對List進行從小大大排序時,會用到List#sort(Comparator)方法,需要傳遞實現Comparator介面的物件作為引數:

default void sort(Comparator<? super E> c) {
    // 省略方法體
}

可以想到有如下四種不同的程式碼編寫的方式。

1、 建立Comparator的實現類

根據需求,手動實現Comparator介面:

public class AscComparator implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
}

然後,建立AscComparator例項,傳給List#sort(Comparator)方法:

Comparator<Integer> ascComparator = new AscComparator();
list.sort(ascComparator);

2、建立Comparator的匿名物件

可以直接建立Comparator的匿名物件,然後傳給List#sort(Comparator)方法:

Comparator<Integer> anonymousComparator = new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
};
list.sort(anonymousComparator);

等價於:

list.sort(new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
});

3、lambda表示式

直接使用lambda表示式:

list.sort((o1, o2) -> o1.compareTo(o2));

4、方法引用

使用方法引用(方法引用具體概念和使用可以檢視相關文章):

list.sort(Integer::compare);

可以明顯看出,使用lambda表示式和方法引用極大提高了開發的速度,提升了程式碼的簡潔性。

1.2 本質

實際上,lambda表示式只是JVM提供的語法糖。在JVM執行過程中,會根據lambda表示式的規則,動態建立出匿名的介面實現類物件。

  • lambda表示式本質上是Java物件。

可以透過檢視lambda表示式的Class物件和例項物件來證明這一點:

public class LambdaExpression {
    public void printConsumer(Consumer consumer) {
        System.out.println(consumer.getClass());
        System.out.println(consumer.getClass().getInterfaces()[0]);
        System.out.println(consumer);
    }
}

1、案例1

執行以下程式碼:

LambdaExpression lambdaObjPrinter = new LambdaExpression();
lambdaObjPrinter.printConsumer(o -> o.getClass());
lambdaObjPrinter.printConsumer(o -> o.getClass());

會有如下輸出:

class lambda.LambdaExpression$$Lambda$1/2003749087
interface java.util.function.Consumer
lambda.LambdaExpression$$Lambda$1/2003749087@41629346
class lambda.LambdaExpression$$Lambda$2/1078694789
interface java.util.function.Consumer
lambda.LambdaExpression$$Lambda$2/1078694789@6d311334
  • 這證明了執行過程中會根據lambda表示式動態生成函式式介面的實現類,並建立該實現類的例項。
  • 同時,先後執行的2個lambda表示式,儘管格式相同,仍然動態生成了2個實現類。

檢視編譯後的.class檔案如下:

LambdaExpression lambdaObjPrinter = new LambdaExpression();
lambdaObjPrinter.printConsumer((o) -> {
    o.getClass();
});
lambdaObjPrinter.printConsumer((o) -> {
    o.getClass();
});

2、案例2

執行如下程式碼:

LambdaExpression lambdaObjPrinter = new LambdaExpression();
for (int i = 0; i < 2; i++) {
    lambdaObjPrinter.printConsumer(o -> o.getClass());
}
System.out.println("=============");
for (int i = 0; i < 2; i++) {
    lambdaObjPrinter.printConsumer(o -> o.getClass());
}

會發現有如下輸出:

class lambda.LambdaExpression$$Lambda$1/2003749087
interface java.util.function.Consumer
lambda.LambdaExpression$$Lambda$1/2003749087@41629346
class lambda.LambdaExpression$$Lambda$1/2003749087
interface java.util.function.Consumer
lambda.LambdaExpression$$Lambda$1/2003749087@41629346
=============
class lambda.LambdaExpression$$Lambda$2/1078694789
interface java.util.function.Consumer
lambda.LambdaExpression$$Lambda$2/1078694789@6d311334
class lambda.LambdaExpression$$Lambda$2/1078694789
interface java.util.function.Consumer
lambda.LambdaExpression$$Lambda$2/1078694789@6d311334
  • 說明在不同for迴圈中(while等迴圈結果相同),只會動態生成1個實現類。

檢視編譯後的.class檔案如下:

LambdaExpression lambdaObjPrinter = new LambdaExpression();

int i;
for(i = 0; i < 2; ++i) {
    lambdaObjPrinter.printConsumer((o) -> {
        o.getClass();
    });
}

System.out.println("=============");

for(i = 0; i < 2; ++i) {
    lambdaObjPrinter.printConsumer((o) -> {
        o.getClass();
    });
}
  • 說明這不是編譯器編譯的結果,應該是JVM執行時對迴圈語句中lambda表示式的最佳化。

2 基本語法

lambda表示式本質上是對函式式介面的匿名實現類例項的一種簡化寫法:方法格式和lambda表示式格式一一對應。

對於執行邏輯而言,方法主要由兩部分組成(沒有返回值):形參和方法體。

lambda表示式與之對應:
1、形參:(t1, t2[, ……]),對應方法的形參(T1 t1, T2 t2[, ……])
2、箭頭:->,固定
3、方法體:{},對應方法的方法體

2.1 分類

根據方法形參和返回值的不同組合,lambda表示式可以分成以下幾類:

  1. 沒有形參:
() -> {
	// 方法體
}
  1. 一個形參:
(t) -> {
	// 方法體
}
  1. 多個形參:

    (t1, t2[, ……]) -> {
    // 方法體
    }

  2. 沒有返回值:

() -> {
	// 方法體
}
  1. 有返回值:
() -> {
	// 方法體
	return something;
}

根據形參個數的不同,形參部分可以有不同的寫法:
1、沒有形參或多個形參(超過1個),需要帶()

() -> {
	// 方法體
}
(t1, t2[, ……]) {
	// 方法體
}

2、一個形參,可以省略()

(t) -> {
	// 方法體
}
t -> {
	// 方法體
}

根據方法體中程式碼行數的不同,方法體部分也有不同的寫法:
1、一行程式碼,可以省略{}(此時該行程式碼的return;也必須省略):

() -> {
	System.out.println("Hello World!");
}
() -> System.out.println("Hello World!")
() -> {
	return "Hello World!"
}
() -> "Hello World!"

2、多行程式碼,不可以省略{}

() -> {
	System.out.println("Hello");
	System.out.println("World!");
}
() -> {
	System.out.println("Hello");
	return "Hello World!"
}

2.2 案例

  • 定義函式式介面,模擬不同型別的lambda表示式:
public class FunctionInterface {
    interface AcceptEmpty {
        void accept();
    }

    interface AcceptOne<T> {
        void accept(T t);
    }

    interface AcceptMore<T, E> {
        void accept(T t, E e);
    }

    interface ReturnVoid {
        void returnVoid();
    }

    interface ReturnR<R> {
        R returnR();
    }
}
  • 定義呼叫類,接收不同的lambda表示式:
/**
* 呼叫函式式介面的服務類
* @param <T> 第一個形參型別
* @param <E> 第二個形參型別
* @param <R> 返回值型別
*/
public class Service<T, E, R> {
    private T t;
    private E e;

    public Service(T t, E e) {
        this.t = t;
        this.e = e;
    }

    void acceptEmpty(FunctionInterface.AcceptEmpty acceptEmpty) {
        acceptEmpty.accept();
    }

    void acceptOne(FunctionInterface.AcceptOne<T> acceptOne) {
        acceptOne.accept(this.t);
    }

    void acceptMore(FunctionInterface.AcceptMore<T, E> acceptMore) {
        acceptMore.accept(this.t, this.e);
    }

    void returnVoid(FunctionInterface.ReturnVoid returnVoid) {
        returnVoid.returnVoid();
    }

    R returnR(FunctionInterface.ReturnR<R> returnR) {
        return returnR.returnR();
    }
}
  • 建立服務類例項:
Service<Integer, Integer, String> service = new Service<>(1, 2);

1、沒有形參

service.acceptEmpty(new FunctionInterface.AcceptEmpty() {
    @Override
    public void accept() {
        System.out.println("沒有形參");
    }
});
service.acceptEmpty(() -> {
    System.out.println("沒有形參");
});
service.acceptEmpty(() -> System.out.println("沒有形參"));

2、一個形參

service.acceptOne(new FunctionInterface.AcceptOne<Integer>() {
    @Override
    public void accept(Integer t) {
        System.out.println(t);
    }
});
service.acceptOne((t) -> System.out.println(t));
service.acceptOne(t -> System.out.println(t));

3、多個形參

service.acceptMore(new FunctionInterface.AcceptMore<Integer, Integer>() {
    @Override
    public void accept(Integer t, Integer e) {
        System.out.println(t);
        System.out.println(e);
    }
});
service.acceptMore((t, e) -> {
    System.out.println(t);
    System.out.println(e);
});

4、沒有返回值

service.returnVoid(new FunctionInterface.ReturnVoid() {
    @Override
    public void returnVoid() {
        System.out.println("沒有返回值");
    }
});
service.returnVoid(() -> System.out.println("沒有返回值"));

5、有返回值

service.returnR(new FunctionInterface.ReturnR<String>() {
    @Override
    public String returnR() {
        return "3";
    }
});
service.returnR(() -> "3");

3 執行邏輯

  • lambda表示式本質上是傳遞了一個動態生成的匿名物件,是一種假的函數語言程式設計。

lambda表示式形式上看起來很像是函數語言程式設計:將一個函式當作形參傳給方法。

實際上,lambda表示式只是Java的一個語法糖,它本質上仍然是一個普通的Java物件。

在執行的過程中,lambda表示式最終還是會被解析成匿名的介面實現類物件。

由於多型特性,在執行過程中,呼叫是外部傳進來的實現類例項的程式碼。

在這個過程中,我們甚至可以將該匿名物件儲存起來,便於後續多次呼叫。

  • 定義一個函式式介面:
public interface Lambda<T, R> {
    R method(T t);
}
  • 定義呼叫類:
public class FakeFunctionalProgramming<T, R> {
    private T t;
    private Lambda<T, R> lambda;

    public void setT(T t) {
        this.t = t;
    }

    public void setLambda(Lambda<T, R> lambda) {
        this.lambda = lambda;
    }

    public void doSomeThing() {
        T t = before();
        R r = lambda.method(t);
        after(r);
    }

    public T before() {
        return t;
    }
    public void after(R r) {
        System.out.println(r);
    }
}
  • 執行以下程式碼:
FakeFunctionalProgramming<String, String> ffp = new FakeFunctionalProgramming<>();
ffp.setT("Xianhuii");
ffp.setLambda((t) -> "Hello " + t + "!");
ffp.doSomeThing();  // Hello Xianhuii!

從上述結果可以看出,lambda表示式的程式設計方式本質上是利用了多型的特性,同時又使用了模板方法模式:

  • 呼叫處接收一個介面例項Lambda<T, R>作為形參。
  • 執行before()方法,處理相對固定的前處理邏輯。
  • 將執行過程中相關值作為形參傳給Lambda<T, R>例項,進行特定處理。
  • 接收Lambda<T, R>特定處理後的返回值。
  • 執行after()方法,處理相對固定的後處理邏輯。

此時,我們應該能夠透徹理解lambda表示式中形參的來源返回值的去向了。

藉助Java多型特性,以及JVM動態生成匿名實現類例項的功能,lambda表示式才表現得那麼像是函數語言程式設計。

相關文章