f2 = x -> x * 2;
```
所以,Lambda 表示式本身是中性的,它本身無關乎函式介面的名稱,它只關心方法簽署,但忽略方法名稱。
函式介面是僅具單一抽象方法的介面,不過有時候會難以直接看出介面是否為函式介面。例如,介面可能有預設方法(JDK8 的新特性)、可能繼承其他介面、重新定義了某些方法等,這些都會使得確認介面是否為函式介面更為困難。有個新的標註 `@FunctionalInterface` 被引入,它可以這麼使用:
```java
@FunctionalInterface
public interface Func {
R apply(P p);
}
```
如果介面使用了 `@FunctinalInterface` 來標註,而本身並非函式介面的話,就會引發編譯錯誤。例如:
```java
@FunctionalInterface
public interface Function
{
R call(P p);
R call(P p1, P p2);
}
```
編譯器會對此介面產生以下編譯錯誤:
@FunctionalInterface
^
Function is not a functional interface
multiple non-overriding abstract methods found in interface Function
看來,Lambda 語法不過就是匿名類別的編譯器語法蜜糖嘛!真的嗎?來看一下接下來的程式,想想看結果會如何顯示?
```java
import static java.lang.System.out;
public class Hello {
Runnable r1 = new Runnable() {
public void run() {
out.println(this);
}
};
Runnable r2 = new Runnable() {
public void run() {
out.println(toString());
}
};
public String toString() { return "Hello, world!"; }
public static void main(String[] args) {
new Hello().r1.run();
new Hello().r2.run();
}
}
```
結果會顯示像是 Hello$1@103368e 與 Hello$2@1f2ae62,這是因為 `this` 以及 `toString` 代表的對象,實際上會來自匿名類別對應的實例。再來看看接下來的程式,它會顯示什麼?
```java
import static java.lang.System.out;
public class Hello {
Runnable r1 = () -> { out.println(this); };
Runnable r2 = () -> { out.println(toString()); };
public String toString() { return "Hello, world!"; }
public static void main(String[] args) {
new Hello().r1.run();
new Hello().r2.run();
}
}
```
結果會顯示兩次的 “Hello, world!",也就是說,Lambda 表示式本體中的 `this` 與 `toString` 實際參考對象,是來自當時包含它們的環境,也就是 `Hello` 實例。也注意到,先前定義的 `compose` 方法中,參數列上並不需要 `final` 關鍵字。
在 〈Java 的稻草人提案〉 中我們看過,如果要在匿名類別中使用外在的區域變數,Java 的編譯器會強制你在區塊變數加上 `final`,即使變數實際上於匿名類別中並不會做任何修改。JDK8 放寬了這個限制,如果變數本身等效於 `final` 區域變數,也就是說,如果變數不會在 Lambda 表示式中有重新指定的動作,就可以不用加上 `final` 關鍵字。
不過,我們可以在 Lambda 表示式中改變被捕捉的變數值嗎?像是在 JavaScript 或 Scala 中可以做到的事情?因為可重新指定的閒置變數(Free variable)也代表著可變的狀態,而可變狀態代表著在並行程式設計(JDK8 會想要採用 Lambda 的理由之一)會有鎖定問題,JDK8 特意禁止你捕捉可變動的區域變數。你無法在 Lambda 表示式中改變被捕捉的變數值。
你已經看過 JDK8 Lambda 的基本語法了,如你到目前看到的,與現有 API 保持相容性也是 Java 中採用 Lambda 的目標之一。Java 是一門古老且具有一大堆 API 的語言,它會採用什麼策略來解決這個問題?這是接下來要討論的內容。
## 方法參考與建構式參考
根據名稱的長度進行排序,可以如下撰寫程式:
```java
List names = Arrays.asList("Justin", "Monica", "Irene", "caterpillar");
Collections.sort(names, new Comparator() {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
});
```
如果你單只是看 `compare` 的方法本體實作,並不容易看出程式碼要做些什麼。你也許還會有其他的排序策略,因此,你在 `StringOrder` 類別中,定義了幾個 `static` 方法:
```java
public class StringOrder {
public static int byLength(String s1, String s2) {
return s1.length() - s2.length();
}
public static int byLexicography(String s1, String s2) {
return s1.compareTo(s2);
}
public static int byLexicographyIgnoreCase(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
...
}
```
現在,你可以將先前的程式碼改寫為以下:
```java
Collections.sort(names, new Comparator() {
public int compare(String s1, String s2) {
return StringOrder.byLength(s1, s2);
}
});
```
程式打算做些什麼,現在看來是清楚多了。使用 JDK8 Lambda 的話,可以讓這個程式碼變得更清楚些。
```java
Collections.sort(names, (s1, s2) -> StringOrder.byLength(s1, s2));
```
也許有個聰明的傢伙發現了,除了方法名稱之外,`byLength` 方法的簽署與 `Comparator` 的 `compare` 方法相同。我們知道,Lambda 運算式是匿名方法(函式),而 Lambda 運算式的本體部份就是函式介面(Functional interface)的方法實作。因為我們只是把參數 `s1` 與 `s2` 傳給 `byLength` 方法,那麼可以直接重用 `byLength` 方法的實作不是更好嗎?是的,JDK8 提供了方法可參考的特性,可以達到這個目的:
```java
Collections.sort(names, StringOrder::byLength);
```
在 Java 中引人 Lambda 的同時,與現有 API 維持相容性是主要考量之一。除了採用函式介面之外,方法參數(Method reference)在重用現有 API 上也扮演了重要的角色。重用現有的方法實作,可避免到處寫下 Lambda 運算式。上面的例子是運用了方法參考中的一種形式 – 參考了 `static `方法。你也可以參考至特定型態的任意物件之實例方法。例如,按照字典順序對名稱清單進行排序,原本可以如下撰寫:
```java
Collections.sort(names, StringOrder::byLexicography);
```
從先前的段落說明中,我們知道 `StringOrder::byLexicography` 會參考到 `byLexicography` 方法實作,而以下的程式碼也有相同的排序效果:
```java
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));
```
我們可以發現到,在 Lambda 運算式的本體部份,第一個參數 `s1` 會是 `compareTo` 的接受者,而第二個參數 `s2` 則是 `compareTo` 方法的引數,在這種情況下,其實我們可以直接參考 `String` 類別的 `compareTo` 方法,像是:
```java
Collections.sort(names, String::compareTo);
```
類似地,想對名稱清單按照字典順序排序,但忽略大小寫差異,本來可以如下參考 `static` 方法來達到:
```java
Collections.sort(names, StringOrder::byLexicographyIgnoreCase);
```
再次地,在 `byLexicographyIgnoreCase` 的方法實作中,第一個參數是 `compareToIgnoreCase` 方法的接受者,而第二個參數是 `compareToIgnoreCase` 方法的引數,此時,我們可以直接參考 `String` 類別的 `compareToIgnoreCase` 方法。
```java
Collections.sort(names, String::compareToIgnoreCase);
```
可輕易觀察到,方法參考不僅避免了重複撰寫 Lambda 運算式,也可以讓程式碼更為清楚。除了以下兩種方法參考形式外,我們還可以參考特定物件的實例方法。例如,假設你正在設計一個可以過濾職缺應徵者的軟體,而你有以下兩個類別:
```java
public class JobVacancy {
...
public int bySeniority(JobApplicant ja1, JobApplicant ja2) {
...
}
public int byEducation(JobApplicant ja1, JobApplicant ja2) {
...
}
...
}
```
```java
public class JobApplicant {
...
}
```
如果你使用 JDK8,並如下撰寫 Lambda 演算式來進行應徵者的排序:
```java
List applicants = ...;
JobVacancy vacancy = ...;
Collections.sort(applicants, (ja1, ja2) -> vacancy.bySeniority(ja1, ja2));
```
Lambda 運算式捕捉了 `vacancy` 參考的物件。`bySeniority` 方法的簽署與 `Comparator `的 `compare` 方法相同,此時,我們可以直接參考 `vacancy` 物件的 `bySeniority` 方法。
```java
Collections.sort(applicants, vacancy::bySeniority);
```
除了方法參考之外,JDK8 還提供了建構式參考(Constructor references)。你也許會發出疑問:「建構式?他們有傳回值型態嗎?」有的!其實每個建構式都會有傳回值型態 – 也就是定義它們的類別本身。例如,若你有以下的介面:
```java
public interface Part {
...
}
```
```java
public interface Material {
...
}
```
```java
public interface PartFactory {
Part createPart(Material material);
}
```
你為這些介面撰寫了一些實作:
```java
public class PartImpl implements Part {
public PartImpl(Material material) {
...
}
}
```
```java
public class MaterialImpl implements Material {
...
}
```
```java
public PartFactoryImpl implements PartFactory {
public Part createPart(Material material) {
return new PartImpl(material);
}
}
```
接著,你可能使用以下的程式碼來建立 `Part` 實例:
```java
PartFactory factory = new PartFactoryImpl();
Part part = factory.createPart(new MaterialImpl());
```
`createPart` 方法的實作中,只是使用建構式來建立了 `Part` 的實例。使用 JDK8 的話,你就不用特別花時間定義 `PartFactoryImpl` 類別,你可以直接參考 `PartImpl` 的建構式。
```java
PartFactory factory = PartImpl::new;
Part part = factory.createPart(new MaterialImpl());
```
如果某類別有多個建構式,就會使用函式介面的方法簽署來比對,找出對應的建構式進行呼叫。
「等一下!怎麼沒討論預設方法(Default method)?那不是 Lambda 專案的一部份嗎?」
是的,預設方法確實是 Lambda 專案的一部份,不過它與現存的 API 如何演化有關。預設方法解除了介面上的一些限制,讓 Java 介面在進行防禦式(Defensive)的 API 演化時容易一些,並為流程的重用開啟了更多可能性,不過,也帶入多重繼承上的一些複雜度。在討論如何將現在的 API 演化的時候,我們也許會看到一些函數式程式設計(Functional programming)的影子。這些會留到下一個篇章 〈函數式程式設計〉 中來討論。
----------
----------
# 函數式程式設計
在 〈認識 Lambda/Closure〉 中,我談到了什麼是 Lambda/Closure、不同程式語言的支援方式,以及為什麼 JDK8 採用了目前的 Lambda 語法。想要善用 Lambda,可以從其他已具備一級函式的語言中借鏡,實際上在這些語言中,許多一級函式的概念是直接或間接來自於函數式程式設計(Functional programming),更進一步地,函數式程式設計的模型是基於 Lambda 演算。若能一步步深入這些概念,就更能夠瞭解 Lambda/Closure 的實際應用場合。
## 初探函數式程式設計
在 〈一級函式與 Lambda 演算〉 中,曾經簡介過 Lambda 演算的基本概念,而接下來我會介紹何謂函數式程式設計,以及在 Java 中如何進行函數式程式設計。不過,為什麼要認識函數式程式設計?
在 [〈你的程式語言可以這樣做嗎?〉](http://local.joelonsoftware.com/wiki/%E4%BD%A0%E7%9A%84%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80%E5%8F%AF%E4%BB%A5%E9%80%99%E6%A8%A3%E5%81%9A%E5%97%8E%EF%BC%9F "〈你的程式語言可以這樣做嗎?〉")([Can Your Programming Language Do This?](http://www.joelonsoftware.com/items/2006/08/01.html "Can Your Programming Language Do This?"))中,Joel Spolsky 說過:
> …有第一級函數的編程語言讓你找到更多抽象化的機會…
在《編程的領尖對話》(Coders at Work) 這本書中,Simon Peyton Jones 提到:
> …純函數式領域中學到的觀念與想法,可能給主流領域帶來資訊,帶來啟發…
在 《Java 開發者的函數式程式設計》(Functional Programming for Java Developers)這本書中,Dean Wampler 在第一章〈為何要函數式程式設計?〉(Why Function Programming?)列出了五個看法:
* 我想寫好並行程式(Concurrent program)
* 大部份程式只是在做資料處理問題(Data Management Problem)
* 函數式程式設計更模組化
* 工作上我要更有效率
* 函數式程式設計是返樸歸真
想瞭解這幾點在說些什麼,最好的方式就是親自進行函數式程式設計。
> **插播** 我故意模彷了 Joel Spolsky 的文章,寫了篇 [〈你的腦袋可以這樣想嗎?〉](http://www.javaworld.com.tw/roller/caterpillar/entry/can_your_brain_think_this "〈你的腦袋可以這樣想嗎?〉"),有興趣的可以參考看看。
首先來看看,要怎麼取得費式數(Fibonacci number)。根據維基百科中對費式數列的定義:
> 在數學上,費波那西數列是以遞歸的方法來定義:
>
> ```
F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2
```
在命令式程式設計(Imperative programming)中,你必須告訴電腦求解問題的每個步驟。例如,可以使用 Java 實作一個如下的 `static` 方法:
```java
static int fib(int n) {
if(n == 0 || n == 1) {
return n;
}
int a = 0;
int b = 1;
for(int i = 2; i <= n; i++) {
int tmp = b;
b = a + b;
a = tmp;
}
return b;
}
```
在函數式程式設計中,只要定義問題就可以了。也就是說,以你使用的程式語言來宣告問題。例如,可以使用 Java 如下宣告問題:
```java
static int fib(int n) {
if(n == 0 || n == 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
```
這個程式碼的可讀性比第一個好此,不過如果程式語言直接支援函數式程式設計的話,會有更好的可讀性。例如,使用 Haskell 來實作的話,程式碼看起來與費式數的數學定義就很像:
```haskell
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)
```
不過,這個函式的效能不太好。因為每次呼叫都會再進行兩次自身呼叫,如此呼叫下去,遞迴呼叫次數會呈指數增長。
可以使用遞迴中的一個特定形式 – 尾遞迴(Tail recursion) – 來改寫這個函式。在尾端迴中,函式會直接呼叫自身,或者是在最後一個步驟進行回傳,因此在呼叫堆疊中增加一個堆疊框架(Stack frame)時會耗用較少的資源,例如,此時就不再需要記錄區域變數,因為必要的運算都已完成;從遞迴返回時,也只需回傳相同的值,更重要的是,函數式語言常允許尾遞迴消去(Tail recursion elimination),像是在編譯時期用迴圈來取代尾遞迴,或者在執行時期進行最佳化,藉以增進效能。使用尾遞迴來重寫先前函式的話,會像是:
```haskell
fib 0 = 0
fib 1 = 1
fib n = addPrevsRecusivelyUntilCounterIsN (fib 1) (fib 0) 2 n
addPrevsRecusivelyUntilCounterIsN prev1 prev2 counter n
| counter == n = result
| otherwise = addPrevsRecusivelyUntilCounterIsN result prev1 (counter + 1) n
where result = prev1 + prev2
```
呃 … 尾遞迴可能會增加一些複雜性。效能與可讀性幾乎總是對立的,即便如此,至少函數式程式設計的基本原則還是存在的,我們還是在定義問題,只是定義變得比較囉嗦而已,只不過,使用函數式語言的話,可以有更多的機會在效能與可讀性之間找到平衡點。你也可以運用尾遞迴來改寫先前的 Java 方法,只不過,程式碼會更複雜,而且 Java 並不支援尾遞迴消去。
這邊的重點在於,如果使用的語言並非函數式語言,你不能不假思索地直接套用函數式設計的所有概念,否則就有可能事倍功半,可讀性與效能都會變差。如果你想進行純函數式程式設計,使用函數式語言會比較好,像是 Scala、Haskell 等。
那麼使用非函數式語言,要怎麼撰寫函數式風格的程式碼呢?只有先瞭解函數式設計的本質,才可以讓我們明瞭,如何適當地擷取函數式設計的概念。
函數式程式設計中一個顯而易見的外在形式就是遞迴,不過,遞迴不是重點,將問題分解為子問題才是重點,通常子問題在分解之後,自然而然就會形成遞迴。以加總數列為例,以命令式方式求解的話,就必須要求電腦將變數 `sum` 初始為 0,取得數列的下個元素,將元素加至 `sum`,重複這個動作直到所有元素都迭代過為止,接著傳回 `sum`。
```java
static int sum(int... nums) {
int sum = 0;
for(int num : nums) {
sum += num;
}
return sum;
}
```
那麼,以函數式要如何求解呢?讓我們重新思考加總數列這個問題,子問題之一是,空數列的加總為何?是 0!子問題之二是,首元素為 `x` 而尾數列為 `xs` 的加總為何?那就是 `x` 與 `xs` 的總和進行加總!使用 Haskell 寫下這兩個子問題的話就會是…
```haskell
sum [] = 0
sum (x:xs) = x + sum xs
```
那麼要怎麼求解呢?不用求解,Haskell 會幫你求解,你只要定義問題就好了。
「等一下!等一下!現在是在討論 Java 啊!別老是寫 Haskell…XD」
好的!不過呢!物件導向程式設計是 Java 的主流典範,在使用 Java 以函數式方式求解之前,得先來談談代數資料型態(Algebraic data type)、處理數列時共通的模式、以及不可變特性(Immutability),這會在之後分別探討…
## 代數資料型態
我們大多熟悉物件導向程式設計,熟悉抽象資料型態(Abstract data type, ADT)。抽象資料型態的模型中封裝了資料結構與實作,僅透露互動時的公開介面;然而,代數資料型態(Algebraic data type)相對地曝露了基本的資料結構及規律性,在函數式程式設計的領域中,代數資料型態是基本元素。
> **插播** 縮寫的 ADT 廣泛應用為 Abstract Data Type 的縮寫,在函數式程式設計中並不使用這個縮寫,因此英文中都直接使用 Abstract Data Type 作為全名。)
Java 是物件導向程式語言,對代數資料型態沒有直接的支援,有兩種方式可以模擬該型態。由於代數資料型態會曝露基本的資料結構,因而可使用具公開值域(Field)的類別來模擬代數資料型態,不過,許多物件導向原則並不鼓勵公開值域,如此一來就得尋找其他方式來模擬。因為代數資料型態會曝露規律性,規律性這聽起來像個行為表現,在 Java 中討論行為時,通常會使用 `interface` 加以定義。
以清單類型為例,我們知道 Java SE API 中定義了 `java.util.List`,而這個 `List` 是抽象資料型態。如果想要以代數資料型態的模型來定義清單該怎麼做?在函數式程式設計中,清單會是由首(head)元素與尾(tail)清單組成。若想使用 `interface` 來定義(或模擬)這樣的清單則會是:
```java
public interface List {
T head();
List tail();
}
```
這種清單的實例之一是空清單,若運用以上介面來實作一個空清單的話,可以如下 …
```java
public class AlgebraicType {
private static List extends Object> Nil = new List