本章內容
應對不斷變化的需求
行為參數化
匿名類
Lambda表達式預覽
真實示例:
Comparator
、Runnable
和GUI
在軟件工程中,一個眾所周知的問題就是,不管你做什麼,用戶的需求肯定會變。比方說,有個應用程序是幫助農民瞭解自己的庫存的。這位農民可能想有一個查找庫存中所有綠色蘋果的功能。但到了第二天,他可能會告訴你:“其實我還想找出所有重量超過150克的蘋果。”又過了兩天,農民又跑回來補充道:“要是我可以找出所有既是綠色,重量也超過150克的蘋果,那就太棒了。”你要如何應對這樣不斷變化的需求?理想的狀態下,應該把你的工作量降到最少。此外,類似的新功能實現起來還應該很簡單,而且易於長期維護。
行為參數化就是可以幫助你處理頻繁變更的需求的一種軟件開發模式。一言以蔽之,它意味著拿出一個代碼塊,把它準備好卻不去執行它。這個代碼塊以後可以被你程序的其他部分調用,這意味著你可以推遲這塊代碼的執行。例如,你可以將代碼塊作為參數傳遞給另一個方法,稍後再去執行它。這樣,這個方法的行為就基於那塊代碼被參數化了。例如,如果你要處理一個集合,可能會寫一個方法:
-
可以對列表中的每個元素做“某件事”
-
可以在列表處理完後做“另一件事”
-
遇到錯誤時可以做“另外一件事”
行為參數化說的就是這個。打個比方吧:你的室友知道怎麼開車去超市,再開回家。於是你可以告訴他去買一些東西,比如麵包、奶酪、葡萄酒什麼的。這相當於調用一個goAndBuy
方法,把購物單作為參數。然而,有一天你在上班,你需要他去做一件他從來沒有做過的事情:從郵局取一個包裹。現在你就需要傳遞給他一系列指示了:去郵局,使用單號,和工作人員說明情況,取走包裹。你可以把這些指示用電子郵件發給他,當他收到之後就可以按照指示行事了。你現在做的事情就更高級一些了,相當於一個方法:go
,它可以接受不同的新行為作為參數,然後去執行。
這一章首先會給你講解一個例子,說明如何對你的代碼加以改進,從而更靈活地適應不斷變化的需求。在此基礎之上,我們將展示如何把行為參數化用在幾個真實的例子上。比如,你可能已經用過了行為參數化模式——使用Java API中現有的類和接口,對List
進行排序,篩選文件名,或告訴一個Thread
去執行代碼塊,甚或是處理GUI事件。你很快會發現,在Java中使用這種模式十分囉嗦。Java 8中的Lambda解決了代碼囉嗦的問題。我們會在第3章中向你展示如何構建Lambda表達式、其使用場合,以及如何利用它讓代碼更簡潔。
2.1 應對不斷變化的需求
編寫能夠應對變化的需求的代碼並不容易。讓我們來看一個例子,我們會逐步改進這個例子,以展示一些讓代碼更靈活的最佳做法。就農場庫存程序而言,你必須實現一個從列表中篩選綠蘋果的功能。聽起來很簡單吧?
2.1.1 初試牛刀:篩選綠蘋果
第一個解決方案可能是下面這樣的:
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>; ←─累積蘋果的列表
for(Apple apple: inventory){
if( "green".equals(apple.getColor ) { ←─僅僅選出綠蘋果
result.add(apple);
}
}
return result;
}
突出顯示的行就是篩選綠蘋果所需的條件。但是現在農民改主意了,他還想要篩選紅蘋果。你該怎麼做呢?簡單的解決辦法就是複製這個方法,把名字改成filterRedApples
,然後更改if
條件來匹配紅蘋果。然而,要是農民想要篩選多種顏色:淺綠色、暗紅色、黃色等,這種方法就應付不了了。一個良好的原則是在編寫類似的代碼之後,嘗試將其抽像化。
2.1.2 再展身手:把顏色作為參數
一種做法是給方法加一個參數,把顏色變成參數,這樣就能靈活地適應變化了:
public static List<Apple> filterApplesByColor(List<Apple> inventory,
String color) {
List<Apple> result = new ArrayList<>;
for (Apple apple: inventory){
if ( apple.getColor.equals(color) ) {
result.add(apple);
}
}
return result;
}
現在,只要像下面這樣調用方法,農民朋友就會滿意了:
List<Apple> greenApples = filterApplesByColor(inventory, "green");
List<Apple> redApples = filterApplesByColor(inventory, "red");
…
太簡單了對吧?讓我們把例子再弄得複雜一點兒。這位農民又跑回來和你說:“要是能區分輕的蘋果和重的蘋果就太好了。重的蘋果一般是重量大於150克。”
作為軟件工程師,你早就想到農民可能會要改變重量,於是你寫了下面的方法,用另一個參數來應對不同的重量:
public static List<Apple> filterApplesByWeight(List<Apple> inventory,
int weight) {
List<Apple> result = new ArrayList<>;
For (Apple apple: inventory){
if ( apple.getWeight > weight ){
result.add(apple);
}
}
return result;
}
解決方案不錯,但是請注意,你複製了大部分的代碼來實現遍歷庫存,並對每個蘋果應用篩選條件。這有點兒令人失望,因為它打破了DRY(Don't Repeat Yourself,不要重複自己)的軟件工程原則。如果你想要改變篩選遍歷方式來提升性能呢?那就得修改所有方法的實現,而不是只改一個。從工程工作量的角度來看,這代價太大了。
你可以將顏色和重量結合為一個方法,稱為filter
。不過就算這樣,你還是需要一種方式來區分想要篩選哪個屬性。你可以加上一個標誌來區分對顏色和重量的查詢(但絕不要這樣做!我們很快會解釋為什麼)。
2.1.3 第三次嘗試:對你能想到的每個屬性做篩選
一種把所有屬性結合起來的笨拙嘗試如下所示:
public static List<Apple> filterApples(List<Apple> inventory, String color,
int weight, boolean flag) {
List<Apple> result = new ArrayList<>;
for (Apple apple: inventory){
if ( (flag && apple.getColor.equals(color)) ||
(!flag && apple.getWeight > weight) ){ ←─十分笨拙的選擇顏色或重量的方式
result.add(apple);
}
}
return result;
}
你可以這麼用(但真的很笨拙):
List<Apple> greenApples = filterApples(inventory, "green", 0, true);
List<Apple> heavyApples = filterApples(inventory, "", 150, false);
…
這個解決方案再差不過了。首先,客戶端代碼看上去糟透了。true
和false
是什麼意思?此外,這個解決方案還是不能很好地應對變化的需求。如果這位農民要求你對蘋果的不同屬性做篩選,比如大小、形狀、產地等,又怎麼辦?而且,如果農民要求你組合屬性,做更複雜的查詢,比如綠色的重蘋果,又該怎麼辦?你會有好多個重複的filter
方法,或一個巨大的非常複雜的方法。到目前為止,你已經給filterApples
方法加上了值(比如String
、Integer
或boolean
)的參數。這對於某些確定性問題可能還不錯。但如今這種情況下,你需要一種更好的方式,來把蘋果的選擇標準告訴你的filterApples
方法。在下一節中,我們會介紹了如何利用行為參數化實現這種靈活性。
2.2 行為參數化
你在上一節中已經看到了,你需要一種比添加很多參數更好的方法來應對變化的需求。讓我們後退一步來看看更高層次的抽像。一種可能的解決方案是對你的選擇標準建模:你考慮的是蘋果,需要根據Apple
的某些屬性(比如它是綠色的嗎?重量超過150克嗎?)來返回一個boolean
值。我們把它稱為謂詞(即一個返回boolean
值的函數)。讓我們定義一個接口來對選擇標準建模:
public interface ApplePredicate{
boolean test (Apple apple);
}
現在你就可以用ApplePredicate
的多個實現代表不同的選擇標準了,比如(如圖2-1所示):
public class AppleHeavyWeightPredicate implements ApplePredicate{ ←─僅僅選出重的蘋果
public boolean test(Apple apple){
return apple.getWeight > 150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate{ ←─僅僅選出綠蘋果
public boolean test(Apple apple){
return "green".equals(apple.getColor);
}
}
圖 2-1 選擇蘋果的不同策略
你可以把這些標準看作filter
方法的不同行為。你剛做的這些和“策略設計模式”1相關,它讓你定義一族算法,把它們封裝起來(稱為“策略”),然後在運行時選擇一個算法。在這裡,算法族就是ApplePredicate
,不同的策略就是AppleHeavyWeightPredicate
和AppleGreenColorPredicate
。
1見http://en.wikipedia.org/wiki/Strategy_pattern。
但是,該怎麼利用ApplePredicate
的不同實現呢?你需要filterApples
方法接受ApplePredicate
對象,對Apple
做條件測試。這就是行為參數化:讓方法接受多種行為(或戰略)作為參數,並在內部使用,來完成不同的行為。
要在我們的例子中實現這一點,你要給filterApples
方法添加一個參數,讓它接受ApplePredicate
對象。這在軟件工程上有很大好處:現在你把filterApples
方法迭代集合的邏輯與你要應用到集合中每個元素的行為(這裡是一個謂詞)區分開了。
第四次嘗試:根據抽像條件篩選
利用ApplePredicate
改過之後,filter
方法看起來是這樣的:
public static List<Apple> filterApples(List<Apple> inventory,
ApplePredicate p){
List<Apple> result = new ArrayList<>;
for(Apple apple: inventory){
if(p.test(apple)){ ←─謂詞對像封裝了測試蘋果的條件
result.add(apple);
}
}
return result;
}
1. 傳遞代碼/行為
這裡值得停下來小小地慶祝一下。這段代碼比我們第一次嘗試的時候靈活多了,讀起來、用起來也更容易!現在你可以創建不同的ApplePredicate
對象,並將它們傳遞給filterApples
方法。免費的靈活性!比如,如果農民讓你找出所有重量超過150克的紅蘋果,你只需要創建一個類來實現ApplePredicate
就行了。你的代碼現在足夠靈活,可以應對任何涉及蘋果屬性的需求變更了:
public class AppleRedAndHeavyPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "red".equals(apple.getColor)
&& apple.getWeight > 150;
}
}
List<Apple> redAndHeavyApples =
filterApples(inventory, new AppleRedAndHeavyPredicate);
你已經做成了一件很酷的事:filterApples
方法的行為取決於你通過ApplePredicate
對像傳遞的代碼。換句話說,你把filterApples
方法的行為參數化了!
請注意,在上一個例子中,唯一重要的代碼是test
方法的實現,如圖2-2所示;正是它定義了filterApples
方法的新行為。但令人遺憾的是,由於該filterApples
方法只能接受對象,所以你必須把代碼包裹在ApplePredicate
對像裡。你的做法就類似於在內聯“傳遞代碼”,因為你是通過一個實現了test
方法的對象來傳遞布爾表達式的。你將在2.3節(第3章中有更詳細的內容)中看到,通過使用Lambda,你可以直接把表達式"red".equals(apple.getColor) &&apple.getWeight > 150
傳遞給filterApples
方法,而無需定義多個ApplePredicate
類,從而去掉不必要的代碼。
圖 2-2 參數化filterApples
的行為,並傳遞不同的篩選策略
2. 多種行為,一個參數
正如我們先前解釋的那樣,行為參數化的好處在於你可以把迭代要篩選的集合的邏輯與對集合中每個元素應用的行為區分開來。這樣你可以重複使用同一個方法,給它不同的行為來達到不同的目的,如圖2-3所示。
圖 2-3 參數化filterApples
的行為並傳遞不同的篩選策略
這就是說行為參數化 是一個有用的概念的原因。你應該把它放進你的工具箱裡,用來編寫靈活的API。
為了保證你對行為參數化運用自如,看看測驗2.1吧!
測驗2.1:編寫靈活的
prettyPrintApple
方法編寫一個
prettyPrintApple
方法,它接受一個Apple
的List
,並可以對它參數化,以多種方式根據蘋果生成一個String
輸出(有點兒像多個可定制的toString
方法)。例如,你可以告訴prettyPrintApple
方法,只打印每個蘋果的重量。此外,你可以讓prettyPrintApple
方法分別打印每個蘋果,然後說明它是重的還是輕的。解決方案和我們前面討論的篩選的例子類似。為了幫你上手,我們提供了prettyPrintApple
方法的一個粗略的框架:public static void prettyPrintApple(List<Apple> inventory, ???){ for(Apple apple: inventory) { String output = ???.???(apple); System.out.println(output); } }
答案如下。
首先,你需要一種表示接受
Apple
並返回一個格式String
值的方法。前面我們在編寫ApplePredicate
接口的時候,寫過類似的東西:public interface AppleFormatter{ String accept(Apple a); }
現在你就可以通過實現
AppleFormatter
方法,來表示多種格式行為了:public class AppleFancyFormatter implements AppleFormatter{ public String accept(Apple apple){ String characteristic = apple.getWeight > 150 ? "heavy" : "light"; return "A " + characteristic + " " + apple.getColor +" apple"; } } public class AppleSimpleFormatter implements AppleFormatter{ public String accept(Apple apple){ return "An apple of " + apple.getWeight + "g"; } }
最後,你需要告訴
prettyPrintApple
方法接受AppleFormatter
對象,並在內部使用它們。你可以給prettyPrintApple
加上一個參數:public static void prettyPrintApple(List<Apple> inventory, AppleFormatter formatter){ for(Apple apple: inventory){ String output = formatter.accept(apple); System.out.println(output); } }
搞定啦!現在你就可以給
prettyPrintApple
方法傳遞多種行為了。為此,你首先要實例化AppleFormatter
的實現,然後把它們作為參數傳給prettyPrintApple
:prettyPrintApple(inventory, new AppleFancyFormatter);
這將產生一個類似於下面的輸出:
A light green apple A heavy red apple …
或者試試這個:
prettyPrintApple(inventory, new AppleSimpleFormatter);
這將產生一個類似於下面的輸出:
An apple of 80g An apple of 155g …
你已經看到,可以把行為抽像出來,讓你的代碼適應需求的變化,但這個過程很囉嗦,因為你需要聲明很多只要實例化一次的類。讓我們來看看可以怎樣改進。
2.3 對付囉嗦
我們都知道,人們都不願意用那些很麻煩的功能或概念。目前,當要把新的行為傳遞給filterApples
方法的時候,你不得不聲明好幾個實現ApplePredicate
接口的類,然後實例化好幾個只會提到一次的ApplePredicate
對象。下面的程序總結了你目前看到的一切。這真是很囉嗦,很費時間!
代碼清單2-1 行為參數化:用謂詞篩選蘋果
public class AppleHeavyWeightPredicate implements ApplePredicate{ ←─選擇較重蘋果的謂詞
public boolean test(Apple apple){
return apple.getWeight > 150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate{ ←─選擇綠蘋果的謂詞
public boolean test(Apple apple){
return "green".equals(apple.getColor);
}
}
public class FilteringApples{
public static void main(String...args){
List<Apple> inventory = Arrays.asList(new Apple(80,"green"),
new Apple(155, "green"),
new Apple(120, "red"));
List<Apple> heavyApples =
filterApples(inventory, new AppleHeavyWeightPredicate); ←─結果是一個包含一個155克Apple的List
List<Apple> greenApples =
filterApples(inventory, new AppleGreenColorPredicate); ←─結果是一個包含兩個綠Apple的List
}
public static List<Apple> filterApples(List<Apple> inventory,
ApplePredicate p) {
List<Apple> result = new ArrayList<>;
for (Apple apple : inventory){
if (p.test(apple)){
result.add(apple);
}
}
return result;
}
}
費這麼大勁兒真沒必要,能不能做得更好呢?Java有一個機制稱為匿名類,它可以讓你同時聲明和實例化一個類。它可以幫助你進一步改善代碼,讓它變得更簡潔。但這也不完全令人滿意。2.3.3節簡短地介紹了Lambda表達式如何讓你的代碼更易讀,我們將在下一章詳細討論。
2.3.1 匿名類
匿名類和你熟悉的Java局部類(塊中定義的類)差不多,但匿名類沒有名字。它允許你同時聲明並實例化一個類。換句話說,它允許你隨用隨建。
2.3.2 第五次嘗試:使用匿名類
下面的代碼展示了如何通過創建一個用匿名類實現ApplePredicate
的對象,重寫篩選的例子:
List<Apple> redApples = filterApples(inventory, new ApplePredicate { ←─直接內聯參數化filterapples方法的行為
public boolean test(Apple apple){
return "red".equals(apple.getColor);
}
});
GUI應用程序中經常使用匿名類來創建事件處理器對像(下面的例子使用的是Java FX API,一種現代的Java UI平台):
button.setOnAction(new EventHandler<ActionEvent> {
public void handle(ActionEvent event) {
System.out.println("Woooo a click!!");
}
});
但匿名類還是不夠好。第一,它往往很笨重,因為它佔用了很多空間。還拿前面的例子來看,如下面高亮的代碼所示:
第二,很多程序員覺得它用起來很讓人費解。比如,測驗2.2展示了一個經典的Java謎題,它讓大多數程序員都措手不及。你來試試看吧。
測驗2.2:匿名類謎題
下面的代碼執行時會有什麼樣的輸出呢,
4
、5
、6
還是42
?public class MeaningOfThis { public final int value = 4; public void doIt { int value = 6; Runnable r = new Runnable{ public final int value = 5; public void run{ int value = 10; System.out.println(this.value); } }; r.run; } public static void main(String...args) { MeaningOfThis m = new MeaningOfThis; m.doIt; ←─這一行的輸出是什麼? } }
答案是
5
,因為this
指的是包含它的Runnable
,而不是外面的類MeaningOfThis
。
整體來說,囉嗦就不好;它讓人不願意使用語言的某種功能,因為編寫和維護囉嗦的代碼需要很長時間,而且代碼也不易讀。好的代碼應該是一目瞭然的。即使匿名類處理在某種程度上改善了為一個接口聲明好幾個實體類的囉嗦問題,但它仍不能令人滿意。在只需要傳遞一段簡單的代碼時(例如表示選擇標準的boolean
表達式),你還是要創建一個對象,明確地實現一個方法來定義一個新的行為(例如Predicate
中的test
方法或是EventHandler
中的handler
方法)。
在理想的情況下,我們想鼓勵程序員使用行為參數化模式,因為正如你在前面看到的,它讓代碼更能適應需求的變化。在第3章中,你會看到Java 8的語言設計者通過引入Lambda表達式——一種更簡潔的傳遞代碼的方式——解決了這個問題。好了,懸念夠多了,下面簡單介紹一下Lambda表達式是怎麼讓代碼更乾淨的。
2.3.3 第六次嘗試:使用Lambda表達式
上面的代碼在Java 8里可以用Lambda表達式重寫為下面的樣子:
List<Apple> result =
filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor));
不得不承認這代碼看上去比先前乾淨很多。這很好,因為它看起來更像問題陳述本身了。我們現在已經解決了囉嗦的問題。圖2-4對我們到目前為止的工作做了一個小結。
圖 2-4 行為參數化與值參數化
2.3.4 第七次嘗試:將List
類型抽像化
在通往抽像的路上,我們還可以更進一步。目前,filterApples
方法還只適用於Apple
。你還可以將List
類型抽像化,從而超越你眼前要處理的問題:
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){ ←─引入類型參數T
List<T> result = new ArrayList<>;
for(T e: list){
if(p.test(e)){
result.add(e);
}
}
return result;
}
現在你可以把filter
方法用在香蕉、桔子、Integer
或是String
的列表上了。這裡有一個使用Lambda表達式的例子:
List<Apple> redApples =
filter(inventory, (Apple apple) -> "red".equals(apple.getColor));
List<Integer> evenNumbers =
filter(numbers, (Integer i) -> i % 2 == 0);
酷不酷?你現在在靈活性和簡潔性之間找到了最佳平衡點,這在Java 8之前是不可能做到的!
2.4 真實的例子
你現在已經看到,行為參數化是一個很有用的模式,它能夠輕鬆地適應不斷變化的需求。這種模式可以把一個行為(一段代碼)封裝起來,並通過傳遞和使用創建的行為(例如對Apple
的不同謂詞)將方法的行為參數化。前面提到過,這種做法類似於策略設計模式。你可能已經在實踐中用過這個模式了。Java API中的很多方法都可以用不同的行為來參數化。這些方法往往與匿名類一起使用。我們會展示三個例子,這應該能幫助你鞏固傳遞代碼的思想了:用一個Comparator
排序,用Runnable
執行一個代碼塊,以及GUI事件處理。
2.4.1 用Comparator
來排序
對集合進行排序是一個常見的編程任務。比如,你的那位農民朋友想要根據蘋果的重量對庫存進行排序,或者他可能改了主意,希望你根據顏色對蘋果進行排序。聽起來有點兒耳熟?是的,你需要一種方法來表示和使用不同的排序行為,來輕鬆地適應變化的需求。
在Java 8中,List
自帶了一個sort
方法(你也可以使用Collections.sort
)。sort
的行為可以用java.util.Comparator
對像來參數化,它的接口如下:
// java.util.Comparator
public interface Comparator<T> {
public int compare(T o1, T o2);
}
因此,你可以隨時創建Comparator
的實現,用sort
方法表現出不同的行為。比如,你可以使用匿名類,按照重量升序對庫存排序:
inventory.sort(new Comparator<Apple> {
public int compare(Apple a1, Apple a2){
return a1.getWeight.compareTo(a2.getWeight);
}
});
如果農民改了主意,你可以隨時創建一個Comparator
來滿足他的新要求,並把它傳遞給sort
方法。而如何進行排序這一內部細節都被抽像掉了。用Lambda表達式的話,看起來就是這樣:
inventory.sort(
(Apple a1, Apple a2) -> a1.getWeight.compareTo(a2.getWeight));
現在暫時不用擔心這個新語法,下一章我們會詳細講解如何編寫和使用Lambda表達式。
2.4.2 用Runnable
執行代碼塊
線程就像是輕量級的進程:它們自己執行一個代碼塊。但是,怎麼才能告訴線程要執行哪塊代碼呢?多個線程可能會運行不同的代碼。我們需要一種方式來代表稍候執行的一段代碼。在Java裡,你可以使用Runnable
接口表示一個要執行的代碼塊。請注意,代碼不會返回任何結果(即void
):
// java.lang.Runnable
public interface Runnable{
public void run;
}
你可以像下面這樣,使用這個接口創建執行不同行為的線程:
Thread t = new Thread(new Runnable {
public void run{
System.out.println("Hello world");
}
});
用Lambda表達式的話,看起來是這樣:
Thread t = new Thread( -> System.out.println("Hello world"));
2.4.3 GUI事件處理
GUI編程的一個典型模式就是執行一個操作來響應特定事件,如鼠標單擊或在文字上懸停。例如,如果用戶單擊“發送”按鈕,你可能想顯示一個彈出式窗口,或把行為記錄在一個文件中。你還是需要一種方法來應對變化;你應該能夠作出任意形式的響應。在JavaFX中,你可以使用 EventHandler
,把它傳給setOnAction
來表示對事件的響應:
Button button = new Button("Send");
button.setOnAction(new EventHandler<ActionEvent> {
public void handle(ActionEvent event) {
label.setText("Sent!!");
}
});
這裡,setOnAction
方法的行為就用EventHandler
參數化了。用Lambda表達式的話,看起來就是這樣:
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));
2.5 小結
以下是你應從本章中學到的關鍵概念。
-
行為參數化,就是一個方法接受多個不同的行為作為參數,並在內部使用它們,完成不同行為的能力。
-
行為參數化可讓代碼更好地適應不斷變化的要求,減輕未來的工作量。
-
傳遞代碼,就是將新行為作為參數傳遞給方法。但在Java 8之前這實現起來很囉嗦。為接口聲明許多只用一次的實體類而造成的囉嗦代碼,在Java 8之前可以用匿名類來減少。
-
Java API包含很多可以用不同行為進行參數化的方法,包括排序、線程和GUI處理。