Java8中的Lambda表示式

湯圓學Java發表於2021-04-16

作者:湯圓

個人部落格:javalover.cc

前言

大家好啊,我是湯圓,今天給大家帶來的是《Java8中的Lambda表示式》,希望對大家有幫助,謝謝

文章純屬原創,個人總結難免有差錯,如果有,麻煩在評論區回覆或後臺私信,謝啦

簡介

Lambda表示式是一個可傳遞的程式碼塊,可以在以後執行一次或多次;

下面貼個對比程式碼:

// Java8之前:舊的寫法
Runnable runnable = new Runnable() {
  @Override
  public void run() {
    System.out.println("old run");
  }
};
Thread t = new Thread(runnable);

// Java8之後:新的寫法
Runnable runnable1 = ()->{
  System.out.println("lambda run");
};
Thread t1 = new Thread(runnable1);

可以看到,有了lambda,程式碼變得簡潔多了

你可以把lambda當作一個語法糖

下面讓我們一起來探索lambda的美好世界吧

目錄

下面列出本文的目錄

  • lambda的語法
  • 為啥引入lambda
  • 什麼是函式式介面
  • 什麼是行為引數化
  • 手寫一個函式式介面
  • 常用的函式式介面
  • 什麼是方法引用
  • 什麼是構造引用
  • lambda的組合操作

正文

1. lambda的語法

lambda語法

