841
汽車大全
Java FP: Java中函數式編程的謂詞函數(Predicates)第一部分
你一直在聽說函數式編程將稱霸整個編程屆,而自己仍然沉浸在普通的Java裏?請不要擔心,因為你已經在日常Java代碼中加入了函數式編程的特性。此外,函數式編程很有趣,能夠幫你節省多行代碼並且降低錯誤率。
什麼是謂詞函數?
許久之前,那時我還在用Java 1.4進行編碼,當我第一次發現Apache Commons Collections,便愛上了謂詞函數。Apache Commons Collections裏的謂詞函數僅僅隻是一個隻有一個方法的接口:
evaluate(Object object): boolean
這就是謂詞函數,輸入一個對象,返回true或者false。最近誕生了類似Apache Commons Collections的持有Apache 2.0許可的Google Guava。在Google Guava中,定義了Predicate接口,該接口包含一個帶有泛型參數的方法:
apply(T input): boolean
如果想在程序中使用謂詞函數,隻需要利用自己的邏輯實現該接口即可。
一個簡單的例子
先舉一個例子,假設你有一個訂單列表,每個訂單用PurchaseOrder表示,PurchaseOrder中包含日期,顧客和狀態。不同的用例會要求你有不同的輸出,比如獲取某個顧客所有、等待發貨、已發貨、已交付或者過去一個小時內完成的訂單。當然你可以在循環中使用if判斷實現這些功能:
//List<PurchaseOrder> orders... public List<PurchaseOrder> listOrdersByCustomer(Customer customer) { final List<PurchaseOrder> selection = new ArrayList<PurchaseOrder>(); for (PurchaseOrder order : orders) { if (order.getCustomer().equals(customer)) { selection.add(order); } } return selection; }
以上是獲取某個顧客所有訂單的代碼。不同的功能需要編寫多個類似的循環:
public List<PurchaseOrder> listRecentOrders(Date fromDate) { final List<PurchaseOrder> selection = new ArrayList<PurchaseOrder>(); for (PurchaseOrder order : orders) { if (order.getDate().after(fromDate)) { selection.add(order); } } return selection; }
這些重複代碼非常明顯:除了if的判斷條件之外沒有任何差異(譯者注:方法參數可歸為判斷條件)。采用謂詞函數的思想在於,利用傳入到函數內的謂詞的調用替代if語句塊裏的硬編碼的判斷條件。這意味著,你隻需編寫一遍帶有謂詞函數作為參數的方法,就可以覆蓋所有的甚至你還不知道的測試用例:
public List<PurchaseOrder> listOrders(Predicate<PurchaseOrder> condition) { final List<PurchaseOrder> selection = new ArrayList<PurchaseOrder>(); for (PurchaseOrder order : orders) { if (condition.apply(order)) { selection.add(order); } } return selection; }
如果需要考慮到複用,可以把謂詞函數聲明成一個單獨的類,否則可以把謂詞聲明成匿名類:
final Customer customer = new Customer("BruceWaineCorp"); final Predicate<PurchaseOrder> condition = new Predicate<PurchaseOrder>() { public boolean apply(PurchaseOrder order) { return order.getCustomer().equals(customer); } };
如果你的使用過真正意義上的函數式語言(Scala, Clojure, Haskell等)的朋友看到這些代碼,可能會覺得在處理通用功能時代碼顯得非常冗餘。然而我們已經習慣於Java冗長的語法,並且我們有強大的工具(自動補齊、重構)幫助我們適應它,這使得我們的Java項目無法一夜之間轉變成其他語言的項目。
謂詞函數是集合類的好朋友
回到之前的例子,我們寫了一個覆蓋了所有用例的循環,我們為共性的抽離感到開心,但是你的朋友依然會嘲笑你。幸運的是,Apache或者Google的API都提供了你想要的東西,還特別提供了一個類似java.util.Collections的命名為Collections2的類(名字不是很新穎)。
這個類提供給了與我們先前編寫的代碼功能類似的filter()函數,所以我們可以把方法重構成無循環的版本:
public Collection<PurchaseOrder> selectOrders(Predicate<PurchaseOrder> condition) { return Collections2.filter(orders, condition); }
實際上,這個方法返回了一個過濾後的視圖:
返回的集合是未經過濾的集合(輸入的集合)的真實縮影(譯者注:先前版本的函數返回的集合是輸入集合的一個子集的拷貝),更改其中一個集合會影響另一個集合。
這意味著這種方式將使用更少的內存,因為不會把原始集合的內容拷貝到返回的集合中。
在一個類似的場景中,我們可以要求返回在給定的迭代器之上過濾好的隻符合謂詞函數的元素的迭代器(裝飾模式)。
Iterator filteredIterator = Iterators.filter(unfilteredIterator, condition);
從Java 5開始,Iterable接口和循環使用起來非常方便,所以我們更傾向於使用以下寫法:
public Iterable<PurchaseOrder> selectOrders(Predicate<PurchaseOrder> condition) { return Iterables.filter(orders, condition); } // you can directly use it in a foreach loop, and it reads well: for (PurchaseOrder order : orders.selectOrders(condition)) { //... }
現成的謂詞函數
為了使用謂詞,我們必須聲明自己的謂詞接口,或者為應用程序中使用到的謂詞參數都聲明一個類。這是可行的,然而從類似Guava以及Commons的API中使用標準謂詞接口的好處是:你可以結合這類API提供的大量優秀組件實現你自己的謂詞函數。
如果你需要的是判斷一個對象是否為空或者不為空的條件,你不需要自己實現一個謂詞函數,隻需要使用現成的謂詞就可以了:
// gives you a predicate that checks if an integer is zeroPredicate <Integer> isZero = Predicates.equalTo(0); // gives a predicate that checks for non null objects Predicate<String> isNotNull = Predicates.notNull(); // gives a predicate that checks for objects that are instanceof the given Class Predicate<Object> isString = Predicates.instanceOf(String.class);
對於給定的謂詞,你可以反轉它(返回相反的返回值,比如true變成false):
Predicates.not(predicate);
利用AND,OR操作結合多個謂詞:
Predicates.and(predicate1, predicate2); Predicates.or(predicate1, predicate2); // gives you a predicate that checks for either zero or null Predicate<Integer> isNullOrZero = Predicates.or(isZero, Predicates.isNull());
當然你也可以擁有返回固定值(true或者false)的特殊謂詞(譯者注:隻返回true即為邏輯學中的永真式,反之為永假式)。這些謂詞非常有用,我們可以在之後的例子中證明:
Predicates.alwaysTrue(); Predicates.alwaysFalse();
如何定位謂詞
起先,我經常編寫匿名的謂詞類,後來這些謂詞總是頻繁使用,所以我會將匿名的謂詞升級成實體類、內部類等。
順便提一下,如何定位謂詞呢?請參考Robert C. Martin的文章 Common Closure Principle (CCP)中提到的一段話 :
一起變化的類,屬於一個整體。
因為謂詞總是對一個特定類型的對象進行操作,我喜歡將謂詞重新定位為謂詞操作的參數的類型。比如,類CustomerOrderPredicate,PendingOrderPredicate 和RecentOrderPredicate 應該被防止在同一個包下或者子包下(如果你有很多包),而不是把代碼寫到這些謂詞所操作的主體PurchaseOrder裏。另一個選擇是,將謂詞聲明成它們要操作的主體類型的內部類。顯然,謂詞與主體對象是非常耦合的。
資源
這裏有本篇文章的例子源代碼:cyriux_predicates_part1 (zip)
在下一小節,我們著重觀察謂詞函數如何簡化測試、謂詞如何與域驅動設計裏的標準相聯係,以及一些能讓你高效利用謂詞函數的額外知識。
參考文獻
A touch of functional style in plain Java with predicates – Part 1 from our JCG partner Cyrille Martraire at the Cyrille Martraire’s blog
最後更新:2017-05-23 10:31:58