《Java8實戰》-第八章筆記(重構、測試和調試)

重構、測試和調試

經過本書的前七章,咱們瞭解了Lambda和Stream API的強大威力。你可能主要在新項目的代碼中使用這些特性。若是你建立的是全新的Java項目,這是極好的時機,你能夠輕裝上陣,迅速地將新特性應用到項目中。然而不幸的是,大多數狀況下你沒有機會從頭開始一個全新的項目。不少時候,你不得不面對的是用老版Java接口編寫的遺留代碼。java

這些就是本章要討論的內容。咱們會介紹幾種方法,幫助你重構代碼,以適配使用Lambda表達式,讓你維護的代碼具有更好的可讀性和靈活性。除此以外,咱們還會討論目前比較流行的幾種面向對象的設計模式,包括策略模式、模板方法模式、觀察者模式、責任鏈模式,以及工廠模式,在結合Lambda表達式以後變得更簡潔的狀況。最後,咱們會介紹如何測試和調試使用Lambda表達式和Stream API的代碼。git

爲改善可讀性和靈活性重構代碼

從本書的開篇咱們就一直在強調,利用Lambda表達式,你能夠寫出更簡潔、更靈活的代碼。用「更簡潔」來描述Lambda表達式是由於相較於匿名類,Lambda表達式能夠幫助咱們用更緊湊的方式描述程序的行爲。第3章中咱們也提到,若是你但願將一個既有的方法做爲參數傳遞給另外一個方法,那麼方法引用無疑是咱們推薦的方法,利用這種方式咱們能寫出很是簡潔的代碼。程序員

採用Lambda表達式以後,你的代碼會變得更加靈活,由於Lambda表達式鼓勵你們使用第2章中介紹過的行爲參數化的方式。在這種方式下,應對需求的變化時,你的代碼能夠依據傳入的參數動態選擇和執行相應的行爲。github

這一節,咱們會將全部這些綜合在一塊兒,經過例子展現如何運用前幾章介紹的Lambda表達式、方法引用以及Stream接口等特性重構遺留代碼,改善程序的可讀性和靈活性。算法

改善代碼的可讀性

改善代碼的可讀性到底意味着什麼?咱們很難定義什麼是好的可讀性,由於這可能很是主觀。一般的理解是,「別人理解這段代碼的難易程度」。改善可讀性意味着你要確保你的代碼能很是容易地被包括本身在內的全部人理解和維護。爲了確保你的代碼能被其餘人理解,有幾個步驟能夠嘗試,好比確保你的代碼附有良好的文檔,並嚴格遵照編程規範。數據庫

跟以前的版本相比較,Java 8的新特性也能夠幫助提高代碼的可讀性:編程

  • 使用Java 8,你能夠減小冗長的代碼,讓代碼更易於理解
  • 經過方法引用和Stream API,你的代碼會變得更直觀

這裏咱們會介紹三種簡單的重構,利用Lambda表達式、方法引用以及Stream改善程序代碼的可讀性:設計模式

  • 重構代碼,用Lambda表達式取代匿名類
  • 用方法引用重構Lambda表達式
  • 用Stream API重構命令式的數據處理

從匿名類到 Lambda 表達式的轉換

你值得嘗試的第一種重構,也是簡單的方式,是將實現單一抽象方法的匿名類轉爲Lambda表達式。爲何呢?前面幾章的介紹應該足以說服你,由於匿名類是極其繁瑣且容易出錯的。採用Lambda表達式以後,你的代碼會更簡潔,可讀性更好。還記得第3章的例子就是一個建立Runnable 對象的匿名類,這段代碼及其對應的Lambda表達式實現以下:安全

Runnable r1 = new Runnable(){
    public void run() {
        System.out.println("Hello");
    }
};
Runnable r2 = () -> System.out.println("Hello");

可是某些狀況下,將匿名類轉換爲Lambda表達式多是一個比較複雜的過程。 首先,匿名類和Lambda表達式中的 this 和 super 的含義是不一樣的。在匿名類中, this 表明的是類自身,可是在Lambda中,它表明的是包含類。其次,匿名類能夠屏蔽包含類的變量,而Lambda表達式不能(它們會致使編譯錯誤),譬以下面這段代碼:架構

int a = 10;
Runnable r1 = () -> {
    // 編譯錯誤
    int a = 2;
    System.out.println(a);
};

Runnable r2 = new Runnable(){
    public void run(){
        // 一切正常
        int a = 2;
        System.out.println(a);
    }
};

最後,在涉及重載的上下文裏,將匿名類轉換爲Lambda表達式可能致使最終的代碼更加晦澀。實際上,匿名類的類型是在初始化時肯定的,而Lambda的類型取決於它的上下文。經過下面這個例子,咱們能夠了解問題是如何發生的。咱們假設你用與 Runnable 一樣的簽名聲明瞭一個函數接口,咱們稱之爲 Task (你但願採用與你的業務模型更貼切的接口名時,就可能作這樣的變動):

