如果您從事大型企業項目開發
您就會熟悉編寫模塊化代碼的好處
良構的
模塊化的代碼更容易編寫
調試
理解和重用
Java 開發人員的問題是
函數編程范型長期以來只是通過像 Haskell
Scheme
Erlang 和 Lisp 這樣的特殊語言實現的
在本文中
作者 Abhijit Belapurkar 展示了
如何使用像閉包(closure)和 高階函數(higher order function)這樣的函數編程結構
在 Java 語言中編寫良構的
模塊化的代碼
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 代碼單元在需要時由一個 高階函數 執行
例如
清單
中的代碼在一個類型為 java
lang
Runnable 的對象中封閉了一個方法調用
清單
隱藏的閉包
Runnable worker = new Runnable()
{
public void run()
{
parseData();
}
};
方法 parseData 確實 封閉(因而有了名字
閉包
)在 Runnable 對象的實例 worker 中
它可以像數據一樣在方法之間傳遞
並可以在任何時間通過發送消息(稱為 run ) 給 worker 對象而執行
更多的例子 另一個在面向對象世界中使用閉包和高階函數的例子是 Visitor 模式
如果還不熟悉這種模式
請參閱 參考資料以了解更多有關它的內容
基本上
Visitor 模式展現一個稱為 Visitor 的參與者
該參與者的實例由一個復合對象(或者數據結構)接收
並應用到這個數據結構的每一個構成節點
Visitor 對象實質上 封閉 了處理節點/元素的邏輯
使用數據結構的 accept (visitor) 方法作為應用邏輯的高階函數
通過使用適當不同的 Visitor 對象(即閉包)
可以對數據結構的元素應用完全不同的處理邏輯
與此類似
可以向不同的高階函數傳遞同樣的閉包
以用另一種方法處理數據結構(例如
這個新的高階函數可以實現不同邏輯
用於遍歷所有構成元素)
類 java
utils
Collections 提供了另一個例子
這個類在版本
以後成為了 Java
SDK 的一部分
它提供的一種實用程序方法是對在 java
util
List 中包含的元素排序
不過
它使調用者可以將排序列表元素的邏輯封裝到一個類型為 java
util
Comparator 的對象中
其中 Comparator 對象作為第二個參數傳遞給排序方法
在內部
sort 方法的引用實現基於 合並
排序(merge
sort) 算法
它通過對順序中相鄰的對象調用 Comparator 對象的 compare 方法遍歷列表(list)中的元素
在這種情況下
Comparator 是閉包
而 Collections
sort 方法是高階函數
這種方式的好處是明顯的
可以根據在列表中包含的不同對象類型傳遞不同的 Comparator 對象(因為如何比較列表中任意兩個對象的邏輯安全地封裝在 Comparator 對象中)
與此類似
sort 方法的另一種實現可以使用完全不同的算法(比如說
快速排序(quick
sort)) 並仍然重復使用同樣的 Comparator 對象
將它作為基本函數應用到列表中兩個元素的某種組合
創建閉包 廣意地說
有兩種生成閉包的技術
使用閉包的代碼可以等效地使用這兩種技術
創建閉包後
可以以統一的方式傳遞它
也可以向它發送消息以讓它執行其封裝的邏輯
因此
技術的選擇是偏好的問題
在某些情況下也與環境有關
最後一次要求下載!
從這以後的討論將結合基於 Apache Commons Functor 庫的例子
如果您還沒有下載這個庫
應當 現在就下載
我將假定您可以訪問 Javadocs 所帶的 Apache 庫
因此對單獨的庫類不再做過多的說明
在第一種技術 表達式特化(expression specialization)中
由基礎設施為閉包提供一個一般性的接口
通過編寫這個接口的特定實現創建具體的閉包
在第二種技術 表達式合成(expression composition) 中
基礎設施提供實現了基本一元/二元/三元/
/n 元操作(比如一元操作符 not 和二元操作符 and / or )的具體幫助類
在這裡
新的閉包由這些基本構建塊的任意組合創建而成
我將在下面的幾節中詳細討論這兩種技術
表達式特化 假定您在編寫一個在線商店的應用程序
商店中可提供的商品用類 SETLItem 表示
每一件商品都有相關的標簽價格
SETLItem 類提供了名為 getPrice 的方法
對商品實例調用這個方法時
會返回該商品的標簽價格
如何檢查 item
的成本是否不低於 item
呢?在 Java 語言中
一般要編寫一個像這樣的表達式
assert(item
getPrice() >= item
getPrice());
像這樣的表達式稱為 二元謂詞(binary predicate)
二元是因為它取兩個參數
而 謂詞 是因為它用這兩個參數做一些事情並生成布爾結果
不過要注意
只能在執行流程中執行上面的表達式
它的輸出取決於 item
和 item
在特定瞬間的值
從函數編程的角度看
這個表達式還不是一般性的邏輯
就是說
它不能不管執行控制的當前位置而隨心所欲地傳遞並執行
為了使二元謂詞發揮作用
必須將它封裝到一個對象中
通過 特化(specializing) 一個稱為 BinaryPredicate 的接口做到這一點
這個接口是由 Apache Functor 庫提供的
如清單
所示
清單
表達式特化方法
package sys
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 sys
setl
fp;
import java
util
Comparator;
public class PriceComparator implements Comparator
{
public int compare (Object o
Object o
)
{
return (((SETLItem)o
)
getPrice()
((SETLItem)o
)
getPrice());
}
}
package sys
setl
fp;
import mons
functor
*;
import parator
*;
import java
util
*;
public class TestA
{
public static void main(String[] args)
{
try
{
Comparator pc = new PriceComparator();
BinaryPredicate bp = new IsGreaterThanOrEqual(pc);
SETLItem item
= new SETLItem();
item
setPrice(
);
SETLItem item
= new SETLItem();
item
setPrice(
);
if (bp
test(item
item
))
System
out
println(
Item
costs more than Item
!
);
else
System
out
println(
Item
costs more than Item
!
);
SETLItem item
= new SETLItem();
item
setPrice(
);
if (bp
test(item
item
))
System
out
println(
Item
costs more than Item
!
);
else
System
out
println(
Item
costs more than Item
!
);
}
catch (Exception e)
{
e
printStackTrace();
}
}
}
BinaryPredicate 接口以由 Apache Functor 庫提供的 IsGreaterThanOrEqual 類的形式特化
PriceComparator 類實現了 java
util
Comparator 接口
並被作為輸入傳遞給 IsGreaterThanOrEqual 類
收到一個 test 消息時
IsGreaterThanOrEqual 類自動調用 PriceComparator 類的 compare 方法
compare 方法預期接收兩個 SETLItem 對象
相應地它返回兩個商品的價格差
compare 方法返回的正值表明 item
的成本不低於 item
初看之下
對一個相當基本的操作要做很多的工作
那它有什麼好處呢?特化 BinaryPredicate 接口(而不是編寫 Java 比較表達式) 使您無論在何時何地都可以比較任意兩個商品的價格
可以將 bp 對象作為數據傳遞並向它發送消息
以在任何時候
使用這兩個參數的任何值來執行它(稱為 test )
表達式合成 表達式合成是得到同樣結果的一種稍有不同的技術
考慮計算特定 SETLItem 的淨價問題
要考慮當前折扣和銷售稅率
清單
列出了這個問題基於仿函數的解決方式
清單
表達式合成方法
package sys
setl
fp;
import mons
functor
BinaryFunction;
import mons
functor
UnaryFunction;
import mons
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 sys
setl
fp;
import mons
functor
*;
import posite
*;
import mons
functor
adapter
*;
import mons
functor
UnaryFunction;
public class TestB
{
public static void main(String[] args)
{
try
{
double discountRate =
;
double taxRate=
;
SETLItem item = new SETLItem();
item
setPrice(
);
UnaryFunction calcDiscountedPrice =
new RightBoundFunction(new Multiply()
new Double(
discountRate));
UnaryFunction calcTax =
new RightBoundFunction(new Multiply()
new Double(
+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
清單
顯示了名為 Multiply 的特化的 BinaryFunction
它取兩個 Double 作為輸入並返回一個新的
由前兩個雙精度值相乘而得到 Double
在 calcDiscountedRate 中實例化了一個新的 RightBoundFunction
它通過用 (
discountRate) 作為其常量第二參數
將二元 Multiply 函數適配為一元接口
結果
可以用一個 Double 參數向 calcDiscountRate 發送一個名為 evaluate 的消息
在內部
輸入參數 Double 乘以 calcDiscountRate 對象本身包含的常量值
與此類似
在 calcTaxRate 中實例化了一個新的 RightBoundFunction
它通過用 (
+ taxRate) 作為其第二個常量參數將二元 Multiply 函數適配為一元接口
結果
可以用一個 Double 參數向 calcTaxRate 發送一個名為 evaluate 的消息
在內部
輸入參數 Double 乘以 calcTaxRate 對象本身包含的常量值
這種將多參數的函數重新編寫為一個參數的函數的合成(composition)技術也稱為 currying
合成魔術在最後的時候就發揮作用了
實質上
計算對象淨價的算法是首先計算折扣價格(使用 calcDiscountRate 仿函數)
然後通過在上面加上銷售稅(用 calcSalesTax 仿函數)計算淨價
就是說
需要組成一個函數
在內部調用第一個仿函數並將計算的輸出流作為第二個仿函數的計算的輸入
Apache 庫提供了用於這種目的的一個內置仿函數
稱為 CompositeUnaryFunction
在清單
中
CompositeUnaryFunction 實例化為變量 calcNetPrice
作為 calcDiscountRate 和 calcSalesTax 仿函數的合成
與前面一樣
將可以向其他函數傳遞這個對象
其他函數也可以通過向它發送一個包含商品參數的 evaluate 消息要求它計算這種商品的淨價
一元與二元合成 在清單
中
您看到了 一元合成的一個例子
其中一個一元仿函數的結果是另一個的輸入
另一種合成稱為 二元合成
作為 evaluate 消息的一部分
需要傳遞兩個一元仿函數的結果作為二元仿函數的參數
清單
是說明二元合成的必要性和風格的一個例子
假定希望保證商店可以給出的最大折扣有一個最大限度
因此
必須將作為 calcDiscount 仿函數計算結果得到的折扣量與 cap 值進行比較
並取最小值作為計算出的折扣價格
折扣價格是通過用標簽價減去實際的折扣而計算的
清單
二元合成
package sys
setl
fp;
import mons
functor
BinaryFunction;
public class Subtract implements BinaryFunction
{
public Object evaluate(Object left
Object right)
{
return new Double(((Double)left)
doubleValue()
((Double)right)
doubleValue());
}
}
package sys
setl
fp;
import mons
functor
BinaryFunction;
import mons
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 sys
setl
fp;
import mons
functor
*;
import posite
*;
import mons
functor
adapter
*;
import mons
functor
UnaryFunction;
import re
Constant;
import parator
Min;
public class TestC
{
public static void main(String[] args)
{
double discountRate =
;
double taxRate=
;
double maxDiscount =
;
SETLItem item = new SETLItem();
item
setPrice(
);
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(
+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 仿函數取一個二元函數和兩個一元函數作為輸入
首先計算後兩個函數
它們的輸出作為輸入傳遞給二元函數
在清單
中對二元合成使用這個仿函數兩次
Constant 仿函數的計算總是返回一個常量值(即在其構造時輸入的值)
不管以後任何計算消息中傳遞給它的參數是什麼值
在清單
中
變量 cap 的類型為 Constant 並總是返回最大折扣數量
Identity 仿函數只是返回作為 evaluate 消息的輸入參數傳遞給它的這個對象作為輸出
清單
顯示 Identity 仿函數的一個實例
該仿函數是在創建 calcDiscountedPrice 時作為一個一元仿函數創建和傳遞的
同時在清單
中
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 一元仿函數(也在清單
中遇到)和 calcDiscountedPriceAsUnary (在前面一段中描述)創建 CompositeUnaryFunction
而以一個一元合成完成
這樣得到的 calcNetPrice 變為接收一個 evaluate 消息和一個參數(所討論商品的標簽價格)
而在內部
首先用這個參數計算 calcDiscountedPriceAsUnary 仿函數
然後用前一個計算的輸出作為參數計算 calcTax 仿函數
使用閉包實現業務規則 Apache Library 提供了各種不同的內置一元和二元仿函數
它使得將業務邏輯編寫為可以傳遞並且可以用不同的參數在不同的位置執行的對象變得非常容易
在後面幾節中
我將使用一個簡單的例子展示對一個類似問題的函數編程方式
假定一個特定的商品是否可以有折扣取決於該商品的類別和定價
具體說
只有 Category
A
中定價高於
美元和 Category
B
中定價高於
美元的商品才有資格打折
清單
中的代碼顯示了一個名為 isEligibleForDiscount 的業務規則對象 ( UnaryPredicate )
如果用一個 item 對象作為參數發送 evaluate 消息
將返回一個表明是否可以對它打折的 Boolean
清單
一個函數業務規則對象
package sys
setl
fp;
import mons
functor
BinaryPredicate;
import mons
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 sys
setl
fp;
import mons
functor
*;
import posite
*;
import mons
functor
adapter
*;
import mons
functor
UnaryFunction;
import re
Constant;
import re
IsEqual;
import parator
IsGreaterThanOrEqual;
import parator
Min;
import re
Identity;
public class TestD
{
public static void main(String[] args)
{
SETLItem item
= new SETLItem();
item
setPrice(
);
item
setCategory(
A
);
SETLItem item
= new SETLItem();
item
setPrice(
);
item
setCategory(
A
);
SETLItem item
= new SETLItem();
item
setPrice(
);
item
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 usd
= new Constant(new Double(
));
Constant usd
= new Constant(new Double(
));
BinaryPredicateUnaryPredicate belongsToCatA = new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsEqual()
getItemCat
catA));
BinaryPredicateUnaryPredicate belongsToCatB = new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsEqual()
getItemCat
catB));
BinaryPredicateUnaryPredicate moreThanUSD
= new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsGreaterThanOrEqual()
getItemPrice
usd
));
BinaryPredicateUnaryPredicate moreThanUSD
= new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsGreaterThanOrEqual()
getItemPrice
usd
));
UnaryOr isEligibleForDiscount = new UnaryOr(new UnaryAnd(belongsToCatA
moreThanUSD
)
new UnaryAnd(belongsToCatB
moreThanUSD
));
if (isEligibleForDiscount
test(item
))
System
out
println(
Item #
is eligible for discount!
);
else
System
out
println(
Item #
is not eligible for discount!
);
if (isEligibleForDiscount
test(item
))
System
out
println(
Item #
is eligible for discount!
);
else
System
out
println(
Item #
is not eligible for discount!
);
if (isEligibleForDiscount
test(item
))
System
out
println(
Item #
is eligible for discount!
);
else
System
out
println(
Item #
is not eligible for discount!
);
}
}
使用 ComparableComparator
清單
中可能注意到的第一件事是我利用了名為 isEqual (用於檢查所說商品的類別是否等於
A
或者
B
) 和 isGreaterThanOrEqual (用於檢查所述商品的定價是否大於或者等於指定值
對於 Category
A
商品是
對於 Category
B
商品是
) 的內置二元謂詞仿函數
您可能還記得在 清單
中
原來必須傳遞 PriceComparator 對象(封裝了比較邏輯)以使用 isGreaterThanOrEqual 仿函數進行價格比較
不過在清單
中
不顯式傳遞這個 Comparator 對象
如何做到不需要它? 技巧就是
在沒有指定該對象時
isGreaterThanOrEqual 仿函數(對這一點
甚至是 IsEqual 仿函數)使用默認的 ComparableComparator
這個默認的 Comparator 假定兩個要比較的對象實現了 java
lang
Comparable 接口
並對第一個參數(在將它類型轉換為 Comparable 後)只是調用 compareTo 方法
傳遞第二個參數作為這個方法的參數
通過將比較工作委派給這個對象本身
對於 String 比較(像對 item 目錄所做的)和 Double 比較(像對 item 價格所做的)
可以原樣使用默認的 Comparator
String 和 Double 都是實現了 Comparable 接口的默認 Java 類型
將二元謂詞適配為一元 可能注意到的第二件事是我引入了一個名為 BinaryPredicateUnaryPredicate 的新仿函數
這個仿函數(類似於在 清單
中第一次遇到的 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 對象並要求累積列表中成本高於
美元的那些商品的定價
清單
展現了完成這一工作的代碼
清單
使用外部和內部迭代器
package sys
setl
fp;
import java
util
*;
import mons
functor
Algorithms;
import mons
functor
UnaryFunction;
import mons
functor
UnaryProcedure;
import re
Constant;
import llection
FilteredIterator;
import parator
IsGreaterThanOrEqual;
import posite
UnaryCompositeBinaryPredicate;
public class TestE
{
public static void main(String[] args)
{
Vector items = new Vector();
for (int i=
; i<
; i++)
{
SETLItem item = new SETLItem();
if (i%
==
)
item
setPrice(
);
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 =
;
Iterator i = erator();
while (i
hasNext())
{
int itemPrice = ((SETLItem)i
next())
getPrice();
if (itemPrice >=
)
runningSum += itemPrice;
}
return runningSum;
}
public int calcPriceInternalIterator(List items)
{
Iterator i = erator();
UnaryFunction getItemPrice =
new UnaryFunction()
{
public Object evaluate (Object obj)
{
return new Double(((SETLItem)obj)
getPrice());
}
};
Constant usd
= new Constant(new Double(
));
BinaryPredicateUnaryPredicate moreThanUSD
= new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsGreaterThanOrEqual()
getItemPrice
usd
));
FilteredIterator fi = new FilteredIterator(i
moreThanUSD
);
Summer addPrice = new Summer();
Algorithms
foreach(fi
addPrice);
return addPrice
getSum();
}
static class Summer implements UnaryProcedure
{
private int sum=
;
public void run(Object obj)
{
sum += ((SETLItem)obj)
getPrice();
}
public int getSum()
{
return sum;
}
}
}
在 main() 方法中
設置一個有
種商品的列表
其中奇數元素的價格為
美元
(在真實應用程序中
將使用調用 JDBC 獲得的 ResultSet 而不是本例中使用的 Vector
)
然後用兩種不同的方法對列表執行所需要的操作
calcPriceExternalIterator 和 calcPriceInternalIterator
正如其名字所表明的
前者基於 ExternalIterator 而後者基於 InternalIterator
您將關心後一種方法
因為所有 Java 開發人員都應該熟悉前者
注意 InternalIterator 方法使用由 Apache Functor 庫提供的兩個結構
第一個結構稱為 FilteredIterator
它取一個迭代器加上一個一元 謂詞 作為輸入
並返回一個帶有所感興趣的屬性的 Iterator
這個屬性給出了在遍歷 Iterator 時遇到的每一個滿足在 謂詞 中規定的條件的元素
(因此由 FilteredIterator 的一個實例返回的 Iterator 可以作為 FilteredIterator 的第二個實例的輸入傳遞
以此類推
以設置過濾器鏈
用於根據多種標准分步挑出元素
)在本例中
只對滿足 一元謂詞
大於或等於
美元
規則的商品感興趣
這種規則是在名為 moreThanUSD
的 BinaryPredicateUnaryPredicate 中規定的
我們在 清單
中第一次遇到了它
Apache Functor 庫提供的第二個結構是名為 Algorithms 的實用程序類
在 前面描述 過這個類
在這個例子中
名為 Summer 的一元過程只是包含傳遞給它的 SETLItem 實例的定價
並將它添加到(本地)運行的 total 變量上
這是一個實現了前面討論的內部迭代器概念的類
使用仿函數進行集合操縱 我討論了用仿函數和高階函數編寫模塊的大量基礎知識
我將用最後一個展示如何用仿函數實現集合操縱操作的例子作為結束
通常
有兩種描述集合成員關系的方式
第一種是完全列出集合中的所有元素
這是 Java 編程人員傳統上使用的機制 ── java
util
Set 接口提供了一個名為 add(Object) 的方法
如果作為參數傳遞到底層集合中的對象還未存在的話
該方法就添加它
不過
當集合中的元素共享某些公共屬性時
通過聲明惟一地標識了集合中元素的屬性可以更高效地描述集合的成員關系
例如
後一種解決方案適合於集合成員的數量很大
以致不能在內存中顯式地維護一個集合實現(像前一種方式那樣)的情況
在這種情況下
可以用一個一元 謂詞 表示這個集合
顯然
一個一元 謂詞 隱式定義了一組可以導致謂詞計算為 true的所有值(對象)
事實上
所有集合操作都可以用不同類型的謂詞組合來定義
清單
中展示了這一點
清單
使用仿函數的集合操作
package sys
setl
fp;
import mons
functor
UnaryPredicate;
import posite
UnaryAnd;
import posite
UnaryNot;
import posite
UnaryOr;
public class SetOps
{
public static UnaryPredicate union(UnaryPredicate up
UnaryPredicate up
)
{
return new UnaryOr(up
up
);
}
public static UnaryPredicate intersection(UnaryPredicate up
UnaryPredicate up
)
{
return new UnaryAnd(up
up
);
}
public static UnaryPredicate difference(UnaryPredicate up
UnaryPredicate up
)
{
return new UnaryAnd(up
new UnaryNot(up
));
}
public static UnaryPredicate symmetricDifference(UnaryPredicate up
UnaryPredicate up
)
{
return difference(union(up
up
)
intersection(up
up
));
}
}
用一元 謂詞 來描述集合 並集(union) 和 交集(intersection) 操作的定義應當是明確的
如果一個對象至少使指示兩個集合的兩個一元 謂詞 中的一個計算為 true
那麼這個對象就屬於兩個集合的並集(邏輯 Or )
如果它使兩個一元 謂詞 都計算為 true
那麼它就屬於兩個集合的交集(邏輯 And )
兩個集合的差在數學上定義為屬於第一個集合但是不屬於第二個集合一組元素
根據這個定義
靜態方法 difference 也容易理解
最後
兩個集合的對稱差(symmetric difference)定義為只屬於兩個集合中的一個(或者兩個都不屬於)的所有元素
這可以取兩個集合的並集
然後從中刪除屬於兩個集合的交集的元素得到
就是說
它是對原來的集合(在這裡是一元 謂詞 )使用 union 和 intersection 操作分別得到的兩個集合的 difference 操作
後一個定義解釋了為什麼用前三種方法作為第四個方法中的構建塊
結束語 模塊化是任何平台上高生產率和成功的編程的關鍵
這一點早已被認識到了
Java 開發人員的問題是模塊化編程不僅是將問題分解
它還要求能將小的解決方案粘接到一起
成為一個有效的整體
由於這種類型的開發繼承了函數編程范型
在 Java 平台上開發模塊化代碼時使用函數編程技術應是很自然的事
在本文中
我介紹了兩種函數編程技術
它們可以容易地結合到 Java 開發實踐中
正如從這裡看到的
閉包和高階函數對於 Java 開發人員來說並不是完全陌生的
它們可以有效地結合以創建一些非常有用的模塊化解決方案
From:http://tw.wingwit.com/Article/program/Java/hx/201311/25697.html