Java8 實戰學習 「Lambda 表達式」

Java8 實戰學習 — Lambda 表達式

上一章,我們學習了參數化代碼的實現方法,這個邏輯的推導對我自己來說還是蠻有意義的,因為這將對我以后的代碼編輯產生影響。

這一節我們繼續學習,我們將學習 Lambda 表達式的具體使用。

Lambda 概述:

可以把Lambda表達式理解為簡潔地表示可傳遞的匿名函數的一種方式:它沒有名稱,但它 有參數列表、函數主體、返回類型,可能還有一個可以拋出的異常列表。這個定義夠大的,讓我 們慢慢道來。

Lambda 的主要特點就是需要我們寫的東西很少,但是構建 Lambda 的過程是需要思考的,以前看到一個接口或者一個抽象類,我們第一反應是 new Function(){...} ,但是有了 Lambda 以后我們需要重新思考,以便于我們代碼更加便于閱讀和減少代碼的書寫。


Lambda 的標準形式:

書中通過構建一個 Comparator 對象來給我們舉例說明如果替換原來的匿名內部類寫法。 我們都知道實現一個 Comparator 的方法:

    Comparator<Apple> byWeight = new Comparator<Apple>() {
        public int compare(Apple a1, Apple a2) {
            return a1.getWeight().compareTo(a2.getWeight());
        }
    };

我們可以使用 Comparator 的方法為:

Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

這看起來并沒有難度,閱讀起來是 我有兩個蘋果對象,拿參數 a1,a2 作為比較對象,第一個蘋果的重量和第二個蘋果的重量比較,并返回比較結果(0代表相同,大于0的數代表a1重量大于a2重量,小于0則相反)。

請注意你基本上只傳遞了比較兩個蘋果重量所真 正需要的代碼。看起來就像是只傳遞了compare方法的主體。

書中給出了上述解釋,之后給出語法通用表示:

  1. (parameters) -> expression 即 (參數)-> 方法實現 適合單個語句
  2. (parameters) -> { statements; } (參數)-> {方法實現;} 適合多語句

注意:

  1. 使用第一種方式不可出現 ; 若方法實現即 Lambda 主體如果有多個語句,就必須使用{;}

  2. 書中 (String s) -> s.length() 這個例子的后邊說了一句話當時對我造成了不少困惑:

    第一個Lambda表達式具有一個String類型的參 數并返回一個 int 。 Lambda沒有 return 語句, 因為已經隱含了return

    這說明的意思說 如果是第一種表達方式或者第二種表達方式的返回void 都屬于 return 類型可以推敲的時候,可以不寫 return ,并不能說 Lambda 沒有 return。


哪里可以使用 Lambda

Lambda表達式允許你直接以內聯的形式為函數式接口的抽象方法提供實現,并把 整個表達式作為函數式接口的實例 (具體說來,是函數式接口一個具體實現 的實例)。你用匿名內部類也可以完成同樣的事情,只不過比較笨拙:需要提供一個實現,然后 再直接內聯將它實例化。

這里有兩個概念:

  1. 函數式接口:函數式接口就是只定義一個抽象方法的接口
  2. Lambda 是函數式接口的抽象方法的實現,但它可以作為整個接口的一個具體實例。

厲害了我的哥,第二點難理解,但這又恰恰是讀懂 Lambda 表達式的必須需要理解的地方。

舉一個栗子:

// 需要調用函數式接口的方法
public static void process(Runnable r) {
    r.run();
}

// 之前的調用方法
Runnable r1 = new Runnable() {
   public void run() {
       System.out.println("Hello World 2");
   }
};
process(r1);

// Lambda 表達方法
process(() -> System.out.println("Hello World 3"));

相信如果有之前實現的方法作比較,我們會很好明白,但是如果你擋住上邊的實現,去看下邊的 lambda 我相信好多人跟我一樣懵逼。這是什么鬼? 雖然我知道結果會是 Hello World 3 ,但是這究竟是什么?

思路是這樣的:

  1. process 方法只接受 Runnable 的具體實現類
  2. Lambda 可以作為函數式接口的一個實現實例
  3. () -> System.out.println("Hello World 3") 應該就是 Runnable 的一個具體實現。
  4. Lambda 是函數式接口的抽象方法的實現,也就是這句話同時也是 run() 方法的具體實現,run方法不需要參數,所以箭頭前邊為一個空的 ()。() -> void代表 了參數列表為空,且返回void的函數。

這充分說明 Lambda 寫得少想得多的特點。但是我們也發現了他的一個優點就是「簡單易讀」,我們看到這句話第一個反應就是:它將打印出 Hello World 3 。


方法簽名

Java 方法簽名 :java 的方法簽名由 全類名.方法名(形參數據類型列表)返回值數據類型 決定,在方法存在重載的時候,方法返回值沒有什么意義,是由方法名和參數列表決定的。

Lambda 表達式的簽名:函數式接口的抽象方法的簽名基本上就是Lambda表達式的簽名。

書中舉了個錯誤的例子:ApplePredicate<Apple> p = (Apple a) -> a.getWeight();
因為之前我們定義的時候:

public interface ApplePredicate{ boolean test (Apple apple); }

我們可以看到,函數式接口 ApplePredicate test 的方法簽名中,返回值為 boolean 但是錯誤代碼中返回了int值,所以方法簽名不同。