interface Task {
    public void execute();
}

public static void doSomething(Runnable r) { r.run(); }
public static void doSomething(Task a) { a.execute(); }

如今,你再傳遞一個匿名類實現的 Task ,不會碰到任何問題:

doSomething(new Task() {
    public void execute() {
        System.out.println("Danger danger!!");
    }
});

可是將這種匿名類轉換爲Lambda表達式時,就致使了一種晦澀的方法調用,由於 Runnable和 Task 都是合法的目標類型:

// 麻煩來了: doSomething(Runnable) 和doSomething(Task)都匹配該類型
doSomething(() -> System.out.println("Danger danger!!"));

你能夠對 Task 嘗試使用顯式的類型轉換來解決這種模棱兩可的狀況:

doSomething((Task)() -> System.out.println("Danger danger!!"));

可是不要所以而放棄對Lambda的嘗試。

從 Lambda 表達式到方法引用的轉換

Lambda表達式很是適用於須要傳遞代碼片斷的場景。不過,爲了改善代碼的可讀性,也請儘可能使用方法引用。由於方法名每每能更直觀地表達代碼的意圖。好比,第6章中咱們曾經展現過下面這段代碼,它的功能是按照食物的熱量級別對菜餚進行分類:

Map<Dish.CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
            groupingBy(dish -> {
                if (dish.getCalories() <= 400) {
                    return Dish.CaloricLevel.DIET;
                } else if (dish.getCalories() <= 700) {
                    return Dish.CaloricLevel.NORMAL;
                } else {
                    return Dish.CaloricLevel.FAT;
                }
            }));

你能夠將Lambda表達式的內容抽取到一個單獨的方法中,將其做爲參數傳遞給 groupingBy方法。變換以後,代碼變得更加簡潔,程序的意圖也更加清晰了:

Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(groupingBy(Dish::getCaloricLevel));

爲了實現這個方案,你還須要在 Dish 類中添加 getCaloricLevel 方法:

public class Dish{
    …
    public CaloricLevel getCaloricLevel(){
        if (this.getCalories() <= 400) {
            return CaloricLevel.DIET;
        } else if (this.getCalories() <= 700) {
            return CaloricLevel.NORMAL;
        } else {
            return CaloricLevel.FAT;
        }
    }
}

除此以外,咱們還應該儘可能考慮使用靜態輔助方法,好比 comparing 、 maxBy 。這些方法設計之初就考慮了會結合方法引用一塊兒使用。經過示例,咱們看到相對於第3章中的對應代碼,優化過的代碼更清晰地表達了它的設計意圖:

inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
inventory.sort(comparing(Apple::getWeight));

此外,不少通用的歸約操做,好比 sum 、 maximum ,都有內建的輔助方法能夠和方法引用結合使用。好比,在咱們的示例代碼中,使用 Collectors 接口能夠輕鬆獲得和或者最大值,與採用Lambada表達式和底層的歸約操做比起來,這種方式要直觀得多。與其編寫:

int totalCalories = menu.stream().map(Dish::getCalories).reduce(0, (c1, c2) -> c1 + c2);

不如嘗試使用內置的集合類,它能更清晰地表達問題陳述是什麼。下面的代碼中,咱們使用了集合類 summingInt (方法的名詞很直觀地解釋了它的功能):

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

從命令式的數據處理切換到 Stream

咱們建議你將全部使用迭代器這種數據處理模式處理集合的代碼都轉換成Stream API的方式。爲何呢?Stream API能更清晰地表達數據處理管道的意圖。除此以外,經過短路和延遲載入以及利用第7章介紹的現代計算機的多核架構,咱們能夠對Stream進行優化。

好比,下面的命令式代碼使用了兩種模式:篩選和抽取,這兩種模式被混在了一塊兒,這樣的代碼結構迫使程序員必須完全搞清楚程序的每一個細節才能理解代碼的功能。此外,實現須要並行運行的程序所面對的困難也多得多:

List<String> dishNames = new ArrayList<>();
for(Dish dish: menu){
    if(dish.getCalories() > 300){
        dishNames.add(dish.getName());
    }
}

替代方案使用Stream API,採用這種方式編寫的代碼讀起來更像是問題陳述,並行化也很是容易:

menu.parallelStream()
    .filter(d -> d.getCalories() > 300)
    .map(Dish::getName)
    .collect(toList());

不幸的是,將命令式的代碼結構轉換爲Stream API的形式是個困難的任務,由於你須要考慮控制流語句,好比 break 、 continue 、 return ,並選擇使用恰當的流操做。

增長代碼的靈活性

第2章和第3章中,咱們曾經介紹過Lambda表達式有利於行爲參數化。你可使用不一樣的Lambda表示不一樣的行爲,並將它們做爲參數傳遞給函數去處理執行。這種方式能夠幫助咱們淡定從容地面對需求的變化。好比,咱們能夠用多種方式爲 Predicate 建立篩選條件,或者使用Comparator 對多種對象進行比較。如今,咱們來看看哪些模式能夠立刻應用到你的代碼中,讓你享受Lambda表達式帶來的便利。

  • 採用函數接口

