1. 程式人生 > >[Java 8] (5) 使用Lambda表示式進行設計

[Java 8] (5) 使用Lambda表示式進行設計

使用Lambda表示式進行設計

在前面的幾篇文章中,我們已經見識到了Lambda表示式是如何讓程式碼變的更加緊湊和簡潔的。

這一篇文章主要會介紹Lambda表示式如何改變程式的設計,如何讓程式變的更加輕量級和簡潔。如何讓介面的使用變得更加流暢和直觀。

使用Lambda表示式來實現策略模式

假設現在有一個Asset型別是這樣的:

public class Asset {
    public enum AssetType { BOND, STOCK };
    private final AssetType type;
    private final int value;
    public
Asset(final AssetType assetType, final int assetValue) { type = assetType; value = assetValue; } public AssetType getType() { return type; } public int getValue() { return value; } }

每一個資產都有一個種類,使用列舉型別表示。同時資產也有其價值,使用整型型別表示。

如果我們想得到所有資產的價值,那麼使用Lambda可以輕鬆實現如下:

public static
int totalAssetValues(final List<Asset> assets) { return assets.stream() .mapToInt(Asset::getValue) .sum(); }

雖然上述程式碼能夠很好的完成計算資產總值這一任務,但是仔細分析會發現這段程式碼將以下三個任務給放在了一起:

  1. 如何遍歷
  2. 計算什麼
  3. 如何計算

如何現在來了一個新需求,要求計算Bond型別的資產總值,那麼很直觀的我們會考慮將上段程式碼複製一份然後有針對性地進行修改:

public static int totalBondValues
(final List<Asset> assets) { return assets.stream() .mapToInt(asset -> asset.getType() == AssetType.BOND ? asset.getValue() : 0) .sum(); }

而唯一不同的地方,就是傳入到mapToInt方法中的Lambda表示式。當然,也可以在mapToInt方法之前新增一個filter,過濾掉不需要的Stock型別的資產,這樣的話mapToInt中的Lambda表示式就可以不必修改了。

public static int totalBondValues(final List<Asset> assets) {
    return assets.stream()
        .filter(asset -> asset.getType == AssetType.BOND)
        .mapToInt(Asset::getValue)
        .sum();
}

這樣雖然實現了新需求,但是這種做法明顯地違反了DRY原則。我們需要重新設計它們來增強可重用性。 在計算Bond資產的程式碼中,Lambda表示式起到了兩個作用:

  1. 如何遍歷
  2. 如何計算

當使用面向物件設計時,我們會考慮使用策略模式(Strategy Pattern)來將上面兩個職責進行分離。但是這裡我們使用Lambda表示式進行實現:

public static int totalAssetValues(final List<Asset> assets, final Predicate<Asset> assetSelector) {
    return assets.stream().filter(assetSelector).mapToInt(Asset::getValue).sum();
}

重構後的方法接受了第二個引數,它是一個Predicate型別的函式式介面,很顯然它的作用就是來指定如何遍歷。這實際上就是策略模式在使用Lambda表示式時的一個簡單實現,因為Predicate表達的是一個行為,而這個行為本身就是一種策略。這種方法更加輕量級,因為它沒有額外建立其他的介面或者型別,只是重用了Java 8中提供的Predicate這一函式式介面而已。

比如,當我們需要計算所有資產的總值時,傳入的Predicate可以是這樣的:

System.out.println("Total of all assets: " + totalAssetValues(assets, asset -> true));

因此,在使用了Predicate自後,就將“如何遍歷”這個任務也分離出來了。因此,任務之間不再糾纏在一起,實現了單一職責的原則,自然而然就提高了重用性。

使用Lambda表示式實現組合(Composition)

在面向物件設計中,一般認為使用組合的方式會比使用繼承的方式更好,因為它減少了不必要的類層次。其實,使用Lambda表示式也能夠實現組合。

比如,在下面的CalculateNAV類中,有一個用來計算股票價值的方法:

public class CalculateNAV {
    public BigDecimal computeStockWorth(final String ticker, final int shares) {
        return priceFinder.apply(ticker).multiply(BigDecimal.valueOf(shares));
    }
    //... other methods that use the priceFinder ...
}

因為傳入的ticker是一個字串,代表的是股票的程式碼。而在計算中我們顯然需要的是股票的價格,所以priceFinder的型別很容易被確定為Function。因此我們可以這樣宣告CalculateNAV的建構函式:

private Function<String, BigDecimal> priceFinder;
public CalculateNAV(final Function<String, BigDecimal> aPriceFinder) {
    priceFinder = aPriceFinder;
}

