Java 語言中常被忽視的一個方面是它被歸類為一種命令式(imperative)編程語言。命令式編程雖然由於與 Java 語言的關聯而相當普及,但是並不是惟一可用的編程風格,也不總是最有效的。在本文中,我將探討在 Java 開發實踐中加入不同的編程方法 ── 即函數編程(FP)。
命令式編程是一種用程序狀態描述計算的方法。使用這種范型的編程人員用語句改變程序狀態。這就是為什麼,像 Java 這樣的程序是由一系列讓計算機執行的命令 (或者語句) 所組成的。另一方面, 函數編程是一種強調表達式的計算而非命令的執行的一種編程風格。表達式是用函數結合基本值構成的,它類似於用參數調用函數。
本文將介紹函數編程的基本特點,但是重點放在兩個特別適用於 Java 開發框架的元素:閉包和高階函數。如果您曾經使用過像 Python、Ruby 或者 Groovy 這樣的敏捷開發語言,那麼您就可能已經遇到過這些元素。在這裡,您將看到在 Java 開發框架中直接使用它們會出現什麼情況。我將首先對函數編程及其核心元素做一個簡短的、概念性的綜述,然後用常用的編程場景展示,用結構化的方式使用閉包和高階函數會給 Java 代碼帶來什麼好處。
什麼是函數編程?
在經常被引用的論文 “Why Functional Programming Matters”中,作者 John Hughes 說明了模塊化是成功編程的關鍵,而函數編程可以極大地改進模塊化。在函數編程中,編程人員有一個天然框架用來開發更小的、更簡單的和更一般化的模塊, 然後將它們組合在一起。函數編程的一些基本特點包括:
支持閉包和高階函數。
支持懶惰計算(lazy evaluation)。
使用遞歸作為控制流程的機制。
加強了引用透明性。
沒有副作用。
我將重點放在在 Java 語言中使用閉包和高階函數上,但是首先對上面列出的所有特點做一個概述。
閉包和高階函數
函數編程支持函數作為第一類對象,有時稱為 閉包或者 仿函數(functor)對象。實質上,閉包是起函數的作用並可以像對象一樣操作的對象。與此類似,FP 語言支持 高階函數。高階函數可以用另一個函數(間接地,用一個表達式) 作為其輸入參數,在某些情況下,它甚至返回一個函數作為其輸出參數。這兩種結構結合在一起使得可以用優雅的方式進行模塊化編程,這是使用 FP 的最大好處。
命令式編程
命令式編程這個名字是從自然語言(比如英語)的 祈使語氣(imperative mood)衍生出來的,在這種語氣中宣布命令並按照執行。除 Java 語言之外,C 和 C++ 是另外兩種廣泛使用的、符合命令式風格的高級編程語言。
懶惰計算
除了高階函數和仿函數(或閉包)的概念,FP 還引入了 懶惰計算的概念。在懶惰計算中,表達式不是在綁定到變量時立即計算,而是在求值程序需要產生表達式的值時進行計算。延遲的計算使您可以編寫可能潛在地生成無窮輸出的函數。因為不會計算多於程序的其余部分所需要的值,所以不需要擔心由無窮計算所導致的 out-of-memory 錯誤。一個懶惰計算的例子是生成無窮 Fibonacci 列表的函數,但是對 第 n 個Fibonacci 數的計算相當於只是從可能的無窮列表中提取一項。
遞歸
FP 還有一個特點是用遞歸做為控制流程的機制。例如,Lisp 處理的列表定義為在頭元素後面有子列表,這種表示法使得它自己自然地對更小的子列表不斷遞歸。
關於實現庫
我使用了由 Apache Commons Functor 項目提供的庫構建本文使用的例子。Apache Commons Functor 庫包括大量基本構造,可以在涉及閉包和高階函數的復雜使用場景中重復使用。當然,可以使用不同的實現(如 Java Generic Libraries、Mango 或者 Generic Algorithms for Java),而不會對在本文中所討論和展示的概念有影響,盡管您必須下載和使用 Apache Commons Functor 庫才能演示這裡的例子。
引用透明性
函數程序通常還加強 引用透明性,即如果提供同樣的輸入,那麼函數總是返回同樣的結果。就是說,表達式的值不依賴於可以改變值的全局狀態。這使您可以從形式上推斷程序行為,因為表達式的意義只取決於其子表達式而不是計算順序或者其他表達式的副作用。這有助於驗證正確性、簡化算法,甚至有助於找出優化它的方法。
副作用
副作用是修改系統狀態的語言結構。因為 FP 語言不包含任何賦值語句,變量值一旦被指派就永遠不會改變。而且,調用函數只會計算出結果 ── 不會出現其他效果。因此,FP 語言沒有副作用。
這些基本描述應足以讓您完成本文中的函數編程例子。
Java 語言中的函數編程
不管是否相信,在 Java 開發實踐中您可能已經遇到過閉包和高階函數,盡管當時您可能沒有意識到。例如,許多 Java 開發人員在匿名內部類中封閉 Java 代碼的一個詞匯單元(lexical unit)時第一次遇到了 閉包。這個封閉的 Java 代碼單元在需要時由一個 高階函數 執行。例如,清單 1 中的代碼在一個類型為 java.lang.Runnable 的對象中封閉了一個方法調用。
清單 1. 隱藏的閉包
Runnable worker = new Runnable()
{
public void run()
{
parseData();
}
};
方法 parseData 確實 封閉(因而有了名字 “閉包”)在 Runnable 對象的實例 worker 中。它可以像數據一樣在方法之間傳遞,並可以在任何時間通過發送消息(稱為 run ) 給 worker 對象而執行。
更多的例子
另一個在面向對象世界中使用閉包和高階函數的例子是 Visitor 模式。基本上,Visitor 模式展現一個稱為 Visitor 的參與者,該參與者的實例由一個復合對象(或者數據結構)接收,並應用到這個數據結構的每一個構成節點。Visitor 對象實質上 封閉 了處理節點/元素的邏輯,使用數據結構的 accept (visitor) 方法作為應用邏輯的高階函數。
通過使用適當不同的 Visitor 對象(即閉包),可以對數據結構的元素應用完全不同的處理邏輯。與此類似,可以向不同的高階函數傳遞同樣的閉包,以用另一種方法處理數據結構(例如,這個新的高階函數可以實現不同邏輯,用於遍歷所有構成元素)。
類 java.utils.Collections 提供了另一個例子,這個類在版本 1.2 以後成為了 Java 2 SDK 的一部分。它提供的一種實用程序方法是對在 java.util.List 中包含的元素排序。不過,它使調用者可以將排序列表元素的邏輯封裝到一個類型為 java.util.Comparator 的對象中,其中 Comparator 對象作為第二個參數傳遞給排序方法。
在內部, sort 方法的引用實現基於 合並-排序(merge-sort) 算法。它通過對順序中相鄰的對象調用 Comparator 對象的 compare 方法遍歷列表(list)中的元素。在這種情況下, Comparator 是閉包,而 Collections.sort 方法是高階函數。這種方式的好處是明顯的:可以根據在列表中包含的不同對象類型傳遞不同的 Comparator 對象(因為如何比較列表中任意兩個對象的邏輯安全地封裝在 Comparator 對象中)。與此類似, sort 方法的另一種實現可以使用完全不同的算法(比如說, 快速排序(quick-sort)) 並仍然重復使用同樣的 Comparator 對象,將它作為基本函數應用到列表中兩個元素的某種組合。
創建閉包
廣意地說,有兩種生成閉包的技術,使用閉包的代碼可以等效地使用這兩種技術。創建閉包後,可以以統一的方式傳遞它,也可以向它發送消息以讓它執行其封裝的邏輯。因此,技術的選擇是偏好的問題,在某些情況下也與環境有關。
在第一種技術 表達式特化(expression specialization)中,由基礎設施為閉包提供一個一般性的接口,通過編寫這個接口的特定實現創建具體的閉包。在第二種技術 表達式合成(expression composition) 中,基礎設施提供實現了基本一元/二元/三元/.../n 元操作(比如一元操作符 not 和二元操作符 and / or )的具體幫助類。在這裡,新的閉包由這些基本構建塊的任意組合創建而成。
我將在下面的幾節中詳細討論這兩種技術。
表達式特化
假定您在編寫一個在線商店的應用程序。商店中可提供的商品用類 SETLItem 表示。每一件商品都有相關的標簽價格, SETLItem 類提供了名為 getPrice 的方法,對商品實例調用這個方法時,會返回該商品的標簽價格。
如何檢查 item1 的成本是否不低於 item2 呢?在 Java 語言中,一般要編寫一個像這樣的表達式:
assert(item1.getPrice() >= item2.getPrice());
像這樣的表達式稱為 二元謂詞(binary predicate), 二元是因為它取兩個參數,而 謂詞 是因為它用這兩個參數做一些事情並生成布爾結果。不過要注意,只能在執行流程中執行上面的表達式,它的輸出取決於 item1 和 item2 在特定瞬間的值。從函數編程的角度看,這個表達式還不是一般性的邏輯,就是說,它不能不管執行控制的當前位置而隨心所欲地傳遞並執行。
為了使二元謂詞發揮作用,必須將它封裝到一個對象中,通過 特化(specializing) 一個稱為 BinaryPredicate 的接口做到這一點,這個接口是由 Apache Functor 庫提供的,如清單 2 所示。
清單 2. 表達式特化方法
package com.infosys.setl.fp;
public class SETLItem
{
private String name;
private String code;
private int price;
private String category;
public int getPrice()
{
return price;
}
public void setPrice(int inPrice)
{
price = inPrice;
}
public String getName()
{
return name;
}
public void setName(String inName)
{
name = inName;
}
public String getCode()
{
return code;
}
public void setCode(String inCode)
{
code = inCode;
}
public String getCategory()
{
return category;
}
public void setCategory(String inCategory)
{
category = inCategory;
}
}
package com.infosys.setl.fp;
import java.util.Comparator;
public class PriceComparator implements Comparator
{
public int compare (Object o1, Object o2)
{
return (((SETLItem)o1).getPrice()-((SETLItem)o2).getPrice());
}
}
package com.infosys.setl.fp;
import org.apache.commons.functor.*;
import org.apache.commons.functor.core.comparator.*;
import java.util.*;
public class TestA
{
public static void main(String[] args)
{
try
{
Comparator pc = new PriceComparator();
BinaryPredicate bp = new IsGreaterThanOrEqual(pc);
SETLItem item1 = new SETLItem();
item1.setPrice(100);
SETLItem item2 = new SETLItem();
item2.setPrice(99);
if (bp.test(item1, item2))
System.out.println("Item1 costs more than Item2!");
else
System.out.println("Item2 costs more than Item1!");
SETLItem item3 = new SETLItem();
item3.setPrice(101);
if (bp.test(item1, item3))
System.out.println("Item1 costs more than Item3!");
else
System.out.println("Item3 costs more than Item1!");
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
BinaryPredicate 接口以由 Apache Functor 庫提供的 IsGreaterThanOrEqual 類的形式特化。PriceComparator 類實現了 java.util.Comparator 接口,並被作為輸入傳遞給 IsGreaterThanOrEqual 類。收到一個 test 消息時, IsGreaterThanOrEqual 類自動調用 PriceComparator 類的 compare 方法。compare 方法預期接收兩個 SETLItem 對象,相應地它返回兩個商品的價格差。compare 方法返回的正值表明 item1 的成本不低於 item2 。
初看之下,對一個相當基本的操作要做很多的工作,那它有什麼好處呢?特化 BinaryPredicate 接口(而不是編寫 Java 比較表達式) 使您無論在何時何地都可以比較任意兩個商品的價格。可以將 bp 對象作為數據傳遞並向它發送消息,以在任何時候、使用這兩個參數的任何值來執行它(稱為 test )。
表達式合成
表達式合成是得到同樣結果的一種稍有不同的技術。考慮計算特定 SETLItem 的淨價問題,要考慮當前折扣和銷售稅率。清單 3 列出了這個問題基於仿函數的解決方式。
清單 3. 表達式合成方法
package com.infosys.setl.fp;
import org.apache.commons.functor.BinaryFunction;
import org.apache.commons.functor.UnaryFunction;
import org.apache.commons.functor.adapter.LeftBoundFunction;
public class Multiply implements BinaryFunction
{
public Object evaluate(Object left, Object right)
{
return new Double(((Double)left).doubleValue() * ((Double)right).doubleValue());
}
}
package com.infosys.setl.fp;
import org.apache.commons.functor.*;
import org.apache.commons.functor.core.composite.*;
import org.apache.commons.functor.adapter.*;
import org.apache.commons.functor.UnaryFunction;
public class TestB
{
public static void main(String[] args)
{
try
{
double discountRate = 0.1;
double taxRate=0.33;
SETLItem item = new SETLItem();
item.setPrice(100);
UnaryFunction calcDiscountedPrice =
new RightBoundFunction(new Multiply(), new Double(1-discountRate));
UnaryFunction calcTax =
new RightBoundFunction(new Multiply(), new Double(1+taxRate));
CompositeUnaryFunction calcNetPrice =
new CompositeUnaryFunction(calcTax, calcDiscountedPrice);
Double netPrice = (Double)calcNetPrice.evaluate(new Double(item.getPrice()));
System.out.println("The net price is: " + netPrice);
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
BinaryFunction 類似於前面看到的 BinaryPredicate ,是一個由 Apache Functor 提供的一般化仿函數(functor)接口。BinaryFunction 接口有兩個參數並返回一個 Object 值。類似地, UnaryFunction 是一個取一個 Object 參數並返回一個 Object 值的仿函數接口。
RightBoundFunction 是一個由 Apache 庫提供的適配器類,它通過使用常量右參數(right-side argument)將 BinaryFunction 適配給 UnaryFunction 接口。即,在一個參數中收到相應的消息( evaluate ) 時,它在內部用兩個參數發送一個 evaluate 消息給正在適配的 BinaryFunction ── 左邊的是發送給它的參數,右邊的是它知道的常量。您一定會猜到,名字 RightBoundFunction 來自於常量值是作為第二個 (右邊) 參數傳遞這一事實。(是的,Apache 庫還提供了一個 LeftBoundFunction ,其中常量是作為第一個參數或者左參數傳遞的。)
用於雙精度相乘的特化的 BinaryFunction
清單 3 顯示了名為 Multiply 的特化的 BinaryFunction ,它取兩個 Double 作為輸入並返回一個新的、由前兩個雙精度值相乘而得到 Double 。
在 calcDiscountedRate 中實例化了一個新的 RightBoundFunction ,它通過用 (1 - discountRate) 作為其常量第二參數,將二元 Multiply 函數適配為一元接口。
結果,可以用一個 Double 參數向 calcDiscountRate 發送一個名為 evaluate 的消息。在內部,輸入參數 Double 乘以 calcDiscountRate 對象本身包含的常量值。
與此類似,在 calcTaxRate 中實例化了一個新的 RightBoundFunction ,它通過用 (1 + taxRate) 作為其第二個常量參數將二元 Multiply 函數適配為一元接口。結果,可以用一個 Double 參數向 calcTaxRate 發送一個名為 evaluate 的消息。在內部,輸入參數 Double 乘以 calcTaxRate 對象本身包含的常量值。
這種將多參數的函數重新編寫為一個參數的函數的合成(composition)技術也稱為 currying。
合成魔術在最後的時候就發揮作用了。實質上,計算對象淨價的算法是首先計算折扣價格(使用 calcDiscountRate 仿函數),然後通過在上面加上銷售稅(用 calcSalesTax 仿函數)計算淨價。就是說,需要組成一個函數,在內部調用第一個仿函數並將計算的輸出流作為第二個仿函數的計算的輸入。Apache 庫提供了用於這種目的的一個內置仿函數,稱為 CompositeUnaryFunction 。
在清單 3 中, CompositeUnaryFunction 實例化為變量 calcNetPrice ,作為 calcDiscountRate 和 calcSalesTax 仿函數的合成。與前面一樣,將可以向其他函數傳遞這個對象,其他函數也可以通過向它發送一個包含商品參數的 evaluate 消息要求它計算這種商品的淨價。
一元與二元合成
在清單 3 中,您看到了 一元合成的一個例子,其中一個一元仿函數的結果是另一個的輸入。另一種合成稱為 二元合成,作為 evaluate 消息的一部分,需要傳遞兩個一元仿函數的結果作為二元仿函數的參數。
清單 4 是說明二元合成的必要性和風格的一個例子。假定希望保證商店可以給出的最大折扣有一個最大限度。因此,必須將作為 calcDiscount 仿函數計算結果得到的折扣量與 cap 值進行比較,並取最小值作為計算出的折扣價格。折扣價格是通過用標簽價減去實際的折扣而計算的。
清單 4. 二元合成
package com.infosys.setl.fp;
import org.apache.commons.functor.BinaryFunction;
public class Subtract implements BinaryFunction
{
public Object evaluate(Object left, Object right)
{
return new Double(((Double)left).doubleValue() - ((Double)right).doubleValue());
}
}
package com.infosys.setl.fp;
import org.apache.commons.functor.BinaryFunction;
import org.apache.commons.functor.UnaryFunction;
public class BinaryFunctionUnaryFunction implements UnaryFunction
{
private BinaryFunction function;
public BinaryFunctionUnaryFunction(BinaryFunction f)
{
function=f;
}
public Object evaluate(Object obj)
{
return function.evaluate(obj,obj);
}
}
package com.infosys.setl.fp;
import org.apache.commons.functor.*;
import org.apache.commons.functor.core.composite.*;
import org.apache.commons.functor.adapter.*;
import org.apache.commons.functor.UnaryFunction;
import org.apache.commons.functor.core.Constant;
import org.apache.commons.functor.core.comparator.Min;
public class TestC
{
public static void main(String[] args)
{
double discountRate = 0.1;
double taxRate=0.33;
double maxDiscount = 30;
SETLItem item = new SETLItem();
item.setPrice(350);
UnaryFunction calcDiscount =
new RightBoundFunction(new Multiply(), new Double(discountRate));
Constant cap = new Constant(new Double(maxDiscount));
BinaryFunction calcActualDiscount =
new UnaryCompositeBinaryFunction (new Min(), calcDiscount, cap);
BinaryFunctionUnaryFunction calcActualDiscountAsUnary =
new BinaryFunctionUnaryFunction(calcActualDiscount);
BinaryFunction calcDiscountedPrice =
new UnaryCompositeBinaryFunction (new Subtract(), new Identity(), calcActualDiscountAsUnary);
BinaryFunctionUnaryFunction calcDiscountedPriceAsUnary =
new BinaryFunctionUnaryFunction(calcDiscountedPrice);
UnaryFunction calcTax =
new RightBoundFunction(new Multiply(), new Double(1+taxRate));
CompositeUnaryFunction calcNetPrice =
new CompositeUnaryFunction(calcTax, calcDiscountedPriceAsUnary);
Double netPrice = (Double)calcNetPrice.evaluate(new Double(item.getPrice()));
System.out.println("The net price is: " + netPrice);
}
}
通過首先觀察所使用的 Apache Functor 庫中的三個標准仿函數,開始分析和理解這段代碼。
UnaryCompositeBinaryFunction 仿函數取一個二元函數和兩個一元函數作為輸入。首先計算後兩個函數,它們的輸出作為輸入傳遞給二元函數。在清單 4 中對二元合成使用這個仿函數兩次。
Constant 仿函數的計算總是返回一個常量值(即在其構造時輸入的值),不管以後任何計算消息中傳遞給它的參數是什麼值。在清單 4 中,變量 cap 的類型為 Constant 並總是返回最大折扣數量。
Identity 仿函數只是返回作為 evaluate 消息的輸入參數傳遞給它的這個對象作為輸出。清單 4 顯示 Identity 仿函數的一個實例,該仿函數是在創建 calcDiscountedPrice 時作為一個一元仿函數創建和傳遞的。同時在清單 4 中, evaluate 消息包含標簽價格作為其參數,這樣 Identity 仿函數就返回標簽價格作為輸出。
第一個二元合成在用計算 calcDiscount (通過對標簽價格直接應用折扣率)和 cap 的 UnaryCompositeBinaryFunction 設置變量 calcActualDiscount 時是可見的。這兩個一元仿函數計算的輸出傳遞給稱為 Min 的內置二元仿函數,它比較這兩者並返回其中最小的值。
這個例子顯示了定制類 BinaryFunctionUnaryFunction 。這個類適配一個二元仿函數,使它像一元仿函數的接口。就是說,當這個類接收一個帶有一個參數的 evaluate 消息時,它在內部發送 (向其封裝的二元函數)一個 evaluate 消息,它的兩個參數是作為輸入接收的同一個對象。因為 calcActualDiscount 是二元函數,所以通過類型為 BinaryFunctionUnaryFunction 的 calcActualDiscountAsUnary 實例將它包裝到一個一元仿函數接口中。很快就可以看到包裝 calcActualDiscount 為一元仿函數的理由。
當用 UnaryCompositeBinaryFunction 設置變量 calcDiscountedPrice 時發生第二個二元合成。UnaryCompositeBinaryFunction 向新的 Identity 實例和 calcActualDiscountAsUnary 對象發送 evaluation 消息,這兩個消息的輸入參數都是標簽價格。
這兩個計算(它們分別得出標簽價格和實際的折扣值)的輸出傳遞給名為 Subtract 的定制二元仿函數。當向後一個對象發送 evaluate 消息時,它立即計算並返回兩個參數之間的差距(這是商品的折扣價)。這個二元仿函數也用定制的 BinaryFunctionUnaryFunction 包裝為一個名為 calcDiscountedPriceAsUnary 的一元仿函數對象。
與前面的情況一樣,代碼通過兩個 calcTax 一元仿函數(也在清單 3 中遇到)和 calcDiscountedPriceAsUnary (在前面一段中描述)創建 CompositeUnaryFunction ,而以一個一元合成完成。這樣得到的 calcNetPrice 變為接收一個 evaluate 消息和一個參數(所討論商品的標簽價格),而在內部,首先用這個參數計算 calcDiscountedPriceAsUnary 仿函數,然後用前一個計算的輸出作為參數計算 calcTax 仿函數。
使用閉包實現業務規則
Apache Library 提供了各種不同的內置一元和二元仿函數,它使得將業務邏輯編寫為可以傳遞並且可以用不同的參數在不同的位置執行的對象變得非常容易。在後面幾節中,我將使用一個簡單的例子展示對一個類似問題的函數編程方式。
假定一個特定的商品是否可以有折扣取決於該商品的類別和定價。具體說,只有 Category “A” 中定價高於 100 美元和 Category “B“ 中定價高於 200 美元的商品才有資格打折。清單 5 中的代碼顯示了一個名為 isEligibleForDiscount 的業務規則對象 ( UnaryPredicate ),如果用一個 item 對象作為參數發送 evaluate 消息,將返回一個表明是否可以對它打折的 Boolean。
清單 5. 一個函數業務規則對象
package com.infosys.setl.fp;
import org.apache.commons.functor.BinaryPredicate;
import org.apache.commons.functor.UnaryPredicate;
public class BinaryPredicateUnaryPredicate implements UnaryPredicate
{
private BinaryPredicate bp;
public BinaryPredicateUnaryPredicate(BinaryPredicate prd)
{
bp=prd;
}
public boolean test(Object obj)
{
return bp.test(obj,obj);
}
}
package com.infosys.setl.fp;
import org.apache.commons.functor.*;
import org.apache.commons.functor.core.composite.*;
import org.apache.commons.functor.adapter.*;
import org.apache.commons.functor.UnaryFunction;
import org.apache.commons.functor.core.Constant;
import org.apache.commons.functor.core.IsEqual;
import org.apache.commons.functor.core.comparator.IsGreaterThanOrEqual;
import org.apache.commons.functor.core.comparator.Min;
import org.apache.commons.functor.core.Identity;
public class TestD
{
public static void main(String[] args)
{
SETLItem item1 = new SETLItem();
item1.setPrice(350);
item1.setCategory("A");
SETLItem item2 = new SETLItem();
item2.setPrice(50);
item2.setCategory("A");
SETLItem item3 = new SETLItem();
item3.setPrice(200);
item3.setCategory("B");
UnaryFunction getItemCat =
new UnaryFunction()
{
public Object evaluate (Object obj)
{
return ((SETLItem)obj).getCategory();
}
};
UnaryFunction getItemPrice =
new UnaryFunction()
{
public Object evaluate (Object obj)
{
return new Double(((SETLItem)obj).getPrice());
}
};
Constant catA = new Constant("A");
Constant catB = new Constant("B");
Constant usd100 = new Constant(new Double(100));
Constant usd200 = new Constant(new Double(200));
BinaryPredicateUnaryPredicate belongsToCatA = new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsEqual(), getItemCat, catA));
BinaryPredicateUnaryPredicate belongsToCatB = new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsEqual(), getItemCat, catB));
BinaryPredicateUnaryPredicate moreThanUSD100 = new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsGreaterThanOrEqual(), getItemPrice, usd100));
BinaryPredicateUnaryPredicate moreThanUSD200 = new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsGreaterThanOrEqual(), getItemPrice, usd200));
UnaryOr isEligibleForDiscount = new UnaryOr(new UnaryAnd(belongsToCatA, moreThanUSD100),
new UnaryAnd(belongsToCatB, moreThanUSD200));
if (isEligibleForDiscount.test(item1))
System.out.println("Item #1 is eligible for discount!");
else
System.out.println("Item #1 is not eligible for discount!");
if (isEligibleForDiscount.test(item2))
System.out.println("Item #2 is eligible for discount!");
else
System.out.println("Item #2 is not eligible for discount!");
if (isEligibleForDiscount.test(item3))
System.out.println("Item #3 is eligible for discount!");
else
System.out.println("Item #3 is not eligible for discount!");
}
}
使用 ComparableComparator
清單 5 中可能注意到的第一件事是我利用了名為 isEqual (用於檢查所說商品的類別是否等於 “A”或者“B ”) 和 isGreaterThanOrEqual (用於檢查所述商品的定價是否大於或者等於指定值,對於 Category “A” 商品是 100,對於 Category “B” 商品是 200) 的內置二元謂詞仿函數。
您可能還記得在 清單 2 中,原來必須傳遞 PriceComparator 對象(封裝了比較邏輯)以使用 isGreaterThanOrEqual 仿函數進行價格比較。不過在清單 5 中,不顯式傳遞這個 Comparator 對象。如何做到不需要它? 技巧就是,在沒有指定該對象時, isGreaterThanOrEqual 仿函數(對這一點,甚至是 IsEqual 仿函數)使用默認的 ComparableComparator 。這個默認的 Comparator 假定兩個要比較的對象實現了 java.lang.Comparable 接口,並對第一個參數(在將它類型轉換為 Comparable 後)只是調用 compareTo 方法,傳遞第二個參數作為這個方法的參數。
通過將比較工作委派給這個對象本身,對於 String 比較(像對 item 目錄所做的)和 Double 比較(像對 item 價格所做的),可以原樣使用默認的 Comparator 。String 和 Double 都是實現了 Comparable 接口的默認 Java 類型。
將二元謂詞適配為一元
可能注意到的第二件事是我引入了一個名為 BinaryPredicateUnaryPredicate 的新仿函數。這個仿函數(類似於在 清單 4 中第一次遇到的 BinaryFunctionUnaryFunction 仿函數)將一個二元謂詞接口適配為一元接口。BinaryPredicateUnaryPredicate 仿函數可以認為是一個帶有一個參數的一元謂詞:它在內部用同一個參數的兩個副本計算包裝的二元謂詞。
isEligibleForDiscount 對象封裝了一個完整的業務規則。如您所見,它的構造方式 ── 即,通過將構造塊從下到上放到一起以構成更復雜的塊,再將它們放到一起以構成更復雜的塊,等等 ── 使它本身天然地成為某種“可視化的”規則構造器。最後的規則對象可以是任意復雜的表達式,它可以動態地構造,然後傳遞以計算底層業務規則。
對集合操作
GoF Iterator 模式提供了不公開其底層表示而訪問集合對象的元素的方法。這種方法背後的思路是迭代與數據結構不再相關聯(即它不是集合的一部分)。這種方式本身要使用一個表示集合中特定位置的對象,並用一個循環條件(在集合中增加其邏輯位置)以遍歷集合中所有元素。循環體中的其他指令可以檢查和/或操作集合中當前 Iterator 對象位置上的元素。在本例中,我們對迭代沒有什麼控制。(例如,必須調用多少次 next 、每次試圖訪問 next 元素時必須首先檢查超出范圍錯誤。) 此外,迭代器必須使用與別人一樣的公共接口訪問底層數據結構的“成員”,這使得訪問效率不高。這種迭代器常被稱為 “外部迭代器(External Iterator)”。
FP 對這個問題采取了一種非常不同的方式。集合類有一個高階函數,後者以一個仿函數作為參數並在內部對集合的每一個成員應用它。在本例中,因為迭代器共享了數據結構的實現,所以您可以完成控制迭代。此外,迭代很快,因為它可以直接訪問數據結構成員。這種迭代器常被稱為 內部迭代器(internal Iterator)。
Apache Functor 庫提供了各種非嚴格地基於 C++ 標准模板庫實現的內部 Iterator 。它提供了一個名為 Algorithms 的 實用工具類,這個類有一個名為 foreach 的方法。foreach 方法以一個 Iterator 對象和一個一元 Procedure 作為輸入,並對遍歷 Iterator 時遇到的每一個元素(元素本身是作為過程的一個參數傳遞的)運行一次。
使用內部迭代器
一個簡單的例子將可以說明外部和內部 Iterator 的不同。假定提供了一組 SETLItem 對象並要求累積列表中成本高於 200 美元的那些商品的定價。清單 6 展現了完成這一工作的代碼。
清單 6. 使用外部和內部迭代器
package com.infosys.setl.fp;
import java.util.*;
import org.apache.commons.functor.Algorithms;
import org.apache.commons.functor.UnaryFunction;
import org.apache.commons.functor.UnaryProcedure;
import org.apache.commons.functor.core.Constant;
import org.apache.commons.functor.core.collection.FilteredIterator;
import org.apache.commons.functor.core.comparator.IsGreaterThanOrEqual;
import org.apache.commons.functor.core.composite.UnaryCompositeBinaryPredicate;
public class TestE
{
public static void main(String[] args)
{
Vector items = new Vector();
for (int i=0; i<10; i++)
{
SETLItem item = new SETLItem();
if (i%2==0)
item.setPrice(101);
else
item.setPrice(i);
items.add(item);
}
TestE t = new TestE();
System.out.println("The sum calculated using External Iterator is: " +
t.calcPriceExternalIterator(items));
System.out.println("The sum calculated using Internal Iterator is: " +
t.calcPriceInternalIterator(items));
}
public int calcPriceExternalIterator(List items)
{
int runningSum = 0;
Iterator i = items.iterator();
while (i.hasNext())
{
int itemPrice = ((SETLItem)i.next()).getPrice();
if (itemPrice >= 100)
runningSum += itemPrice;
}
return runningSum;
}
public int calcPriceInternalIterator(List items)
{
Iterator i = items.iterator();
UnaryFunction getItemPrice =
new UnaryFunction()
{
public Object evaluate (Object obj)
{
return new Double(((SETLItem)obj).getPrice());
}
};
Constant usd100 = new Constant(new Double(100));
BinaryPredicateUnaryPredicate moreThanUSD100 = new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsGreaterThanOrEqual(), getItemPrice, usd100));
FilteredIterator fi = new FilteredIterator(i, moreThanUSD100);
Summer addPrice = new Summer();
Algorithms.foreach(fi, addPrice);
return addPrice.getSum();
}
static class Summer implements UnaryProcedure
{
private int sum=0;
public void run(Object obj)
{
sum += ((SETLItem)obj).getPrice();
}
public int getSum()
{
return sum;
}
}
}
在 main() 方法中,設置一個有 10 種商品的列表,其中奇數元素的價格為 101 美元。(在真實應用程序中,將使用調用 JDBC 獲得的 ResultSet 而不是本例中使用的 Vector 。)
然後用兩種不同的方法對列表執行所需要的操作: calcPriceExternalIterator 和 calcPriceInternalIterator 。正如其名字所表明的,前者基於 ExternalIterator 而後者基於 InternalIterator 。您將關心後一種方法,因為所有 Java 開發人員都應該熟悉前者。注意 InternalIterator 方法使用由 Apache Functor 庫提供的兩個結構。
第一個結構稱為 FilteredIterator 。它取一個迭代器加上一個一元 謂詞 作為輸入,並返回一個帶有所感興趣的屬性的 Iterator 。這個屬性給出了在遍歷 Iterator 時遇到的每一個滿足在 謂詞 中規定的條件的元素。(因此由 FilteredIterator 的一個實例返回的 Iterator 可以作為 FilteredIterator 的第二個實例的輸入傳遞,以此類推,以設置過濾器鏈,用於根據多種標准分步挑出元素。)在本例中,只對滿足 一元謂詞 “大於或等於 100 美元”規則的商品感興趣。這種規則是在名為 moreThanUSD100 的 BinaryPredicateUnaryPredicate 中規定的,我們在 清單 5中第一次遇到了它。
Apache Functor 庫提供的第二個結構是名為 Algorithms 的實用程序類,在 前面描述 過這個類。在這個例子中,名為 Summer 的一元過程只是包含傳遞給它的 SETLItem 實例的定價,並將它添加到(本地)運行的 total 變量上。這是一個實現了前面討論的內部迭代器概念的類。
使用仿函數進行集合操縱
我討論了用仿函數和高階函數編寫模塊的大量基礎知識。我將用最後一個展示如何用仿函數實現集合操縱操作的例子作為結束。
通常,有兩種描述集合成員關系的方式。第一種是完全列出集合中的所有元素。這是 Java 編程人員傳統上使用的機制 ── java.util.Set 接口提供了一個名為 add(Object) 的方法,如果作為參數傳遞到底層集合中的對象還未存在的話,該方法就添加它。
不過,當集合中的元素共享某些公共屬性時,通過聲明惟一地標識了集合中元素的屬性可以更高效地描述集合的成員關系。例如,後一種解決方案適合於集合成員的數量很大,以致不能在內存中顯式地維護一個集合實現(像前一種方式那樣)的情況。
在這種情況下,可以用一個一元 謂詞 表示這個集合。顯然,一個一元 謂詞 隱式定義了一組可以導致謂詞計算為 true的所有值(對象)。事實上,所有集合操作都可以用不同類型的謂詞組合來定義。清單 7 中展示了這一點:
清單 7.使用仿函數的集合操作
package com.infosys.setl.fp;
import org.apache.commons.functor.UnaryPredicate;
import org.apache.commons.functor.core.composite.UnaryAnd;
import org.apache.commons.functor.core.composite.UnaryNot;
import org.apache.commons.functor.core.composite.UnaryOr;
public class SetOps
{
public static UnaryPredicate union(UnaryPredicate up1, UnaryPredicate up2)
{
return new UnaryOr(up1, up2);
}
public static UnaryPredicate intersection(UnaryPredicate up1, UnaryPredicate up2)
{
return new UnaryAnd(up1, up2);
}
public static UnaryPredicate difference(UnaryPredicate up1, UnaryPredicate up2)
{
return new UnaryAnd(up1, new UnaryNot(up2));
}
public static UnaryPredicate symmetricDifference(UnaryPredicate up1,
UnaryPredicate up2)
{
return difference(union(up1, up2), intersection(up1, up2));
}
}
用一元 謂詞 來描述集合 並集(union) 和 交集(intersection) 操作的定義應當是明確的:如果一個對象至少使指示兩個集合的兩個一元 謂詞 中的一個計算為 true,那麼這個對象就屬於兩個集合的並集(邏輯 Or );如果它使兩個一元 謂詞 都計算為 true,那麼它就屬於兩個集合的交集(邏輯 And )。
兩個集合的差在數學上定義為屬於第一個集合但是不屬於第二個集合一組元素。根據這個定義,靜態方法 difference 也容易理解。
最後,兩個集合的對稱差(symmetric difference)定義為只屬於兩個集合中的一個(或者兩個都不屬於)的所有元素。這可以取兩個集合的並集,然後從中刪除屬於兩個集合的交集的元素得到。就是說,它是對原來的集合(在這裡是一元 謂詞 )使用 union 和 intersection 操作分別得到的兩個集合的 difference 操作。後一個定義解釋了為什麼用前三種方法作為第四個方法中的構建塊。
結束語
模塊化是任何平台上高生產率和成功的編程的關鍵,這一點早已被認識到了。Java 開發人員的問題是模塊化編程不僅是將問題分解,它還要求能將小的解決方案粘接到一起,成為一個有效的整體。由於這種類型的開發繼承了函數編程范型,在 Java 平台上開發模塊化代碼時使用函數編程技術應是很自然的事。
在本文中,我介紹了兩種函數編程技術,它們可以容易地結合到 Java 開發實踐中。正如從這裡看到的,閉包和高階函數對於 Java 開發人員來說並不是完全陌生的,它們可以有效地結合以創建一些非常有用的模塊化解決方案。
我希望本文提供了在 Java 代碼中結合閉包和高階函數的很好基礎,並使您見識到了函數編程的優美和效率。