首先,你必須意識到,沒有函數接口,你就沒法使用Lambda表達式。所以,你須要在代碼中引入函數接口。聽起來很合理,可是在什麼狀況下使用它們呢?這裏咱們介紹兩種通用的模式,你能夠依照這兩種模式重構代碼,利用Lambda表達式帶來的靈活性,它們分別是:有條件的延遲執行和環繞執行。

  • 有條件的延遲執行

咱們常常看到這樣的代碼,控制語句被混雜在業務邏輯代碼之中。典型的狀況包括進行安全性檢查以及日誌輸出。好比,下面的這段代碼,它使用了Java語言內置的 Logger 類:

if (logger.isLoggable(Log.FINER)){
    logger.finer("Problem: " + generateDiagnostic());
}

這段代碼有什麼問題嗎?其實問題很多。

    • 日誌器的狀態(它支持哪些日誌等級)經過 isLoggable 方法暴露給了客戶端代碼。
    • 爲何要在每次輸出一條日誌以前都去查詢日誌器對象的狀態?這隻能搞砸你的代碼。更好的方案是使用 log 方法,該方法在輸出日誌消息以前,會在內部檢查日誌對象是否已經設置爲恰當的日誌等級:
logger.log(Level.FINER, "Problem: " + generateDiagnostic());

這種方式更好的緣由是你再也不須要在代碼中插入那些條件判斷,與此同時日誌器的狀態也再也不被暴露出去。不過,這段代碼依舊存在一個問題。日誌消息的輸出與否每次都須要判斷,即便你已經傳遞了參數,不開啓日誌。

這就是Lambda表達式能夠施展拳腳的地方。你須要作的僅僅是延遲消息構造,如此一來,日誌就只會在某些特定的狀況下才開啓(以此爲例,當日志器的級別設置爲 FINER 時)。顯然,Java 8的API設計者們已經意識到這個問題,並由此引入了一個對 log 方法的重載版本,這個版本的 log 方法接受一個 Supplier 做爲參數。這個替代版本的 log 方法的函數簽名以下:

public void log(Level level, Supplier<String> msgSupplier)

你能夠經過下面的方式對它進行調用:

logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic());

若是日誌器的級別設置恰當, log 方法會在內部執行做爲參數傳遞進來的Lambda表達式。這裏介紹的 Log 方法的內部實現以下:

public void log(Level level, Supplier<String> msgSupplier) {
    if(logger.isLoggable(level)){
        log(level, msgSupplier.get());
    }
}

從這個故事裏咱們學到了什麼呢?若是你發現你須要頻繁地從客戶端代碼去查詢一個對象的狀態(好比前文例子中的日誌器的狀態),只是爲了傳遞參數、調用該對象的一個方法(好比輸出一條日誌),那麼能夠考慮實現一個新的方法,以Lambda或者方法表達式做爲參數,新方法在檢查完該對象的狀態以後才調用原來的方法。你的代碼會所以而變得更易讀(結構更清晰),封裝性更好(對象的狀態也不會暴露給客戶端代碼了)。

經過這一節,你已經瞭解瞭如何經過不一樣方式來改善代碼的可讀性和靈活性。接下來,你會了解Lambada表達式如何避免常規面向對象設計中的僵化的模板代碼。

使用 Lambda 重構面向對象的設計模式

新的語言特性經常讓現存的編程模式或設計黯然失色。好比, Java 5中引入了 foreach 循環,因爲它的穩健性和簡潔性,已經替代了不少顯式使用迭代器的情形。Java 7中推出的菱形操做符( <> )讓你們在建立實例時無需顯式使用泛型,必定程度上推進了Java程序員們採用類型接口(type interface)進行程序設計。

對設計經驗的概括總結被稱爲設計模式。設計軟件時,若是你願意,能夠複用這些方式方法來解決一些常見問題。這看起來像傳統建築工程師的工做方式,對典型的場景(好比懸掛橋、拱橋等)都定義有可重用的解決方案。例如,訪問者模式經常使用於分離程序的算法和它的操做對象。單例模式通常用於限制類的實例化,僅生成一份對象

Lambda表達式爲程序員的工具箱又新添了一件利器。它們爲解決傳統設計模式所面對的問題提供了新的解決方案,不但如此,採用這些方案每每更高效、更簡單。使用Lambda表達式後,不少現存的略顯臃腫的面向對象設計模式可以用更精簡的方式實現了。這一節中,咱們會針對五個設計模式展開討論,它們分別是:

  • 策略模式
  • 模板方法
  • 觀察者模式
  • 責任鏈模式
  • 工廠模式

咱們會展現Lambda表達式是如何另闢蹊徑解決設計模式原來試圖解決的問題的。

策略模式