實際上,上面的程式碼使用了一種設計模式叫做“依賴倒轉原則(Dependency Inversion Principle)”,使用依賴注入的方式將型別和具體的實現進行關聯,而不是直接將實現寫死到程式碼中,從而提高了程式碼的重用性。

為了測試CalculateNAV,可以使用JUnit:

public class CalculateNAVTest {
    @Test
    public void computeStockWorth() {
        final CalculateNAV calculateNAV = new CalculateNAV(ticker -> new BigDecimal("6.01"));
        BigDecimal expected = new BigDecimal("6010.00");
        assertEquals(0, calculateNAV.computeStockWorth("GOOG", 1000).compareTo(expected));
    }
    //...
}

當然,也可以使用真實的Web Service來得到某隻股票的價格:

public class YahooFinance {
    public static BigDecimal getPrice(final String ticker) {
        try {
            final URL url = new URL("http://ichart.finance.yahoo.com/table.csv?s=" + ticker);
            final BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));
            final String data = reader.lines().skip(1).findFirst().get();
            final String[] dataItems = data.split(",");
            return new BigDecimal(dataItems[dataItems.length - 1]);
        } catch(Exception ex) {
            throw new RuntimeException(ex);
        }
    }
}

這裡想說明的是在Java 8中,BufferedReader也有一個新方法叫做lines,目的是得到包含所有行資料的一個Stream物件,很明顯這也是為了讓該類和函數語言程式設計能夠更好的融合。

另外想說明的是是Lambda表示式和異常之間的關係。很明顯,當使用getPrice方法時,我們可以直接傳入方法引用:YahooFinance::getPrice。但是如果在呼叫此方法期間發生了異常該如何處理呢?上述程式碼在發生了異常時,將異常包裝成RuntimeException並重新丟擲。這樣做是因為只有當函式式介面中的方法本身聲明瞭會丟擲異常時(即聲明瞭throws XXX),才能夠丟擲受檢異常(Checked Exception)。而顯然在Function這一個函式式介面的apply方法中並未宣告可以丟擲的受檢異常,因此getPrice本身是不能丟擲受檢異常的,我們可以做的就是將異常封裝成執行時異常(非受檢異常),然後再丟擲。

使用Lambda表示式實現裝飾模式(Decorator Pattern)

裝飾模式本身並不複雜,但是在面向物件設計中實現起來並不輕鬆,因為使用它需要設計和實現較多的型別,這無疑增加了開發人員的負擔。比如JDK中的各種InputStream和OutputStream,在其上有各種各樣的型別用來裝飾它,所以最後I/O相關的型別被設計的有些過於複雜了,學習成本較高,要想正確而高效地使用它們並不容易。

使用Lambda表示式來實現裝飾模式,就相對地容易多了。在影象領域,濾鏡(Filter)實際上就是一種裝飾器(Decorator),我們會為一幅影象增加各種各樣的濾鏡,這些濾鏡的數量是不確定的,順序也是不確定的。

比如以下程式碼為攝像機對色彩的處理進行建模:

@SuppressWarnings("unchecked")
public class Camera {
    private Function<Color, Color> filter;
    public Color capture(final Color inputColor) {
        final Color processedColor = filter.apply(inputColor);
        //... more processing of color...
        return processedColor;
    }
    //... other functions that use the filter ...
}

目前只定義了一個filter。我們可以利用Function的compose和andThen來進行多個Function(也就是filter)的串聯操作:

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);
    return (V v) -> apply(before.apply(v));    
}

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}

可以發現,compose和andThen方法的區別僅僅在於串聯的順序。使用compose時,傳入的Function會被首先呼叫;使用andThen時,當前的Function會被首先呼叫。

因此,在Camera型別中,我們可以定義一個方法用來串聯不定數量的Filters:

public void setFilters(final Function<Color, Color>... filters) {
    filter = Arrays.asList(filters).stream()
        .reduce((current, next) -> current.andThen(next))
        .orElse(color -> color);
}

前面介紹過,由於reduce方法返回的物件是Optional型別的,因此當結果不存在時,需要進行特別處理。以上的orElse方法在結果不存在時會被呼叫來得到一個替代方案。那麼當setFilters方法沒有接受任何引數時,orElse就會被呼叫,color -> color的意義就是直接返回該color,不作任何操作。

實際上,Function介面中也定義了一個靜態方法identity用來處理需要直接返回自身的場景:

