從歷史上看,用 Java 進行函數式編程并不容易,甚至一些函數式編程在 Java 中是不可能實現的。在 Java 8 , Oracle 做了一些努力使得函數式編程變得更容易,這些努力在某種程度上取得了成功。在這個 Java 函數式編程教程中將介紹基本的函數式編程,以及 Java 中可以實現的部分。
函數式編程基礎
函數式編程包括以下幾個關鍵的概念:
- 函數是第一類對象
- 純函數
- 高階函數
純函數編程也有一套規則需要遵守:
- 無狀態
- 無副作用
- 不可變變量
- 優先使用遞歸而不是循環
這些概念和規則將會在這篇教程的剩余部分一一介紹。
盡管如果你一直沒有遵守這些規則,你同樣可以從函數式編程中獲取收獲并應用在你的應用中。你將會看到函數式編程并不是解決所有問題的正確工具,特別是 “無副作用” 使得寫入數據庫(這是個副作用)變得很困難。你需要了解哪些問題函數式編程擅長解決,哪些不是。
函數是第一類對象
在函數式編程范式中,函數是語言里第一類對象。這意味著你可以創建一個函數的 “實例”,作為一個函數實例的引用,就像 String、 Map 和其它對象的引用一樣。函數同樣可以當做參數傳遞給其它函數。
在 Java 里,方法 不是第一類對象。 最接近的是 Java Lamda 表達式,在這里我不討論這個。
純函數
一個函數如果是一個 純函數,需要滿足以下條件:
- 函數的執行不產生副作用
- 函數的返回值只依賴入參
下面是一個 Java 里的 純函數 例子:
public class ObjectWithPureFunction{
public int sum(int a, int b) {
return a + b;
}
}
注意 sum()
函數返回值只依賴入參。同樣注意下 sum()
函數沒有副作用,意味著它不改變當前函數以外任何狀態(變量)。
肯定的是,下面這是不是一個 純函數:
public class ObjectWithNonPureFunction{
private int value = 0;
public int add(int nextValue) {
this.value += nextValue;
return this.value;
}
}
注意 add()
方法用了一個成員變量來計算它的返回值,而且它也修改改了 value
成員變量的值,因此他有副作用。
高階函數
一個函數如果是高階函數則至少要滿足以下條件之一:
- 使用一個或多個函數作為參數
- 函數返回另外一個函數作為結果
在 Java 中,最接近高階函數的是:一個函數(方法)使用一個或多個 lambda 表達式作為參數,以及返回另外一個 lambda 表達式。下面就是一個 Java 里 高階函數 的例子:
public class HigherOrderFunctionClass {
public <T> IFactory<T> createFactory(IProducer<T> producer, IConfigurator<T> configurator) {
return () -> {
T instance = producer.produce();
configurator.configure(instance);
return instance;
}
}
}
注意 :1. createFactory()
方法返回一個 lambda 表達式作為返回值。這是作為 函數式編程的一個條件。
2. createFactory()
方法使用兩個接口(IProducer
and Iconfigurator
)的實現的實例作為參數. Java lambda 表達式需要實現一個 函數式接口,記得嗎?
想象一下接口是這樣:
public interface IFactory<T> {
T create();
}
public interface IProducer<T> {
T produce();
}
public interface IConfigurator<T> {
void configure(T t);
}
你可以看到,這些接口都是函數式接口。因此他們可以被 Java lamda 表達式所實現 —— 所以 createFactory
方法是一個 高階函數
無狀態
像在教程開頭提到的,函數式編程范式的規則之一就是無狀態。 "無狀態" 通常指函數之外沒有狀態。一個函數可以擁有包含內部臨時狀態的本地變量,但是這個函數不能引用任何該函數所屬類或對象的成員變量。
下面是一個沒有使用外部狀態的例子:
public class Calculator {
public int sum(int a, int b) {
return a + b;
}
}
相反, 下面是一個使用外部狀態的例子
public class Calculator {
private int initVal = 5;
public int sum(int a) {
return initVal + a;
}
}
這個函數違背了 "無狀態" 規則
無副作用
函數式范式另外一個規則就是 "無副作用"。這意味著,一個函數不能修改任何函數之外的狀態。改變函數之外的狀態被稱作一個 副作用。
函數外部狀態有:函數所屬類或對象的成員變量,函數參數中的成員變量,或者外部系統的狀態比如文件系統或者數據庫。
不可變變量(Immutable Variables)
函數式編程范式的第三個規則是 “不可變變量”。不可變變量有利于防止產生副作用。
優先使用遞歸而不是循環
函數式編程范式的第四個規則是“優先使用遞歸而不是循環”。遞歸使用函數調用來達到循環的目的,因此代碼變得更函數式。
循環的另一個代替是 Java Stream API. 這個 API 是受 函數式啟發的。
函數式接口
函數式接口在 Java 里是只擁有一個抽象方法的接口。一個抽象方法意味著只有一個方法沒有實現。一個接口可以有多個方法比如默認方法和靜態方法——兩個都有實現,但只要這個接口只有一個沒有實現的接口,這個接口就叫做 函數式接口
下面是一個函數式的例子:
public interface MyInterface {
public void run();
}
下面是另外一個攜帶默認方法和靜態方法實現的函數式接口:
public interface MyInterface2 {
public void run();
public default void doIt() {
System.out.println("doing it");
}
public static void doItStatically() {
System.out.println("doing it statically");
}
}
注意這兩個擁有實現的方法。這仍是一個 函數接口,因為只有 run()
方法沒有被實現。可是如果再有一個沒有實現的方法,這個接口就不再是函數式接口,不能被 Java lambda 表達式所實現。
譯自: Java Functional Programming