java8 java Lambda functional

Java 8 系列 - 行為參數化與 Lambda 表達式

曾克維 2017/12/29 15:15:49
5447

Java 8 系列 - 行為參數化與 Lambda 表達式


簡介

學習如何運用 Java 8 新特性,將行為邏輯參數化,並藉由 Lambda 表達式將繁瑣的程式碼簡化,增加可讀性。

作者

曾克維


為何要學習 Java 8 的新特性?

  1. 現有的 Java 實作並不能很好地利用多核處理器
  2. 函數式編寫使程式碼更簡潔易讀
  3. Java8 中 Stream 的概念,讓程式碼更簡潔易讀,並支援並行處理 stream 元素
  4. 可以在介面中使用默認方法,在實作類別沒有實作方法時提供預設的方法內容
  5. 其他來自 function style 的概念,包括處理 null 和使用模式匹配

透過行為參數化傳遞程式碼

  在軟體工程中,一個眾所皆知的問題是,用戶的需求肯定會變。

行為參數化就是可以幫助處理頻繁變更的需求的一種軟體開發模式。
你可能已經用過的行為參數化模式,使用 Java API 中現有的類別和介面:

  • 對 List 做排序
  • 告訴一個 Thread 需要被執行的程式
  • 處理 GUI 事件

在 Java 8 之前使用這種模式很囉唆,但 Java 8 的 Lambda 解決了這個問題。

應對不斷變化的需求

1. 篩選綠蘋果

第一個解決方案:

if判斷式內就是篩選蘋果的條件。但現在農民想要篩選紅蘋果,該怎麼做?
簡單作法如下:

  1. 複製這個方法
  2. 把方法名改成 filterRedApples
  3. 更改if條件來匹配紅蘋果

但若是農民想要篩選更多的顏色,這種方法就應付不了。一個良好的原則就是將篩選條件抽象化。

2. 把顏色作為參數

之後只需要這樣呼叫方法:
現在農民又想要篩選重量,於是寫了如下方法:
我們會發現重複了大部分的程式碼…

行為參數化

一種可能的解決方案是對你的選擇標準建模:我們考慮的是蘋果,需要根據 Apple 的某些屬性(顏色 or 重量)來回傳一個 boolean 值。它稱為 Predicate (即一個回傳 boolean 值的函數)。
定義一個介面來對 選擇標準 建模:
  現在就可以利用 ApplePredicate 來實作不同的選擇標準了,如下:
 

3. 根據抽象條件篩選

利用ApplePredicate改過之後,filter方法如下:

  到目前為止,我們已經做到了 行為參數化 ,這樣的好處在於可以重複使用同一個方法,給它不同的行爲來達到不同的目的,讓程式適應需求的變化,但是這個過程很囉唆,因為需要定義很多只要實例化一次的類別。
 

4. 使用匿名類別

但匿名類別還是不夠好。
第一,它往往很笨重,因為它佔用很多空間來撰寫模板程式碼。
第二,它不容易閱讀。

5. 使用Lambda表達式

  使用Lambda表達式後,看起來更簡潔、容易閱讀。

Lambda表達式

Lambda 簡介

  可以把  Lambda  理解為簡潔地表示可傳遞的匿名函數的一種方式:它沒有名稱,但它有參數列表、函數主體、回傳類型。
原來寫法:
用了 Lambda 表示式:
  • 參數列表:要帶入 Lambda 主體的參數
  • 箭頭:把參數列表與 Lambda 主體分隔開
  • Lambda 主體:主體就是 Lambda 的回傳值了

範例:

Lambda 的基本語法:

  • (parameters) -> expression
  • (parameters) -> { statements; }

在哪裡及如何使用 Lambda

1. 函數式介面

Predicate<T> 就是一個函數式介面,因為 Predicate 只有定義了一個抽象方法:

函數式介面就是只有定義了一個「抽象」方法的介面

Java API 中的一些函數式介面:

  範例:

2. 函數描述符

函數式介面的抽象方法定義了 Lambda 表示式應該帶入的參數,以及回傳值,這樣的抽象方法稱作函數描述符

例如:Runnable介面可以看做一個什麼參數都不用傳也沒有回傳值(void)的函數定義

@FunctionalInterface是什麼?
新的 Java API,函數式介面帶有@FunctionalInterface的Annotation。
這個Annotation表示這個介面會設計成一個函數式介面。
如果用@FunctionalInterface定義了一個介面,但它實際卻不是函數式介面的話,編譯時會出錯。

 