static <T> Function<T, T> identity() {
    return t -> t;
}

因此可以將上面的setFilters方法的實現改進成下面這樣:

public void setFilters(final Function<Color, Color>... filters) {
    filter = Arrays.asList(filters).stream()
        .reduce((current, next) -> current.andThen(next))
        .orElseGet(Function::identity);
}

orElse被替換成了orElseGet,兩者的定義如下:

public T orElse(T other) {
    return value != null ? value : other;
}

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}

前者當value為空時會直接返回傳入的引數other,而後者則是通過呼叫呼叫Supplier中的get方法來得到要返回的物件。這裡又出現了一個新的函式式介面Supplier:

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

它不需要任何引數,直接返回需要的物件。這一點和類的無參建構函式(有些情況下也被稱為工廠)有些類似。

說到了Supplier,就不能不提和它相對的Consumer函式介面,它們正好是一種互補的關係。Consumer中的accept方法會接受一個引數,但是沒有返回值。

現在,我們就可以使用Camera的濾鏡功能了:

final Camera camera = new Camera();
final Consumer<String> printCaptured = (filterInfo) ->
    System.out.println(String.format("with %s: %s", filterInfo,
        camera.capture(new Color(200, 100, 200))));

camera.setFilters(Color::brighter, Color::darker);
printCaptured.accept("brighter & darker filter");

在不知不覺中,我們在setFilters方法中實現了一個輕量級的裝飾模式(Decorator Pattern),不需要定義任何多餘的型別,只需要藉助Lambda表示式即可。

瞭解介面的default方法

在Java 8中,介面中也能夠擁有非抽象的方法了,這是一個非常重大的設計。那麼從Java編譯器的角度來看,default方法的解析有以下幾個規則:

  1. 子型別會自動擁有父型別中的default方法。
  2. 對於介面中實現的default方法,該介面的子型別能夠對該方法進行覆蓋。
  3. 類中的具體實現以及抽象的宣告,都會覆蓋所有實現介面型別中出現的default方法。
  4. 當兩個或者兩個以上的default方法實現中出現了衝突時,實現類需要解決這個衝突。

舉個例子來說明以上的規則:

