Java 8——行為參數化

前言

《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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容