策略模式表明瞭解決一類算法的通用解決方案,你能夠在運行時選擇使用哪一種方案。在第2章中你已經簡略地瞭解過這種模式了,當時咱們介紹瞭如何使用不一樣的條件(好比蘋果的重量,或者顏色)來篩選庫存中的蘋果。你能夠將這一模式應用到更普遍的領域,好比使用不一樣的標準來驗證輸入的有效性,使用不一樣的方式來分析或者格式化輸入。

策略模式包含三部份內容:

  • 一個表明某個算法的接口(它是策略模式的接口)。
  • 一個或多個該接口的具體實現,它們表明了算法的多種實現(好比,實體類 ConcreteStrategyA 或者 ConcreteStrategyB )。
  • 一個或多個使用策略對象的客戶。

咱們假設你但願驗證輸入的內容是否根據標準進行了恰當的格式化(好比只包含小寫字母或數字)。你能夠從定義一個驗證文本(以 String 的形式表示)的接口入手:

interface ValidationStrategy {
    boolean execute(String s);
}

其次,你定義了該接口的一個或多個具體實現:

static class IsAllLowerCase implements ValidationStrategy {

    @Override
    public boolean execute(String s) {
        return s.matches("[a-z]+");
    }
}

static class IsNumeric implements ValidationStrategy {

    @Override
    public boolean execute(String s) {
        return s.matches("\\d+");
    }
}

以後,你就能夠在你的程序中使用這些略有差別的驗證策略了:

private static class Validator {
    private final ValidationStrategy validationStrategy;

    public Validator(ValidationStrategy validationStrategy) {
        this.validationStrategy = validationStrategy;
    }

    public boolean validate(String s) {
        return validationStrategy.execute(s);
    }
}

Validator v1 = new Validator(new IsNumeric());
// false
System.out.println(v1.validate("aaaa"));
Validator v2 = new Validator(new IsAllLowerCase());
// true
System.out.println(v2.validate("bbbb"));

使用Lambda表達式
到如今爲止,你應該已經意識到 ValidationStrategy 是一個函數接口了(除此以外,它還與 Predicate<String> 具備一樣的函數描述)。這意味着咱們不須要聲明新的類來實現不一樣的策略,經過直接傳遞Lambda表達式就能達到一樣的目的,而且還更簡潔:

Validator v3 = new Validator((String s) -> s.matches("\\d+"));
System.out.println(v3.validate("aaaa"));
Validator v4 = new Validator((String s) -> s.matches("[a-z]+"));
System.out.println(v4.validate("bbbb"));

正如你看到的,Lambda表達式避免了採用策略設計模式時僵化的模板代碼。若是你仔細分析一下箇中原因,可能會發現,Lambda表達式實際已經對部分代碼(或策略)進行了封裝,而這就是建立策略設計模式的初衷。所以,咱們強烈建議對相似的問題,你應該儘可能使用Lambda表達式來解決。

模板方法

若是你須要採用某個算法的框架,同時又但願有必定的靈活度,能對它的某些部分進行改進,那麼採用模板方法設計模式是比較通用的方案。好吧,這樣講聽起來有些抽象。換句話說,模板方法模式在你「但願使用這個算法,可是須要對其中的某些行進行改進,才能達到但願的效果」時是很是有用的。

讓咱們從一個例子着手,看看這個模式是如何工做的。假設你須要編寫一個簡單的在線銀行應用。一般,用戶須要輸入一個用戶帳戶,以後應用才能從銀行的數據庫中獲得用戶的詳細信息,最終完成一些讓用戶滿意的操做。不一樣分行的在線銀行應用讓客戶滿意的方式可能還略有不一樣,好比給客戶的帳戶發放紅利,或者僅僅是少發送一些推廣文件。你可能經過下面的抽象類方式來實如今線銀行應用:

public abstract class AbstractOnlineBanking {
    public void processCustomer(int id) {
        Customer customer = Database.getCustomerWithId(id);
        makeCustomerHappy(customer);
    }

    /**
     * 讓客戶滿意
     *
     * @param customer
     */
    abstract void makeCustomerHappy(Customer customer);

    private static class Customer {}

    private static class Database {
        static Customer getCustomerWithId(int id) {
            return new Customer();
        }
    }
}

processCustomer 方法搭建了在線銀行算法的框架:獲取客戶提供的ID,而後提供服務讓用戶滿意。不一樣的支行能夠經過繼承 AbstractOnlineBanking 類,對該方法提供差別化的實現。

使用Lambda表達式
使用你偏心的Lambda表達式一樣也能夠解決這些問題(建立算法框架,讓具體的實現插入某些部分)。你想要插入的不一樣算法組件能夠經過Lambda表達式或者方法引用的方式實現。

這裏咱們向 processCustomer 方法引入了第二個參數,它是一個 Consumer<Customer> 類型的參數,與前文定義的 makeCustomerHappy 的特徵保持一致:

public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) {
    Customer customer = Database.getCustomerWithId(id);
    makeCustomerHappy.accept(customer);
}

如今,你能夠很方便地經過傳遞Lambda表達式,直接插入不一樣的行爲,再也不須要繼承AbstractOnlineBanking 類了:

