Java8中的Lambda表達式

做者:湯圓java

我的博客:javalover.cc設計模式

前言

你們好啊,我是湯圓,今天給你們帶來的是《Java8中的Lambda表達式》,但願對你們有幫助,謝謝安全

文章純屬原創,我的總結不免有差錯,若是有,麻煩在評論區回覆或後臺私信,謝啦

簡介

Lambda表達式是一個可傳遞的代碼塊,能夠在之後執行一次或屢次;併發

下面貼個對比代碼:app

// 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,代碼變得簡潔多了ide

你能夠把lambda看成一個語法糖函數

下面讓咱們一塊兒來探索lambda的美好世界吧ui

目錄

下面列出本文的目錄this

  • lambda的語法
  • 爲啥引入lambda
  • 什麼是函數式接口
  • 什麼是行爲參數化
  • 手寫一個函數式接口
  • 經常使用的函數式接口
  • 什麼是方法引用
  • 什麼是構造引用
  • lambda的組合操做

正文

1. lambda的語法

lambda語法

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

  • 參數: ( 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));

後記

最後,感謝你們的觀看,謝謝

相關文章
相關標籤/搜索