動手實現一個 Lambda 付諸實踐

動手練習:將下列代買轉化為 Lambda 實現:

   public static String proccessFile() throws IOException {
        try (BufferedReader br = new BufferedReader((new FileReader("data.txt")))) {
            return br.readLine();
        }
    }
  1. 行為參數化

    實際操作中我們可能需要對文件進行不同的操作,比如讀取兩行,讀取最后一行。所以我們要將 proccessFile () 方法 對文件進行不同操作的行為,進行參數化。

    我們期待的結果是 通過 processFile 拿到文件的讀取結果,具體怎么讀取應該由接口的抽象方法具體實現,所以 processFile 的行為就是如何操作文件,我們需要將他參數化。

  2. 使用函數接口來傳遞行為

    為了之后我們能夠使用 Lambda 來實現這個功能,我們需要創建一個 函數式接口。對于初學者來說這點應該是最難得,我們應該如何創建這個函數式接口。

    1. 明確任務條件: 讀取文件需要一個文件的讀取流 這里假定是字符流 BufferReader
    2. 明確任務結果: 返回讀取的結果,應該是一個 String

    即 BufferReader -> String 的過程

```
interface BufferedReaderProcessor {
        String process(BufferedReader bufferedReader);
}
```
  1. 執行一個行為
    執行這個行為將需要用到 processFile 方法了,該方法參數為函數式接口 BufferedReaderProcessor ,用該接口的是實例來進行文件操作,具體怎么操作由該實例決定:

      public static String processFile(BufferedReaderProcessor p) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
            return p.process(br);
        }
    }
    
  1. 改為 Lambda 形式完成 process 的具體實現

    通過之前的學習,我們知道了一個重要的概念就是 Lambda 可以作為,Lambda 是函數式接口的抽象方法的實現,但它可以作為整個接口的一個具體實例。

    1. 需要實現這個接口的具體方法
    2. 整體作為實例傳遞給 processFile

    假設我們現在需要完成一讀取文章前兩行的操作:

    1. (parameters) -> expression

      (BufferReader br) -> br.readLine()+br.readLine();
      
    2. (parameters) -> { statements; }

       String readline = proccessFile((BufferedReader br) -> {
           //readLine 方法會有警告 ,之后會學習如何處理 lambda 中的警告 這里重點在于 lambda 的實現。
                      String line1 = br.readLine();
                      String line2 = br.readLine();
                      return line1 + line2;
                  });
      
      

類型檢查

Lambda 可以作為函數式接口生成一個實例。然而,Lambda 表達式本身并不包含它在實現哪個函數式接口的信息

正如我們文章開頭提到的那樣,Lambda 本身并沒有什么意義,只有結合上下文,更通俗的說是等號左邊的內容,才是我們最終想要的 函數式接口具體實現 對象。

Lambda的類型是從使用Lambda的上下文推斷出來的。

這里的上下文包括: 方法簽名(參數,返回值),以及使用這個 Lambda 的方法的參數類型。

List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);

這個例子還是延續第一篇中的那個篩選蘋果的例子,書中給出了很好的解讀這里就不多贅述直接上圖:

看到這里我只能說這本書翻譯的太好了。

依次類推我們看地方代碼的時候如果它使用了 Lambda 方式書寫代碼,我們可以通過以下方式來找到這個 Lambda 的含義:

  1. 找到使用 Lambda 函數式參數方法的方法定義。
  2. 查看Lambda 所需要實例化的抽象接口的內唯一的一個抽象方法的方法簽名(這里是 T -> boolen)
  3. 檢查 Lambda 表達式是否滿足這個抽象方法的方法簽名即可。

聰明的人可能發現了貓膩,我們可能好多的函數式接口的抽象方法的方法簽名都是 T -> boolen,那么 Lambda 的使用會不會出現問題 ?

事實上,同一個 Lambda 表達式,如果 Lambda 表達式的方法簽名 = 函數式接口的抽象方法接口 那么這個這個 Lambda 就是有效的。

這就需要在我們書寫或者閱讀的時候必須結合上下文,而 JVM 需要做的事情更多,但具體原理并不影響我們使用,所以就先不探討。


類型推斷

之前說過 Lambda 表達式還可以繼續簡單化代碼,剛才我們學習了類型檢查,相同的 Lambda 表達式可以匹配不同的函數接口,JVM 可以根據我們使用 Lambda 表達式的上下文來決定匹配什么樣的函數接口來接收此表達式。

如果這個 Lambda 的目標類型是可以推斷出來的,而參數類型也只有一種的時候,我們可以省略參數的類型:

List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor()));

Lambda表達式有多個參數,代碼可讀性的好處就更為明顯,以下兩個方式是等價的。

Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

書中也給了我們提醒,有的時候沒有類型推斷的 Lambda 表達式更易讀,有時候去掉看起來更好。
需要我們自己去選擇。


至此我們已經成功完成了一次lambda 的實踐過程。通過學習我的體會就是想要 lambda 的學習還是需要多加練習。否則總會處在放下課本就忘得狀態。至此我們學習完了,課本的3.3章的內容。而之后將會介紹一個新的概念叫「方法引用」,它將會使代碼更加簡潔,但是同時也帶來了更大的挑戰,一起來加油吧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容