public static void main(String[] args) {
        new AbstractOnlineBankingLambda().processCustomer(1337, (
            AbstractOnlineBankingLambda.Customer c) -> System.out.println("Hello!"));
    }

這是又一個例子,佐證了Lamba表達式能幫助你解決設計模式與生俱來的設計僵化問題。

觀察者模式

觀察者模式是一種比較常見的方案,某些事件發生時(好比狀態轉變),若是一個對象(一般咱們稱之爲主題)須要自動地通知其餘多個對象(稱爲觀察者),就會採用該方案。建立圖形用戶界面(GUI)程序時,你常常會使用該設計模式。這種狀況下,你會在圖形用戶界面組件(好比按鈕)上註冊一系列的觀察者。若是點擊按鈕,觀察者就會收到通知,並隨即執行某個特定的行爲。 可是觀察者模式並不侷限於圖形用戶界面。好比,觀察者設計模式也適用於股票交易的情形,多個券商可能都但願對某一支股票價格(主題)的變更作出響應。

讓咱們寫點兒代碼來看看觀察者模式在實際中多麼有用。你須要爲Twitter這樣的應用設計並實現一個定製化的通知系統。想法很簡單:好幾家報紙機構,好比《紐約時報》《衛報》以及《世界報》都訂閱了新聞,他們但願當接收的新聞中包含他們感興趣的關鍵字時,能獲得特別通知。

首先,你須要一個觀察者接口,它將不一樣的觀察者聚合在一塊兒。它僅有一個名爲 notify 的方法,一旦接收到一條新的新聞,該方法就會被調用:

interface Observer{
    void inform(String tweet);
}

如今,你能夠聲明不一樣的觀察者(好比,這裏是三家不一樣的報紙機構),依據新聞中不一樣的關鍵字分別定義不一樣的行爲:

private static class NYTimes implements Observer {

    @Override
    public void inform(String tweet) {
        if (tweet != null && tweet.contains("money")) {
            System.out.println("Breaking news in NY!" + tweet);
        }
    }
}

private static class Guardian implements Observer {

    @Override
    public void inform(String tweet) {
        if (tweet != null && tweet.contains("queen")) {
            System.out.println("Yet another news in London... " + tweet);
        }
    }
}

private static class LeMonde implements Observer {

    @Override
    public void inform(String tweet) {
        if(tweet != null && tweet.contains("wine")){
            System.out.println("Today cheese, wine and news! " + tweet);
        }
    }
}

你還遺漏了最重要的部分: Subject !讓咱們爲它定義一個接口:

interface Subject {
    void registerObserver(Observer o);

    void notifyObserver(String tweet);
}

Subject 使用 registerObserver 方法能夠註冊一個新的觀察者,使用 notifyObservers方法通知它的觀察者一個新聞的到來。讓咱們更進一步,實現 Feed 類:

private static class Feed implements Subject {
    private final List<Observer> observers = new ArrayList<>();

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void notifyObserver(String tweet) {
        observers.forEach(o -> o.inform(tweet));
    }
}

這是一個很是直觀的實現: Feed 類在內部維護了一個觀察者列表,一條新聞到達時,它就進行通知。

絕不意外,《衛報》會特別關注這條新聞!

使用Lambda表達式
你可能會疑惑Lambda表達式在觀察者設計模式中如何發揮它的做用。不知道你有沒有注意到, Observer 接口的全部實現類都提供了一個方法: inform 。新聞到達時,它們都只是對同一段代碼封裝執行。Lambda表達式的設計初衷就是要消除這樣的僵化代碼。使用Lambda表達式後,你無需顯式地實例化三個觀察者對象,直接傳遞Lambda表達式表示須要執行的行爲便可:

Feed feedLambda = new Feed();
feedLambda.registerObserver((String tweet) -> {
    if (tweet != null && tweet.contains("money")) {
        System.out.println("Breaking news in NY!" + tweet);
    }
});

feedLambda.registerObserver((String tweet) -> {
    if (tweet != null && tweet.contains("queen")) {
        System.out.println("Yet another news in London... " + tweet);
    }
});

feedLambda.notifyObserver("Money money money, give me money!");

那麼,是否咱們隨時隨地均可以使用Lambda表達式呢?答案是否認的!咱們前文介紹的例子中,Lambda適配得很好,那是由於須要執行的動做都很簡單,所以才能很方便地消除僵化代碼。可是,觀察者的邏輯有可能十分複雜,它們可能還持有狀態,抑或定義了多個方法,諸如此類。在這些情形下,你仍是應該繼續使用類的方式。

責任鏈模式

責任鏈模式是一種建立處理對象序列(好比操做序列)的通用方案。一個處理對象可能須要在完成一些工做以後,將結果傳遞給另外一個對象,這個對象接着作一些工做,再轉交給下一個處理對象,以此類推。