實作 Lambda:環繞執行模式

資源處理(例如:處理檔案、資料庫)是一個常見的模式,就是打開一個資源,做一些處理,然後關閉資源。這個模式初始準備、清理階段總是很類似,並且會圍繞著執行處理的那些重要的程式碼。這就是所謂的環繞執行(execute around)模式,如下:

第一步:記得行為參數化

需要一種方法把行為傳遞給 processFile,以便他可以利用 BufferedReader 執行不同的行為。

因此需要一個接收 BufferedReader 並回傳 String 的 Lambda,看起來會像是:

第二步:使用函數式介面來傳遞行為

Lambda 只能用在宣告為函數式介面的情況。需要創建一個能符合BufferedReader -> String,還能拋出IOException例外的介面:

現在可以把這個介面作為新的 processFile 方法的參數了:

第三步:執行一個行為

任何BufferedReader -> String形式的 Lambda 都可以作為參數來傳遞,因為它們符合 BufferedReaderProcessor 介面中定義的 process 方法。

Lambda 表達式允許你直接為函數式介面的抽象方法提供實作,並且將整個表達式作為函數式介面的一個實例

第四步:傳遞 Lambda

現在你可以透過傳遞不同的 Lambda 重用 processFile 方法,並以不同的方式處理檔案了。

處理一行:

處理兩行:

使用函數式介面

為了應用不同的 Lambda 表達式,需要一套能夠描述常見函數描述符的函數式介面。

Java API 中已經有了幾個函數式介面,例如ComparableRunnableCallable

Java 8 在 java.util.function 中導入了幾個新的函數式介面。

1. Predicate

java.util.function.Predicate<T>介面定義了一個叫 test 的抽象方法,他接受泛型 T 對象,並回傳一個 boolean。

需要表示一個涉及類型 T 的布林表達式時,就可以使用這個介面。

2. Consumer

java.util.function.Consumer<T>介面定義了一個叫 accept 的抽象方法,他接受泛型 T 對象,沒有回傳值(void)。

需要對某個類型 T 的物件執行某些操作,就可以使用這個介面。

3. Function

java.util.function.Function<T, R>介面定義了一個叫 apply 的方法,他接受一個泛型 T 的物件,並回傳一個泛型 R 的物件。

如果需要將輸入物件的訊息反映在輸出,就可以使用這個介面。

4. 基本型別(Primitive Types)特化

Java 型別不是參照型別(Reference Type),就是基本型別(Primitive Type)。

但是泛型(例如:Consumer<T>中的 T)只能綁定到參照型別。

因此 Java 裡有一個將基本型別轉換為對應的參照型別的機制叫做裝箱(boxing)。相反的操作就叫拆箱(unboxing)。

Java 裝箱和拆箱是自動完成的,因此下面的程式碼是有效的(一個 int 被裝箱成為 Integer):

但這在性能方面是要付出成本的。裝箱後的值需要更多的記憶體空間,並且需要額外的記憶體搜索來取得被包裹的原始值。

Java 8 為函數式介面帶來了一個專門的版本,以便在輸入和輸出都是基本型別時避免自動裝箱的操作。

型別檢查、型別推斷以及限制

1. 型別檢查

Lambda 的型別是從使用 Lambda 的上下文推斷出來的。上下文(例如:接受它傳遞的方法的參數,或接受它的值的區域變數)中 Lambda 表達式需要的型別稱為目標型別

filter(inventory, (Apple a) -> a.getWeight() > 150);

  1. 先看看 filter 的定義。

filter(List<Apple> inventory, Predicate<Apple> p)

  1. 目標型別是 Predicate<Apple>

  2. 再看看 Predicate<Apple> 介面的抽象方法是什麼?

boolean test(Apple apple)

  1. 它是 test,接受一個 Apple,並回傳一個 boolean

Apple -> boolean

  1. 函數描述符 Apple -> boolean 匹配 Lambda 的宣告。它接受一個 Apple,回傳一個 boolean,因此程式碼型別檢查無誤

2. 型別推斷

方法引用

方法引用讓你可以重複使用現有的方法定義,並像 Lambda 一樣傳遞

方法引用就是讓你根據已有的方法實作來創建 Lambda 表達式,明白的指明方法名稱,程式碼的可讀性會更好。

曾克維