public interface Fly {
    default void takeOff() { System.out.println("Fly::takeOff"); }
    default void land
            
           

相關推薦

[Java 8] (5) 使用Lambda表示式進行設計

使用Lambda表示式進行設計 在前面的幾篇文章中,我們已經見識到了Lambda表示式是如何讓程式碼變的更加緊湊和簡潔的。 這一篇文章主要會介紹Lambda表示式如何改變程式的設計,如何讓程式變的更加輕量級和簡潔。如何讓介面的使用變得更加流暢和直觀。 使用Lambd

Java 8Lambda表示式學習

Lambda表示式(也稱為閉包)是整個Java 8發行版中最受期待的在Java語言層面上的改變。 Lambda允許把函式作為一個方法的引數(函式作為引數傳遞進方法中),或者把程式碼看成資料:函式式程式設計師對這一概念非常熟悉。在JVM平臺上的很多語言(Groovy,Sc

採用Java 8Lambda表示式和預設方法的模板方法模式

原文連結 作者:   Mohamed Sanaulla  譯者: 李璟([email protected]) 模板方法模式是“四人幫”(譯者注:Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides)所著《Design 

Java 8新增Lambda表示式(6.8)

參考《Java瘋狂講義》 Lambda表示式支援將程式碼塊作為方法引數,Lambda表示式允許使用更簡潔的程式碼來建立只有一個抽象方法的介面(這種介面被稱為函式式介面)的例項 1. Lambda表示式入門 下面先使用匿名內部類來改寫(6.6介紹的命令模式Command表示式的例子)

Java 8Lambda表示式

要搞清楚何為Lambda表示式,首先要弄明白一個概念——函式式介面 函式式介面指的是隻有一個抽象方法的介面。 Lambda表示式的主要作用是:代替匿名內部類的繁瑣語法: 不需要指出重寫的方法的名字 不需要給出重寫的方法的返回值型別 只需要給出重寫的方法括

Java 8 Lambda表示式實現設計模式:命令模式

在這篇部落格裡,我將說明如何在使用 Java 8 Lambda表示式 的函數語言程式設計方式 時實現 命令 設計模式 。命令模式的目標是將請求封裝成一個物件,從對客戶端的不同型別請求,例如佇列或日誌請求引數化,並提供相應的操作。命令模式是一種通用程式設計方式,該方式基於執行

JDK 8Lambda表示式的使用

環境準備JDK 8Lambda表示式的語法基本語法:(parameters) -> expression或(parameters) ->{ statements; }下面是Java lambda表示式的簡單例子: // 1. 不需要引數,返回值為 5 () -> 5

Java Builder模式 Lambda表示式 Java8 lambda表示式10個示例

Java Builder模式 package com.cathay;/** * @Description * @TODO * @Author [email protected] * @Date 建立時間:2018/11/16 **/public class Person { private

java核心技術——lambda表示式

lambda表示式是一個可傳遞的程式碼塊,以及必須傳入程式碼的變數規範。 形式:引數 () 箭頭 ->  表示式 {....} 如果可以推匯出一個lambda表示式的引數型別,則可以忽略器型別: Comparator<String> comp = (first,

Java裡的lambda表示式

在上一篇文章《Java裡的函式式介面》介紹了關於函式式介面的內容,那麼本文基於函式式介面來繼續學習lambda表示式。 語法結構 Runnable runnable = () -> System.out.println("Runnable Instance"); 這種使用箭頭符

通俗理解Java中的Lambda表示式

Lambda Lambda表示式支援將程式碼塊作為方法引數, 允許使用更為簡潔的方式實現抽象類或介面的抽象方法, 而不再是通過匿名內部類的方式, 它具有對某一方法重寫或實現的功能; 接下來通過一個簡單的例子瞭解一下 public class LambaExpre

java中的Lambda表示式

什麼是Lambda表示式? Lambda是Java8的重要更新,Lambda表示式支援將程式碼塊作為方法引數,Lambda表示式允許使用更簡潔的程式碼來建立只有一個抽象方法的介面的例項。 入門 案例:對陣列進行操作 定義運算元組的命令的介面 package or

jdk1.8特性——lambda表示式、stream學習,結合使用

       最近再專案中用到了lambda和Stream,發現用起來程式碼很簡潔,就是有些複雜點的可能用完後可讀性不是很理想,但是簡單點的還是很好理解的,因此專門試了試,感覺真的很棒~先來了解一下 一:lambda表示式         lambda語法:     1

java 8Lambda 五種語法格式

語法格式一:無參、無返回值,lambda體只需一條語句 Runnable r1 =() -> System.out.print("hello"); 以往程式碼如下: Runnable runnable = new Runnable() { @Overrid

深入學習java原始碼之lambda表示式與函式式介面

深入學習java原始碼之lambda表示式與函式式介面 @FunctionalInterface JDK中的函式式介面舉例 java.lang.Runnable, java.awt.event.ActionListener,  java.util.Comparator, java.ut

Java™ 教程(Lambda表示式

Lambda表示式 匿名類的一個問題是,如果匿名類的實現非常簡單,例如只包含一個方法的介面,那麼匿名類的語法可能看起來不實用且不清楚,在這些情況下,你通常會嘗試將方法作為引數傳遞給另一個方法,例如當有人單擊按鈕時應採取的操作,Lambda表示式使你可以執行此操作,將

8000字長文讓你徹底瞭解 Java 8Lambda、函式式介面、Stream 用法和原理

> 我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程式設計師鼓勵師,一個本打算寫詩卻寫起了程式碼的田園碼農! 文章會收錄在 [JavaNewBee](https://github.com/huzhicheng/JavaNewBee) 中,更有 Java 後端知識圖譜,從小白到大牛要走的路都在裡面。公眾號

Java 中的 Lambda 表示式

Lambda表示式   Lambda 表示式是 JDK1.8 的一個新特性,又稱特殊的匿名內部類,可以取代大部分的匿名內部類,語法更簡潔,可以寫出更優雅的 Java 程式碼,可以極大地優化程式碼結構。   Lambda 表示式不會生成單獨的內部類檔案,但匿名內部類會。   Lambda表示式特性

使用idea進行maven install老是報-source 1.5 中不支援 lambda 表示式

1、idea的maven設定 2、JDK配置 3、專案模組配置 以上設定均正常,但是在執行maven的install還是報錯:-source 1.5 中不支援 lambda 表示式 另外發現在執行這個步驟,原來的jdk設定會還原成1.5版本 因為程式

Java 8新特性—02.Lambda 表示式基礎語法

Lambda 表示式的基礎語法:Java8中引入了一個新的操作符“->” 該操作符稱為箭頭操作符或Lambda操作符, 該操作符將Lambda表示式拆分為兩部分: 左側:Lambda 表示式的引數部分 右側:Lamdba 表示式中所需執行的功能,即Lambda 體。