一般,這種模式是經過定義一個表明處理對象的抽象類來實現的,在抽象類中會定義一個字段來記錄後續對象。一旦對象完成它的工做,處理對象就會將它的工做轉交給它的後繼。代碼中,這段邏輯看起來是下面這樣:

private static abstract class AbstractProcessingObject<T> {
    protected AbstractProcessingObject<T> successor;

    public void setSuccessor(AbstractProcessingObject<T> successor) {
        this.successor = successor;
    }

    public T handle(T input) {
        T r = handleWork(input);
        if (successor != null) {
            return successor.handle(r);
        }
        return r;
    }

    protected abstract T handleWork(T input);
}

下面讓咱們看看如何使用該設計模式。你能夠建立兩個處理對象,它們的功能是進行一些文本處理工做。

private static class HeaderTextProcessing extends AbstractProcessingObject<String> {

    @Override
    protected String handleWork(String text) {
        return "From Raoul, Mario and Alan: " + text;
    }
}

private static class SpellCheckerProcessing extends AbstractProcessingObject<String> {

    @Override
    protected String handleWork(String text) {
        return text.replaceAll("labda", "lambda");
    }
}

如今你就能夠將這兩個處理對象結合起來,構造一個操做序列!

AbstractProcessingObject<String> p1 = new HeaderTextProcessing();
AbstractProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2);
String result = p1.handle("Aren't labdas really sexy?!!");
System.out.println(result);

使用Lambda表達式
稍等!這個模式看起來像是在連接(也便是構造) 函數。第3章中咱們探討過如何構造Lambda表達式。你能夠將處理對象做爲函數的一個實例,或者更確切地說做爲 UnaryOperator<String> 的一個實例。爲了連接這些函數,你須要使用 andThen 方法對其進行構造。

UnaryOperator<String> headerProcessing =
            (String text) -> "From Raoul, Mario and Alan: " + text;

UnaryOperator<String> spellCheckerProcessing =
        (String text) -> text.replaceAll("labda", "lambda");

Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);

String result2 = pipeline.apply("Aren't labdas really sexy?!!");
System.out.println(result2);

工廠模式

使用工廠模式,你無需向客戶暴露實例化的邏輯就能完成對象的建立。好比,咱們假定你爲一家銀行工做,他們須要一種方式建立不一樣的金融產品:貸款、期權、股票,等等。

一般,你會建立一個工廠類,它包含一個負責實現不一樣對象的方法,以下所示:

private interface Product {
}

private static class ProductFactory {
    public static Product createProduct(String name) {
        switch (name) {
            case "loan":
                return new Loan();
            case "stock":
                return new Stock();
            case "bond":
                return new Bond();
            default:
                throw new RuntimeException("No such product " + name);
        }
    }
}

static private class Loan implements Product {
}

static private class Stock implements Product {
}

static private class Bond implements Product {
}

這裏貸款( Loan )、股票( Stock )和債券( Bond )都是產品( Product )的子類。createProduct 方法能夠經過附加的邏輯來設置每一個建立的產品。可是帶來的好處也顯而易見,你在建立對象時不用再擔憂會將構造函數或者配置暴露給客戶,這使得客戶建立產品時更加簡單:

Product p1 = ProductFactory.createProduct("loan");

使用Lambda表達式
第3章中,咱們已經知道能夠像引用方法同樣引用構造函數。好比,下面就是一個引用貸款( Loan )構造函數的示例:

Supplier<Product> loanSupplier = Loan::new;
Product p2 = loanSupplier.get();

經過這種方式,你能夠重構以前的代碼,建立一個 Map ,將產品名映射到對應的構造函數:

final static private Map<String, Supplier<Product>> map = new HashMap<>();

static {
    map.put("loan", Loan::new);
    map.put("stock", Stock::new);
    map.put("bond", Bond::new);
}

如今,你能夠像以前使用工廠設計模式那樣,利用這個 Map 來實例化不一樣的產品。

public static Product createProductLambda(String name) {
    Supplier<Product> p = map.get(name);
    if (p != null) {
        return p.get();
    }
    throw new RuntimeException("No such product " + name);
}

這是個全新的嘗試,它使用Java 8中的新特性達到了傳統工廠模式一樣的效果。可是,若是工廠方法 createProduct 須要接收多個傳遞給產品構造方法的參數,這種方式的擴展性不是很好。你不得不提供不一樣的函數接口,沒法採用以前統一使用一個簡單接口的方式。

好比,咱們假設你但願保存具備三個參數(兩個參數爲 Integer 類型,一個參數爲 String類型)的構造函數;爲了完成這個任務,你須要建立一個特殊的函數接口 TriFunction 。最終的結果是 Map 變得更加複雜。

public interface TriFunction<T, U, V, R>{
    R apply(T t, U u, V v);
}
Map<String, TriFunction<Integer, Integer, String, Product>> map = new HashMap<>();

你已經瞭解瞭如何使用Lambda表達式編寫和重構代碼。接下來,咱們會介紹如何確保新編
寫代碼的正確性。

測試 Lambda 表達式

