- Java 8實戰
- (英)Raoul-Gabriel Urma (意)Mario Fusco (英)Alan Mycroft
- 6233字
- 2020-06-23 13:53:39
第2章 通過行為參數化傳遞代碼
本章內容
? 應對不斷變化的需求
? 行為參數化
? 匿名類
? Lambda表達式預覽
? 真實示例:Comparator、Runnable和GUI
在軟件工程中,一個眾所周知的問題就是,不管你做什么,用戶的需求肯定會變。比方說,有個應用程序是幫助農民了解自己的庫存的。這位農民可能想有一個查找庫存中所有綠色蘋果的功能。但到了第二天,他可能會告訴你:“其實我還想找出所有重量超過150克的蘋果。”又過了兩天,農民又跑回來補充道:“要是我可以找出所有既是綠色,重量也超過150克的蘋果,那就太棒了。”你要如何應對這樣不斷變化的需求?理想的狀態下,應該把你的工作量降到最少。此外,類似的新功能實現起來還應該很簡單,而且易于長期維護。
行為參數化就是可以幫助你處理頻繁變更的需求的一種軟件開發模式。一言以蔽之,它意味著拿出一個代碼塊,把它準備好卻不去執行它。這個代碼塊以后可以被你程序的其他部分調用,這意味著你可以推遲這塊代碼的執行。例如,你可以將代碼塊作為參數傳遞給另一個方法,稍后再去執行它。這樣,這個方法的行為就基于那塊代碼被參數化了。例如,如果你要處理一個集合,可能會寫一個方法:
? 可以對列表中的每個元素做“某件事”
? 可以在列表處理完后做“另一件事”
? 遇到錯誤時可以做“另外一件事”
行為參數化說的就是這個。打個比方吧:你的室友知道怎么開車去超市,再開回家。于是你可以告訴他去買一些東西,比如面包、奶酪、葡萄酒什么的。這相當于調用一個goAndBuy方法,把購物單作為參數。然而,有一天你在上班,你需要他去做一件他從來沒有做過的事情:從郵局取一個包裹。現在你就需要傳遞給他一系列指示了:去郵局,使用單號,和工作人員說明情況,取走包裹。你可以把這些指示用電子郵件發給他,當他收到之后就可以按照指示行事了。你現在做的事情就更高級一些了,相當于一個方法:go,它可以接受不同的新行為作為參數,然后去執行。
這一章首先會給你講解一個例子,說明如何對你的代碼加以改進,從而更靈活地適應不斷變化的需求。在此基礎之上,我們將展示如何把行為參數化用在幾個真實的例子上。比如,你可能已經用過了行為參數化模式——使用Java API中現有的類和接口,對List進行排序,篩選文件名,或告訴一個Thread去執行代碼塊,甚或是處理GUI事件。你很快會發現,在Java中使用這種模式十分啰嗦。Java 8中的Lambda解決了代碼啰嗦的問題。我們會在第3章中向你展示如何構建Lambda表達式、其使用場合,以及如何利用它讓代碼更簡潔。
2.1 應對不斷變化的需求
編寫能夠應對變化的需求的代碼并不容易。讓我們來看一個例子,我們會逐步改進這個例子,以展示一些讓代碼更靈活的最佳做法。就農場庫存程序而言,你必須實現一個從列表中篩選綠蘋果的功能。聽起來很簡單吧?
2.1.1 初試牛刀:篩選綠蘋果
第一個解決方案可能是下面這樣的:

突出顯示的行就是篩選綠蘋果所需的條件。但是現在農民改主意了,他還想要篩選紅蘋果。你該怎么做呢?簡單的解決辦法就是復制這個方法,把名字改成filterRedApples,然后更改if條件來匹配紅蘋果。然而,要是農民想要篩選多種顏色:淺綠色、暗紅色、黃色等,這種方法就應付不了了。一個良好的原則是在編寫類似的代碼之后,嘗試將其抽象化。
2.1.2 再展身手:把顏色作為參數
一種做法是給方法加一個參數,把顏色變成參數,這樣就能靈活地適應變化了:
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) { List<Apple> result=new ArrayList<Apple>(); 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<Apple>(); For (Apple apple: inventory){ if ( apple.getWeight() > weight ){ result.add(apple); } } return result; }
解決方案不錯,但是請注意,你復制了大部分的代碼來實現遍歷庫存,并對每個蘋果應用篩選條件。這有點兒令人失望,因為它打破了DRY(Don't Repeat Yourself,不要重復自己)的軟件工程原則。如果你想要改變篩選遍歷方式來提升性能呢?那就得修改所有方法的實現,而不是只改一個。從工程工作量的角度來看,這代價太大了。
你可以將顏色和重量結合為一個方法,稱為filter。不過就算這樣,你還是需要一種方式來區分想要篩選哪個屬性。你可以加上一個標志來區分對顏色和重量的查詢(但絕不要這樣做!我們很快會解釋為什么)。
2.1.3 第三次嘗試:對你能想到的每個屬性做篩選
一種把所有屬性結合起來的笨拙嘗試如下所示:

你可以這么用(但真的很笨拙):
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所示):


圖2-1 選擇蘋果的不同策略
你可以把這些標準看作filter方法的不同行為。你剛做的這些和“策略設計模式”相關,它讓你定義一族算法,把它們封裝起來(稱為“策略”),然后在運行時選擇一個算法。在這里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreen-ColorPredicate。
但是,該怎么利用ApplePredicate的不同實現呢?你需要filterApples方法接受ApplePredicate對象,對Apple做條件測試。這就是行為參數化:讓方法接受多種行為(或戰略)作為參數,并在內部使用,來完成不同的行為。
要在我們的例子中實現這一點,你要給filterApples方法添加一個參數,讓它接受ApplePredicate對象。這在軟件工程上有很大好處:現在你把filterApples方法迭代集合的邏輯與你要應用到集合中每個元素的行為(這里是一個謂詞)區分開了。
第四次嘗試:根據抽象條件篩選
利用ApplePredicate改過之后,filter方法看起來是這樣的:

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 行為參數化:用謂詞篩選蘋果

費這么大勁兒真沒必要,能不能做得更好呢?Java有一個機制稱為匿名類,它可以讓你同時聲明和實例化一個類。它可以幫助你進一步改善代碼,讓它變得更簡潔。但這也不完全令人滿意。2.3.3節簡短地介紹了Lambda表達式如何讓你的代碼更易讀,我們將在下一章詳細討論。
2.3.1 匿名類
匿名類和你熟悉的Java局部類(塊中定義的類)差不多,但匿名類沒有名字。它允許你同時聲明并實例化一個類。換句話說,它允許你隨用隨建。
2.3.2 第五次嘗試:使用匿名類
下面的代碼展示了如何通過創建一個用匿名類實現ApplePredicate的對象,重寫篩選的例子:

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?

答案是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類型抽象化,從而超越你眼前要處理的問題:

現在你可以把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處理。
- Facebook Application Development with Graph API Cookbook
- Spring Cloud Alibaba核心技術與實戰案例
- Python 3.7網絡爬蟲快速入門
- UML和模式應用(原書第3版)
- C++面向對象程序設計(微課版)
- ASP.NET動態網頁設計教程(第三版)
- Flux Architecture
- Learning Apache Mahout Classification
- Learning DHTMLX Suite UI
- Python圖形化編程(微課版)
- 快速入門與進階:Creo 4·0全實例精講
- Building Dynamics CRM 2015 Dashboards with Power BI
- Extending Unity with Editor Scripting
- Zabbix Performance Tuning
- Java EE 7 with GlassFish 4 Application Server