前言
《Java8實戰》不得不說是一本好書,捧起來看起來就興奮得不想放下,其中介紹的函數式編程實在是太令人興奮了,不僅僅大大提高了代碼的可讀性,而且提高了代碼的重用性,并且語法簡單。
Java 8中新增的功能是自Java 1.0發布以來18年以來,發生變化最大的一次。我本身沒有太大的體會,但新增的這些功能,每一個都讓我興奮,這里就書中的內容簡單的介紹一下Java 8的這些新特性,我相信很快,你也會有跟我一樣的感受。
(1)用行為參數化把代碼傳遞給方法
Java 8中增加了通過API來傳遞代碼的能力,但這實在聽起來太繞了,這到底在說什么!打個比方或許要容易理解一些,你想要寫兩個只有幾行代碼不同的方法,那現在你只需要把不同的那部分代碼作為參數傳遞進去就可以了。
在Java 8中,這樣做起來(不止于匿名類)遠遠比你想象的要來得更加清晰、簡潔。行為參數化
我們現在來考慮這樣一個例子:有個應用程序是幫助農民了解自己的庫存的,這位農民可能想有一個查找庫存中所有綠色蘋果的功能。但到了第二天,他可能會告訴你:“其實我還想找出所有重量超過150克的蘋果”。又過了兩天,他可能會繼續補充道:“要是我可以找出所有既是綠色,重量也超過150克的蘋果,那就太棒了。”
想一下你應該如何應對這樣不斷變化的需求呢?理想的狀態下,你應該把你的工作量降到最小,此外類似的新功能實現起來還應該很簡單,而且易于長期維護。
第一次嘗試:篩選綠蘋果
第一個解決方案可能是下面這樣的:
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);
}
} // end for
return result;
}
這樣的代碼看起來似乎也沒什么問題,也很容易看懂,但是現在農民改主意了,他還想要篩選紅蘋果,又該怎么做呢?簡單的方法就是復制這個方法,然后把函數名稱和if判斷的條件來分別匹配到紅蘋果上就可以了。然而,要是農民想要篩選出多種顏色:淺綠色,暗紅色、黃色等,這種方法就應付不了了。一個良好的原則是在編寫類似的代碼之后,嘗試將其抽象化。
第二次嘗試:把顏色作為參數
一種做法是給方法加一個參數,把顏色變成參數,這樣就靈活地適應了變化:
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);
}
} // end for
return result;
}
現在,只要像下面這樣調用方法,農民朋友就會滿意了:
List<Apple> greenApples = filterApplesByColor(inventory,"green");
List<Apple> greenApples = filterApplesByColor(inventory,"red");
....
太簡單了對吧?讓我們把案例變得復雜一點。這位農民又跑回來和你說:“要是能區分輕的蘋果和重的蘋果就太好了。重的蘋果一般大于150克。”
作為軟件工程師,你早就想好了農民可能會要改變重量,于是你寫了下面的方法,用另一個參數來應對不同的重量:
public static List<Apple> filterApplesByColor(List<Apple> inventory,int weight){
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if(apple.getWeight() > weight){
result.add(apple);
}
} // end for
return result;
}
解決方案不錯,但是請注意,你賦值了大部分的代碼來實現遍歷庫存,并對每個蘋果應用篩選條件。這有點兒令人失望,因為它打破了DRY(Don't Repeat Yourself,不要重復你自己)的軟件工程原則。
如果你想要改變篩選遍歷方式來提升性能呢?那就得修改所有方法的實現,而不是只改變一個。從工程工作量的角度來看,這代價太大了。
你可以將顏色和重量結合為一個方法,稱為filter。不過就算這樣,你還是需要一種方式來區分想要篩選哪個屬性。你可以加上一個標志位來區分對顏色和重量的查詢(但絕不要這樣做!很快你就會明白為什么)。
第三次嘗試:對你想到的每個屬性做篩選
一種把所有屬性結合起來的笨拙嘗試如下:
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);
}
} // end for
return result;
}
你可以這么用(但真的很笨拙):
List<Apple> greenApples = filterApples(inventory, "green", 0, ture);
List<Apple> heavyApples = filterApples(inventory, "", 150, false);
這樣的解決方案再差不過了。
首先,客戶端代碼看上去糟糕透了,ture和false是什么意思?此外,這個解決方案還是不能很好的應對變化的需求。如果這位農要求你對蘋果的不同屬性做篩選,比如大小、形狀、產地等,又怎么辦?而且,如果農民要求你組合屬性,做更復雜的查詢,比如綠色的種蘋果,又改怎么辦?你會有好多個重復的filter方法,或者一個巨大的非常復雜的方法。
到目前為止,你已經給filterApples方法加上了值(如String、Integer或boolean)的參數。這對于某些確定性問題可能還不錯,但如今這種情況下,你需要一種更好的方式,來把蘋果的選擇標準告訴你的filterApples方法。
這就是需要行為參數化登場發揮作用的地方了。讓我們后退一步來看看更高層次的抽象。一種可能的解決方案是對你的選擇標準建模:你考慮的是蘋果,需要根據Apple的某些屬性(比如它是綠色的嗎?重量超過150克嗎?)來返回一個boolean值,我們把它稱為謂詞(即一個返回boolean值得函數)。
第四次嘗試:根據抽象條件篩選
讓我們先來定義一個接口對選擇標準建模:
public interface ApplePredicate{
boolean test (Apple apple);
}
現在你可以使用ApplePredicate的多個實現代表 不同的選擇標準了,比如:
你可以把這些標準看作filter方法的不同行為。你剛做的這些和“策略設計模式”相關,它讓你定義一族算法,把它們封裝起來(稱為“策略”),然后在運行時選擇一個算法。在這里算法簇就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。
但是,該怎么利用ApplePredicate的不同實現呢?你需要filterApples方法接受ApplePredicate對象,對Apple做條件測試。這就是行為參數化:讓方法接受多種行為(或戰略)作為參數,并在內部使用,完成不同的行為。
利用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);
} // end if
} // end for
return result;
}
這里值得暫停下來小小地慶祝一下。這段代碼比我們第一次嘗試的時候靈活多了,讀起來、用起來也更容易!現在你可以創建不同的ApplePredicate對象,并將它們傳遞給filterApples方法。免費的靈活性!比如,如果農民讓你找出所有重量超過150克的紅蘋果,你只需要創建一個類來實現ApplePredicacte對象就可以了,你的代碼現在足夠靈活,可以應對任何涉及蘋果屬性的需求變更了。
你已經做成了一件很酷的事:filterApples方法的行為取決于你通過ApplePredicate對象傳遞的代碼,換句話說,你把filterApples方法的行為參數化了!
第五次嘗試:使用匿名類
這樣做起來已經很棒了,還有什么問題呢?
我們都知道,人們都不愿意用那些很麻煩的功能或者概念,目前,當要把新的行為傳遞給filterApples方法的時候,你不得不聲明好幾個實現ApplePredicate接口的類,然后實例化好幾個只會提到一次的ApplePredicate對象。下面這段程序總結了你目前看到的一切,這真的很啰嗦而且費時間:
費這么大勁兒,真的沒什么必要。能不能做得更好呢?Java有一個機制稱為匿名類,它可以讓你同時聲明和實例化一個類,它可以幫助你進一步改善代碼,讓它變得更簡潔:
List<Apple> redApples = filterApples(inventory, new Applepredicate(){
public boolean test(Apple apple){
return "red".equals(apple.getColor());
}
});
GUI應用程序中經常使用匿名類來創建事件處理器對象(下面的例子使用的是JavaFX API,一種現代的Java UI平臺):
button.setOnAction(new EventHandler<ActionEvent>(){
public void handle(ActionEvent event){
System.out.println("Woooo a click!");
}
});
但是匿名類仍然不夠好。
第一,它往往很笨重,因為它占用了很多空間,還拿前面的例子來說:
第二,很多程序員覺得它用起來很讓人費解,比如這里有一道經典的Java謎題,它讓大多數程序員都措手不及,來試試看:
答案是5,因為this指的是包含它的Runnable,而不是外面的類MeaningOfThis。
整體來說,啰嗦就不好。它讓人不愿意使用語言的某種功能,因為編寫和維護啰嗦的代碼需要很長時間,而且代碼也不易讀。好的代碼應該一目了然。
即使匿名類處理在某種程度上改善了為一個接口聲明好幾個實體類的啰嗦問題,但它仍然不能讓人滿意。在只需要傳遞一段簡單的代碼時(例如表示選擇標準的boolean表達式),你還是要創建一個對象,明確地實現一個方法來定義一個新的行為(例如Predicate中的test方法或者是EventHandler中的handler方法)。
在理想的情況下,我們想鼓勵程序員使用行為參數化模式,因為正如你在前面看到的,它讓代碼更能適應需求的變化,但也同樣的,啰嗦不可避免。這也正是Java 8的語言設計者引入Lambda表達式的原因——他讓傳遞代碼的方式變得更加簡潔、干凈。
第六次嘗試:使用Lambda表達式
上面的代碼在Java 8里可以用Lambda表達式重寫為下面的樣子:
List<Apple> result =
filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
不得不承認這代碼看上去比先前干凈很多,這很好,因為它看起來更像問題陳述本身了。我們現在已經解決了啰嗦的問題,下圖總結了到目前為止的工作:
第七次嘗試:將List類型抽象化
在通往抽象的道路上,我們還可以更近一步。目前filterApples方法還只適用于Apple。你還可以將List類型抽象畫,從而超越你眼前要處理的問題:
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){
List<T> result = new ArrayList<>();
for(T e:list){
if(p.test(e)){
result.add(e);
} // end if
} // end for
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之前是不可能做到的!
應用行為參數化的典型例子
一個是用Runnable執行代碼塊,用Lambda表達式的話,看起來就是這個樣子的:
Thread t = new Thread(() -> System.out.println("Hello world"));
另一個就是GUI事件處理:
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));
看起來酷極了吧?不過想要熟練地運用,就要足夠了解Lambda表達式,這將在下一節中再來說。
歡迎轉載,轉載請注明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關注公眾微信號:wmyskxz
分享自己的學習 & 學習資料 & 生活
想要交流的朋友也可以加qq群:3382693