如今你的代碼中已經充溢着Lambda表達式,看起來不錯,也很簡潔。可是,大多數時候,咱們受僱進行的程序開發工做的要求並非編寫優美的代碼,而是編寫正確的代碼。

一般而言,好的軟件工程實踐必定少不了單元測試,藉此保證程序的行爲與預期一致。你編寫測試用例,經過這些測試用例確保你代碼中的每一個組成部分都實現預期的結果。好比,圖形應用的一個簡單的 Point 類,能夠定義以下:

public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public Point moveRightBy(int x) {
        return new Point(this.x + x, this.y);
    }
}

下面的單元測試會檢查 moveRightBy 方法的行爲是否與預期一致:

public class PointTest {

    @Test
    public void testMoveRightBy() {
        Point p1 = new Point(5, 5);
        Point p2 = p1.moveRightBy(10);

        Assert.assertEquals(15, p2.getX());
        Assert.assertEquals(5, p2.getY());
    }
}

測試可見 Lambda 函數的行爲

因爲 moveRightBy 方法聲明爲public,測試工做變得相對容易。你能夠在用例內部完成測試。可是Lambda並沒有函數名(畢竟它們都是匿名函數),所以要對你代碼中的Lambda函數進行測試實際上比較困難,由於你沒法經過函數名的方式調用它們。

有些時候,你能夠藉助某個字段訪問Lambda函數,這種狀況,你能夠利用這些字段,經過它們對封裝在Lambda函數內的邏輯進行測試。好比,咱們假設你在 Point 類中添加了靜態字段compareByXAndThenY ,經過該字段,使用方法引用你能夠訪問 Comparator 對象:

public class Point {
    public final static Comparator<Point> COMPARE_BY_X_AND_THEN_Y =
            comparing(Point::getX).thenComparing(Point::getY);
    ...
}

還記得嗎,Lambda表達式會生成函數接口的一個實例。由此,你能夠測試該實例的行爲。這個例子中,咱們可使用不一樣的參數,對 Comparator 對象類型實例 compareByXAndThenY的 compare 方法進行調用,驗證它們的行爲是否符合預期:

@Test
public void testComparingTwoPoints() {
    Point p1 = new Point(10, 15);
    Point p2 = new Point(10, 20);
    int result = Point.COMPARE_BY_X_AND_THEN_Y.compare(p1 , p2);
    Assert.assertEquals(-1, result);
}

測試使用 Lambda 的方法的行爲

可是Lambda的初衷是將一部分邏輯封裝起來給另外一個方法使用。從這個角度出發,你不該該將Lambda表達式聲明爲public,它們僅是具體的實現細節。相反,咱們須要對使用Lambda表達式的方法進行測試。好比下面這個方法 moveAllPointsRightBy :

public static List<Point> moveAllPointsRightBy(List<Point> points, int x) {
    return points.stream()
            .map(p -> new Point(p.getX() + x, p.getY()))
            .collect(toList());
}

咱們不必對Lambda表達式 p -> new Point(p.getX() + x,p.getY()) 進行測試,它只是 moveAllPointsRightBy 內部的實現細節。咱們更應該關注的是方法 moveAllPointsRightBy 的行爲:

@Test
public void testMoveAllPointsRightBy() {
    List<Point> points =
            Arrays.asList(new Point(5, 5), new Point(10, 5));
    List<Point> expectedPoints =
            Arrays.asList(new Point(15, 5), new Point(20, 5));
    List<Point> newPoints = Point.moveAllPointsRightBy(points, 10);
    Assert.assertEquals(expectedPoints, newPoints);
}

注意,上面的單元測試中, Point 類恰當地實現 equals 方法很是重要,不然該測試的結果就取決於 Object 類的默認實現。

調試

調試有問題的代碼時,程序員的兵器庫裏有兩大老式武器,分別是:

  • 查看棧跟蹤
  • 輸出日誌

查看棧跟蹤

你的程序忽然中止運行(好比忽然拋出一個異常),這時你首先要調查程序在什麼地方發生了異常以及爲何會發生該異常。這時棧幀就很是有用。程序的每次方法調用都會產生相應的調用信息,包括程序中方法調用的位置、該方法調用使用的參數、被調用方法的本地變量。這些信息被保存在棧幀上。

程序失敗時,你會獲得它的棧跟蹤,經過一個又一個棧幀,你能夠了解程序失敗時的概略信息。換句話說,經過這些你能獲得程序失敗時的方法調用列表。這些方法調用列表最終會幫助你發現問題出現的緣由。

Lambda表達式和棧跟蹤
不幸的是,因爲Lambda表達式沒有名字,它的棧跟蹤可能很難分析。在下面這段簡單的代碼中,咱們刻意地引入了一些錯誤:

public class Debugging {
    public static void main(String[] args) {
        List<Point> points = Arrays.asList(new Point(12, 2), null);
        points.stream().map(p -> p.getX()).forEach(System.out::println);
    }
}

運行這段代碼會產生下面的棧跟蹤:

