Java 8 系列 - 行為參數化與 Lambda 表達式
Java 8 系列 - 行為參數化與 Lambda 表達式
簡介 |
學習如何運用 Java 8 新特性,將行為邏輯參數化,並藉由 Lambda 表達式將繁瑣的程式碼簡化,增加可讀性。 |
作者 |
曾克維 |
為何要學習 Java 8 的新特性?
- 現有的 Java 實作並不能很好地利用多核處理器
- 函數式編寫使程式碼更簡潔易讀
- Java8 中 Stream 的概念,讓程式碼更簡潔易讀,並支援並行處理 stream 元素
- 可以在介面中使用默認方法,在實作類別沒有實作方法時提供預設的方法內容
- 其他來自 function style 的概念,包括處理 null 和使用模式匹配
透過行為參數化傳遞程式碼
行為參數化就是可以幫助處理頻繁變更的需求的一種軟體開發模式。
你可能已經用過的行為參數化模式,使用 Java API 中現有的類別和介面:
- 對 List 做排序
- 告訴一個 Thread 需要被執行的程式
- 處理 GUI 事件
在 Java 8 之前使用這種模式很囉唆,但 Java 8 的 Lambda 解決了這個問題。
應對不斷變化的需求
1. 篩選綠蘋果
第一個解決方案:
if
判斷式內就是篩選蘋果的條件。但現在農民想要篩選紅蘋果,該怎麼做?
簡單作法如下:
- 複製這個方法
- 把方法名改成 filterRedApples
- 更改
if
條件來匹配紅蘋果
但若是農民想要篩選更多的顏色,這種方法就應付不了。一個良好的原則就是將篩選條件抽象化。
2. 把顏色作為參數
行為參數化
Predicate
(即一個回傳 boolean 值的函數)。
定義一個介面來對 選擇標準 建模:
ApplePredicate
來實作不同的選擇標準了,如下:
3. 根據抽象條件篩選
利用ApplePredicate
改過之後,filter
方法如下:
4. 使用匿名類別
但匿名類別還是不夠好。
第一,它往往很笨重,因為它佔用很多空間來撰寫模板程式碼。
第二,它不容易閱讀。
5. 使用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:環繞執行模式
第一步:記得行為參數化
需要一種方法把行為傳遞給 processFile,以便他可以利用 BufferedReader 執行不同的行為。
因此需要一個接收 BufferedReader 並回傳 String 的 Lambda,看起來會像是:
第二步:使用函數式介面來傳遞行為
Lambda 只能用在宣告為函數式介面的情況。需要創建一個能符合BufferedReader -> String
,還能拋出IOException
例外的介面:
第三步:執行一個行為
任何BufferedReader -> String
形式的 Lambda 都可以作為參數來傳遞,因為它們符合 BufferedReaderProcessor 介面中定義的 process 方法。
Lambda 表達式允許你直接為函數式介面的抽象方法提供實作,並且將整個表達式作為函數式介面的一個實例
第四步:傳遞 Lambda
現在你可以透過傳遞不同的 Lambda 重用 processFile 方法,並以不同的方式處理檔案了。
處理一行:
使用函數式介面
為了應用不同的 Lambda 表達式,需要一套能夠描述常見函數描述符的函數式介面。
Java API 中已經有了幾個函數式介面,例如Comparable
、Runnable
、Callable
。
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);
- 先看看 filter 的定義。
filter(List<Apple> inventory, Predicate<Apple> p)
-
目標型別是 Predicate<Apple>
-
再看看 Predicate<Apple> 介面的抽象方法是什麼?
boolean test(Apple apple)
- 它是 test,接受一個 Apple,並回傳一個 boolean
Apple -> boolean
- 函數描述符 Apple -> boolean 匹配 Lambda 的宣告。它接受一個 Apple,回傳一個 boolean,因此程式碼型別檢查無誤
2. 型別推斷
方法引用
方法引用就是讓你根據已有的方法實作來創建 Lambda 表達式,明白的指明方法名稱,程式碼的可讀性會更好。