下面分別說下語法中的三個組成部分

  • 引數: ( Dog dog )
    • 引數型別可省略(當編譯器可以自動推導時),比如Comparator<String> comparatorTest = (a, b)->a.length()-b.length();,可以推匯出a,b都為String
    • 當引數型別可省略,且只有一個引數時,括弧也可以省略(但是個人習慣保留)
  • 符號: ->
  • 主體:{ System.out.println("javalover"); }
    • 如果是一條語句,則需要加大括號和分號{;}(比如上圖所示)
    • 如果是一個表示式,則直接寫,啥也不加(比如a.length()- b.length()

2. 為啥引入lambda

為了簡化程式碼

因為Java是面嚮物件語言,所以在lambda出現之前,我們需要先構造一個物件,然後在物件的方法中實現具體的內容,再把構造的物件傳遞給某個物件或方法

但是有了lambda以後,我們可以直接將程式碼塊傳遞給物件或方法

現在再回頭看下開頭的例子

lambda減少了模板程式碼

可以看到,用了lambda表示式後,少了很多模板程式碼,只剩下一個程式碼塊(最核心的部分)

3. 什麼是函式式介面

就是隻定義了一個抽象方法的介面

  • 正例:有多個預設方法,但是如果只有一個抽象方法,那它就是函式式介面,示例程式碼如下
@FunctionalInterface
public interface FunctionInterfaceDemo {
    void abstractFun();
    default void fun1(){
        System.out.println("fun1");    
    }
    default void fun2(){
        System.out.println("fun2");
    }   
}

這裡的註解@FunctionalInterface可以省略,但是建議加上,就是為了告訴編譯器,這是一個函式式介面,此時如果該介面有多個抽象方法,那麼編譯器就會報錯

  • 反例:比如A extends B,A和B各有一個抽象方法,那麼A就不是函式式介面,示例程式碼如下
// 編譯器會報錯,Multiple non-overriding abstract methods found in XXX
@FunctionalInterface
public interface NoFunctionInterfaceDemo extends FunctionInterfaceDemo{
  void abstractFun2();
}

上面的父介面FunctionInterfaceDemo中已經有了一個抽象方法,此時NoFunctionInterfaceDemo又定義了一個抽象方法,結果編譯器就提示了:存在多個抽象方法

在Java8之前,其實我們已經接觸過函式式介面

比如Runnable 和 Comparable

只是沒有註解@FunctionalInterface。

那這個函式式介面要怎麼用呢?

配合lambda食用,效果最佳(就是把lambda傳遞給函式式介面),示例程式碼如下:

new Thread(() -> System.out.println("run")).start();

其中用到的函式式介面是Runnable

4. 什麼是行為引數化

就是把行為定義成引數,行為就是函式式介面

類似泛型中的型別引數化<T>,型別引數化是把型別定義成引數

行為引數化,通俗點來說:

  • 就是用函式式介面形參
  • 然後傳入介面的各種實現內容(即lambda表示式)作為實參
  • 最後在lambda內實現各種行為(好像又回到多型的那一節了?這也是為啥多型是Java的三大特性的原因之一,應用太廣泛了)

這樣來看的話,行為引數化和設計模式中的策略模式有點像了(後面章節會分別講常用的幾種設計模式)

下面我們手寫一個函式式介面來加深理解吧

5. 手寫一個函式式介面

下面我們循序漸進,先從簡單的需求開始

  • 第一步:比如我們想要讀取某個檔案,那可以有如下方法:
public static String processFile() throws IOException {
    // Java7新增的語法,try(){},可自動關閉資源,減少了程式碼的臃腫
    try( BufferedReader bufferedReader = 
        new BufferedReader(new  FileReader("D:\\JavaProject\\JavaBasicDemo\\test.txt"))){
        return bufferedReader.readLine();
    }
}

可以看到,核心的行為動作就是 return bufferedReader.readLine();,表示讀取第一行的資料並返回

那如果我們想要讀取兩行呢?三行?

  • 第二步:這時就需要用到上面的函式式介面了,下面就是我們自己編寫的函式式介面
@FunctionalInterface
interface FileReadInterface{
  	// 這裡接受一個BufferedReader物件,返回一個String物件
    String process(BufferedReader reader) throws IOException;
}

可以看到,只有一個抽象方法process() ,它就是用來處理第一步中的核心動作(讀取檔案內容)

至於想讀取多少內容,那就需要我們在lambda表示式中定義了

  • 第三步:接下來我們定義多個lambda表示式,用來傳遞函式式介面,其中每個lambda表示式就代表了一種不同的行為,程式碼如下:
// 讀取一行
FileReadInterface fileReadInterface = reader -> reader.readLine();
// 讀取兩行
FileReadInterface fileReadInterface2 = reader -> reader.readLine() + reader.readLine();

  • 第四步:我們需要修改第一步的processFile(),讓其接受一個函式式介面,並呼叫其中的抽象方法,程式碼如下:
// 引數為第二步我們自己手寫的函式式介面
public static String processFile(FileReadInterface fileReadInterface) throws IOException {
        try( BufferedReader bufferedReader =
                 new BufferedReader(new FileReader("./test.txt"))){
					// 這裡我們不再自己定義行為,而是交給函式式介面的抽象方法來處理,然後通過lambda表示式的傳入來實現多個行為
          return fileReadInterface.process(bufferedReader);
        }
    }
  • 第五步:拼接後,完整程式碼如下:
public class FileReaderDemo {
    public static void main(String[] args) throws IOException {
				// 第三步: 
      	// lambda表示式1 傳給 函式式介面:只讀取一行
      	FileReadInterface fileReadInterface = reader -> reader.readLine();
				// lambda表示式2 傳給 函式式介面:只讀取兩行
      	FileReadInterface fileReadInterface2 = reader -> reader.readLine() + reader.readLine();
      	// 最後一步: 不同的函式式介面的實現,表現出不同的行為
        String str1 = processFile(fileReadInterface);
        String str2 = processFile(fileReadInterface2);
        System.out.println(str1);
        System.out.println(str2);
    }
  	// 第四步: 讀取檔案方法,接受函式式介面作為引數
    public static String processFile(FileReadInterface fileReadInterface) throws IOException {
        try( BufferedReader bufferedReader =
                 new BufferedReader(new FileReader("./test.txt"))){
					// 呼叫函式式介面中的抽象方法來處理資料					
          return fileReadInterface.process(bufferedReader);
        }
    }
	// 第一步:
  public static String processFile() throws IOException {
        try( BufferedReader bufferedReader =
                 new BufferedReader(new FileReader("./test.txt"))){
          return bufferReader.readLine();
        }
    }


}

// 第二步: 我們手寫的函式式介面
@FunctionalInterface
interface FileReadInterface{
    String process(BufferedReader reader) throws IOException;
}

其實你會發現,我們手寫的這個函式式介面,其實就是Function<T>去除泛型化後的介面,如下所示:

@FunctionalInterface
public interface Function<T, R> {
	// 都是接受一個引數,返回另一個引數
  R apply(T t);
}

下面我們列出Java中常用的一些函式式介面,你會發現自帶的已經夠用了,基本不會需要我們自己去寫

這裡的手寫只是為了自己實現一遍,可以加深理解程度

6. 常用的函式式介面

常用的函式式介面

7. 什麼是方法引用

我們先看一個例子

前面我們寫的lambda表示式,其實還可以簡化,比如

// 簡化前
Function<Cat, Integer> function = c->c.getAge();
// 簡化後
Function<Cat, Integer> function2 = Cat::getAge;

其中簡化後的Cat::getAge,我們就叫做方法引用

方法引用就是引用類或物件的方法

下面我們列出方法引用的三種情況:

  1. Object::instanceMethod(物件的例項方法)
  2. Class::staticMethod(類的靜態方法)
  3. Class::instanceMethod(類的例項方法)

像我們上面舉的例子就是第三種:類的例項方法

下面我們用程式碼演示上面的三種方法:

public class ReferenceDemo {
    public static void main(String[] args) {
        // 第一種:引用物件的例項方法
        Cat cat = new Cat(1);
        Function<Cat, Integer> methodRef1 = cat::getSum; 
        // 第二種:引用類的靜態方法
        Supplier<Integer> methodRef2 = Cat::getAverageAge;
        // 第三種:引用類的例項方法
        Function<Cat, Integer> methodRef3 = Cat::getAge;
    }
}
class Cat {
    int age;

    public Cat(int age) {
        this.age = age;
    }

    // 獲取貓的平均年齡
    public static int getAverageAge(){
        return 15;
    }
    // 獲取兩隻貓的年齡總和
    public int getSum(Cat cat){
        return cat.getAge() + this.getAge();
    }

    public int getAge() {
        return age;
    }    public void setAge(int age) {
        this.age = age;
    }
}

為啥要用這個方法引用呢?

方法引用好比lambda表示式的語法糖,語法更加簡潔,清晰

一看就知道是呼叫哪個類或物件的哪個方法

8. 什麼是構造引用

上面介紹了方法引用,就是直接引用某個方法

這裡的構造引用同理可得,就是引用某個類的構造方法

構造引用的表示式為:Class::new,僅此一種

如果你有多個建構函式,那編譯器會自己進行推斷引數(你看看,多好,多簡潔)

比如下面的程式碼:

// 這裡呼叫 new Cat()
Supplier<Cat> constructRef1 = Cat::new;
// 這裡呼叫 new Cat(Integer)
Function<Integer, Cat> constructRef2 = Cat::new;

9. lambda表示式中引入外部變數的限制

要求引入lambda表示式中的變數,必須是最終變數,即該變數不會再被修改

比如下面的程式碼:

public static void main(String[] args) {
  String str = "javalover.cc";
  Runnable runnable = ()->{
    str = "1";// 這裡會報錯,因為修改了str引用的指向
    System.out.println(str);
  }
}

可以看到,lambda表示式引用了外面的str引用,但是又在表示式內部做了修改,結果就報錯了

為啥要有這個限制呢?

為了執行緒安全,因為lambda表示式有一個好處就是隻在需要的時候才會執行,而不是呼叫後立馬執行

這樣就會存在多個執行緒同時執行的併發問題

所以Java就從根源上解決:不讓變數被修改,都是隻讀的

那你可能好奇,我不把str的修改程式碼放到表示式內部可以嗎?

也不行,道理是一樣的,只要lambda有用到這個變數,那這個變數不管是在哪裡被修改,都是不允許的

不然的話,我這邊先執行了一次lambda表示式,結果你就改了變數值,那我第二次執行lambda,不就亂了嗎

10. lambda的組合操作

最後是lambda的必殺技:組合操作

在這裡叫組合或者複合都可以

概述:組合操作就是先用一個lambda表示式,然後再在後面組合另一個lambda表示式,然後再在後面組合另另一個lambda表示式,然後。。。有點像是鏈式操作

學過JS的都知道Promise,裡面的鏈式操作就和這裡的組合操作很像

用過Lombok的朋友,應該很熟悉@Builder註解,其實就是構造者模式

下面我們用程式碼演示下組合操作:

// 重點程式碼
public class ComposeDemo {
    public static void main(String[] args) {
        List<Dog> list = Arrays.asList(new Dog(1,2), new Dog(1, 1));
        // 1. 先按年齡排序(預設遞增)
      	// Dog::getAge, 上面介紹的方法引用
      	// comparingInt, 是Comparator的一個靜態方法,返回Comparator<T>
      	Comparator<Dog> comparableAge = Comparator.comparingInt(Dog::getAge);
        // 2. 如果有相同的年齡,則年齡相同的再按體重排序(如果年齡已經比較出大小,則下面的體重就不會再去比較)
        Comparator<Dog> comparableWeight = Comparator.comparingInt(Dog::getWeight);;
        // 3. 呼叫list物件的sort方法排序,引數是Comparator<? super Dog>
        list.sort(comparableAge.thenComparing(comparableWeight));
        System.out.println(list);
    }
}
// 非重點程式碼
class Dog{
    private int age;
    private int weight;

    public Dog(int age, int weight) {
        this.age = age;
        this.weight = weight;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "age=" + age +
                ", weight=" + weight +
                '}';
    }
}

輸出:[Dog{age=1, weight=1}, Dog{age=1, weight=2}]

比較的流程如下所示:

組合操作

總結

  1. lambda的語法: 引數+符合+表示式或語句,比如(a,b)->{System.out.println("javalover.cc");}

  2. 函式式介面:只有一個抽象方法,最好加@FunctionalInterface,這樣編譯器可及時發現錯誤,javadoc也說明這是一個函式式介面(可讀性)

  3. 行為引數化:就是函式式介面作為引數,然後再將lambda表示式傳給函式式介面,通過不同的lambda內容實現不同的行為

  4. 方法引用:lambda的語法糖,總共有三種:

    • Object::instanceMethod(物件的例項方法)

    • Class::staticMethod(類的靜態方法)

    • Class::instanceMethod(類的例項方法)

  5. 構造引用:就一種,編譯器自己可判斷是哪個建構函式,語法為Class::new

  6. 在lambda中引入外部變數,必須保證這個變數是最終變數,即不再被修改

  7. lambda的組合操作,就是鏈式操作,組合是通過函式式介面的靜態方法來組合(靜態方法會返回另一個函式式介面的物件)

比如list.sort(comparableAge.thenComparing(comparableWeight));

後記

最後,感謝大家的觀看,謝謝

相關文章