12
Exception in thread "main" java.lang.NullPointerException
    // 這行中的 $0 是什麼意思?
    at xin.codedream.java8.chap8.Debugging.lambda$main$0(Debugging.java:15)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
    at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
    ...

這個程序出現了NPE(空指針異常)異常,由於 Points 列表的第二個元素是空( null )。

這時你的程序實際是在試圖處理一個空引用。因爲Stream流水線發生了錯誤,構成Stream流水線的整個方法調用序列都暴露在你面前了。不過,你留意到了嗎?棧跟蹤中還包含下面這樣相似加密的內容:

at xin.codedream.java8.chap8.Debugging.lambda$main$0(Debugging.java:15)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)

這些表示錯誤發生在Lambda表達式內部。因爲Lambda表達式沒有名字,因此編譯器只能爲它們指定一個名字。這個例子中,它的名字是 lambda$main$0 ,看起來很是不直觀。若是你使用了大量的類,其中又包含多個Lambda表達式,這就成了一個很是頭痛的問題。

即便你使用了方法引用,仍是有可能出現棧沒法顯示你使用的方法名的狀況。將以前的Lambda表達式 p-> p.getX() 替換爲方法引用 reference Point::getX 也會產生難於分析的棧跟蹤:

at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)

注意,若是方法引用指向的是同一個類中聲明的方法,那麼它的名稱是能夠在棧跟蹤中顯示的。好比,下面這個例子:

public class Debugging {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3);
        numbers.stream().map(Debugging::divideByZero).forEach(System
                .out::println);
    }

    public static int divideByZero(int n) {
        return n / 0;
    }
}

方法 divideByZero 在棧跟蹤中就正確地顯示了:

Exception in thread "main" java.lang.ArithmeticException: / by zero
    // divideByZero正確地輸出到棧跟蹤中
    at xin.codedream.java8.chap8.Debugging.divideByZero(Debugging.java:20)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
    ...

總的來講,咱們須要特別注意,涉及Lambda表達式的棧跟蹤可能很是難理解。這是Java編譯器將來版本能夠改進的一個方面。

使用日誌調試

假設你試圖對流操做中的流水線進行調試,該從何入手呢?你能夠像下面的例子那樣,使用forEach 將流操做的結果日誌輸出到屏幕上或者記錄到日誌文件中:

List<Integer> numbers = Arrays.asList(2, 3, 4, 5);
numbers.stream()
            .map(x -> x + 17)
            .filter(x -> x % 2 == 0)
            .limit(3)
            .forEach(System.out::println);

這段代碼的輸出以下:

20
22

不幸的是,一旦調用 forEach ,整個流就會恢復運行。到底哪一種方式能更有效地幫助咱們理解Stream流水線中的每一個操做(好比 map 、 filter 、 limit )產生的輸出?

這就是流操做方法 peek 大顯身手的時候。 peek 的設計初衷就是在流的每一個元素恢復運行以前,插入執行一個動做。可是它不像 forEach 那樣恢復整個流的運行,而是在一個元素上完操做以後,它只會將操做順承到流水線中的下一個操做。下面的這段代碼中,咱們使用 peek 輸出了Stream流水線操做以前和操做以後的中間值:

List<Integer> result = Stream.of(2, 3, 4, 5)
                .peek(x -> System.out.println("taking from stream: " + x)).map(x -> x + 17)
                .peek(x -> System.out.println("after map: " + x)).filter(x -> x % 2 == 0)
                .peek(x -> System.out.println("after filter: " + x)).limit(3)
                .peek(x -> System.out.println("after limit: " + x)).collect(toList());

經過 peek 操做咱們能清楚地瞭解流水線操做中每一步的輸出結果:

taking from stream: 2
after map: 19
taking from stream: 3
after map: 20
after filter: 20
after limit: 20
taking from stream: 4
after map: 21
taking from stream: 5
after map: 22
after filter: 22
after limit: 22

小結

  • Lambda表達式能提高代碼的可讀性和靈活性。
  • 若是你的代碼中使用了匿名類,儘可能用Lambda表達式替換它們,可是要注意兩者間語義的微妙差異,好比關鍵字 this ,以及變量隱藏。
  • 跟Lambda表達式比起來,方法引用的可讀性更好 。
  • 儘可能使用Stream API替換迭代式的集合處理。
  • Lambda表達式有助於避免使用面向對象設計模式時容易出現的僵化的模板代碼,典型的好比策略模式、模板方法、觀察者模式、責任鏈模式,以及工廠模式。
  • 即便採用了Lambda表達式,也一樣能夠進行單元測試,可是一般你應該關注使用了Lambda表達式的方法的行爲。
  • 儘可能將複雜的Lambda表達式抽象到普通方法中。
  • Lambda表達式會讓棧跟蹤的分析變得更爲複雜。
  • 流提供的 peek 方法在分析Stream流水線時,能將中間變量的值輸出到日誌中,是很是有用的工具。

代碼

Github:chap8

Gitee:chap8

相關文章
相關標籤/搜索