【修煉內功】[Java8] Lambda表達式帶來的編程新思路

java-lambda-expressions-tutorial.jpg

該文章已收錄 【修煉內功】躍遷之路java

Lambda表達式,能夠理解爲簡潔地表示可傳遞的匿名函數的一種方式:它沒有名稱,但它有參數列表、函數主體、返回類型。

這裏,默認您已對Java8的Lambda表達式有必定了解,而且知道如何使用。程序員


Java8中引入的Lambda表達式,爲編程體驗及效率帶來了極大的提高。express

0x00 行爲參數化

行爲參數化,是理解函數式編程的一個重要概念。簡單來講即是,一個方法接受多個不一樣的行爲做爲參數,並在內部使用它們,完成不一樣行爲的能力。更爲通俗的講,行爲參數化是指,定義一段代碼,這段代碼並不會當即執行,而是能夠像普通變量/參數同樣進行傳遞,被程序的其餘部分調用。編程

咱們經過一個特別通用的篩選蘋果的例子,來逐步瞭解如何使用Lambda表達式實現行爲參數化。(若是對行爲參數化已十分了解,可直接跳過本節)segmentfault

  • 需求1:篩選綠色蘋果

咱們須要將倉庫中綠色的蘋果過濾出來,對於這樣的問題,大多數人來講都是手到擒來 (step1: 面向過程)設計模式

public static List<Apple> filterGreenApples(List<Apple> apples) {
    List<apple> filteredApples = new LinkedList<>();
    for (Apple apple: apples) {
        if ("green".equals(apple.getColor())) {
            filteredApples.add(apple);
        }
    }
    return filteredApples;
}

List<Apple> greenApples = filterGreenApples(inventory);
  • 需求2:篩選任意顏色蘋果

對於這樣的需求變動,可能也不是很難閉包

public static List<Apple> filterApplesByColor(List<Apple> apples, String color) {
    List<apple> filteredApples = new LinkedList<>();
    for (Apple apple: apples) {
        if (color.equals(apple.getColor())) {
            filteredApples.add(apple);
        }
    }
    return filteredApples;
}

List<Apple> someColorApples = filterApplesByColor(inventory, "red");
  • 需求3:篩選重量大於150克的蘋果

有了先前的教訓,可能會學聰明一些,不會把重量直接寫死到程序裏,而是提供一個入參app

public static List<Apple> filterApplesByWeight(List<Apple> apples, int minWeight) {
    List<apple> filteredApples = new LinkedList<>();
    for (Apple apple: apples) {
        if (apple.getWeight() > minWeight) {
            filteredApples.add(apple);
        }
    }
    return filteredApples;
}

List<Apple> heavyApples = filterApplesByColor(inventory, 150);
  • 需求4:篩選顏色爲紅色且重量大於150克的蘋果

若是照此下去,程序將變得異常難於維護,每一次小的需求變動,都須要進行大範圍的改動。爲了不永無休止的加班,對於瞭解設計模式的同窗,可能會將篩選邏輯抽象出來 (step2: 面向對象)異步

public interface Predicate<Apple> {
    boolean test(Apple apple);
}

預先定義多種篩選策略,將策略動態的傳遞給篩選函數jvm

public static List<Apple> filterApples(List<Apple> apples, Predicate predicate) {
    List<apple> filteredApples = new LinkedList<>();
    for (Apple apple: apples) {
        if (predicate.test(apple)) {
            filteredApples.add(apple);
        }
    }
    return filteredApples;
}

Predicate predicate = new Predicate() {
    @override
    public boolean test(Apple apple) {
        return "red".equals(apple.getColor()) && apple.getWeight > 150;
    }
};

List<Apple> satisfactoryApples = filterApples(inventory, predicate);

或者直接使用匿名類,將篩選邏輯傳遞給篩選函數

List<Apple> satisfactoryApples = filterApples(inventory, new Predicate() {
    @override
    public boolean test(Apple apple) {
        return "red".equals(apple.getColor()) && apple.getWeight > 150;
    }
});

至此,已經能夠知足大部分的需求,但對於這種十分囉嗦、被Java程序員詬病了多年的語法,在Lambda表達式出現後,便出現了一絲起色 (step3: 面向函數)

@FunctionalInterface
public interface Predicate<Apple> {
    boolean test(Apple apple);
}

public List<Apple> filterApples(List<Apple> apples, Predicate<Apple> predicate) {
    return apples.stream.filter(predicate::test).collect(Collectors.toList());
}

List<Apple> satisfactoryApples = filterApples(inventory, apple -> "red".equals(apple.getColor()) && apple.getWeight > 150);

以上示例中使用了Java8的stream及lambda表達式,關於stream及lambda表達式的具體使用方法,這裏再也不贅述,重點在於解釋什麼是行爲參數化,示例中直接將篩選邏輯(紅色且重量大於150克)的代碼片斷做爲參數傳遞給了函數(確切的說是將lambda表達式做爲參數傳遞給了函數),而這段代碼片斷會交由篩選函數進行執行。

Lambda表達式與匿名類很像,但本質不一樣,關於Lambda表達式及匿名類的區別,會在以後的文章詳細介紹

若是想讓代碼更爲簡潔明瞭,能夠繼續將篩選邏輯提取爲函數,使用方法引用進行參數傳遞

private boolean isRedColorAndWeightGt150(Apple apple) {
    return "red".equals(apple.getColor()) && apple.getWeight > 150;
}

List<Apple> satisfactoryApples = filterApples(inventory, this::isRedColorAndWeightGt150);

至此,咱們完成了從面向過程面向對象再到面向函數的編程思惟轉變,代碼也更加具備語義化,不管是代碼閱讀仍是維護,都較以前有了很大的提高

等等,若是須要過濾顏色爲黃色而且重量在180克以上的蘋果,是否是還要定義一個isYellowColorAndWeightGt180的函數出來,貌似又陷入了無窮加班的怪圈~

還有沒有優化空間?可否將篩選條件抽離到單一屬性,如byColorbyMinWeight等,以後再作與或計算傳遞給篩選函數?

接下來就是咱們要介紹的高階函數

0x01 高階函數

高階函數是一個函數,它接收函數做爲參數或將函數做爲輸出返回

  • 接受至少一個函數做爲參數
  • 返回的結果是一個函數

以上的定義聽起來可能有些繞口。結合上節示例,咱們的訴求是將蘋果的顏色、重量或者其餘篩選條件也抽離出來,而不是硬編碼到代碼中

private Predicate<apple> byColor(String color) {
    return (apple) -> color.equals(apple.getColor);
}

private Predicate<Apple> byMinWeight(int minWeight) {
    return (apple) -> apple.getWeight > minWeight;
}

以上兩個函數的返回值,均爲Predicate類型的Lambda表達式,或者能夠說,以上兩個函數的返回值也是函數

接下來咱們定義與運算,只有傳入的全部條件均知足纔算最終知足

private Predicate<Apple> allMatches(Predicate<Apple> ...predicates) {
    return (apple) -> predicates.stream.allMatch(predicate -> predicate.test(apple));
}

以上函數,是將多個篩選邏輯作與計算,注意,該函數接收多個函數(Lambda)做爲入參,並返回一個函數(Lambda),這即是高階函數

如何使用該函數?做爲蘋果篩選示例的延伸,咱們能夠將上一節最後一個示例代碼優化以下

List<Apple> satisfactoryApples = filterApples(inventory, allMatches(byColor("red"), byMinWeight(150)));

至此,還能夠抽象出anyMatchesnonMatches等高階函數,組合使用

// 篩選出 顏色爲紅色 而且 重量在150克以上 而且 採摘時間在1周之內 而且 產地在中國、美國、加拿大任意之一的蘋果
List<Apple> satisfactoryApples = filterApples(
    inventory, 
    allMatches(
        byColor("red"), 
        byMinWeight(150),
        apple -> apple.pluckingTime - currentTimeMillis() < 7L * 24 * 3600 * 1000,
        anyMatches(byGardens("中國"), byGardens("美國"), byGardens("加拿大")
    )
);

若是使用jvm包中的java.util.function.Predicate,咱們還能夠繼續優化,使代碼更爲語義化

// 篩選出 顏色爲紅色 而且 重量在150克以上 而且 採摘時間在1周之內 而且 產地在中國、美國、加拿大任意之一的蘋果
List<Apple> satisfactoryApples = filterApples(
    inventory, 
    byColor("red")
      .and(byMinWeight(150))
      .and(apple -> apple.pluckingTime - currentTimeMillis() < 7L * 24 * 3600 * 1000)
      .and(byGardens("中國").or(byGardens("美國").or(byGardens("加拿大")))
);

這裏使用了Java8中的默認函數,默認函數容許你在接口interface中定義函數的默認行爲,從某方面來說也能夠實現類的多繼承

示例中,and/or函數接收一個Predicate函數(Lambda表達式)做爲參數,並返回一個Predicate函數(Lambda表達式),一樣爲高階函數

關於默認函數的使用,會在以後的文章詳細介紹

0x02 閉包

閉包(Closure),可以讀取其餘函數內部變量的函數

又是一個比較抽象的概念,其實在使用Lambda表達式的過程當中,常常會使用到閉包,好比

public Future<List<Apple>> filterApplesAsync() {
    List<Apple> inventory = getInventory();
    
    return CompletableFuture.supplyAsync(() -> filterApples(inventory, byColor("red")));
}

在提交異步任務時,傳入了內部函數(Lambda表達式),在內部函數中使用了父函數filterApplesAsync中的局部變量inventory,這即是閉包

若是該示例不夠明顯的話,能夠參考以下示例

private Supplier<Integer> initIntIncreaser(int i) {
    AtomicInteger atomicInteger = new AtomicInteger(i);
    return () -> atomicInteger.getAndIncrement();
}

Supplier<Integer> increaser = initIntIncreaser(1);
System.out.println(increaser.get());
System.out.println(increaser.get());
System.out.println(increaser.get());
System.out.println(increaser.get());

//[out]: 1
//[out]: 2
//[out]: 3
//[out]: 4

initIntIncreaser函數返回另外一個函數(內部函數),該函數(increaser)使用了父函數initIntIncreaser的局部變量atomicInteger,該變量會被函數increaser持有,而且會在調用increaser時使用(更改)

0x03 柯里化

柯里化(Currying),是把接受多個參數的函數變換成接受一個單一參數的函數。

柯里化是逐步傳值,逐步縮小函數的適用範圍,逐步求解的過程。

如,設計一個函數,實如今延遲必定時間以後執行給定邏輯,並能夠指定執行的執行器

public ScheduledFuture executeDelay(Runnable runnable, ScheduledExecutorService scheduler, long delay, TimeUnit timeunit) {
    return scheduler.schedule(runnable, delay, timeunit);
}

目前有一批任務,須要使用執行器scheduler1,而且均延遲5分鐘執行

另外一批任務,須要使用執行器scheduler2,而且均延遲15分鐘執行

能夠這樣實現

executeDelay(runnable11, scheduler1, 5, TimeUnit.SECONDS);
executeDelay(runnable12, scheduler1, 5, TimeUnit.SECONDS);
executeDelay(runnable13, scheduler1, 5, TimeUnit.SECONDS);

executeDelay(runnable21, scheduler2, 15, TimeUnit.SECONDS);
executeDelay(runnable22, scheduler2, 15, TimeUnit.SECONDS);
executeDelay(runnable23, scheduler2, 15, TimeUnit.SECONDS);

其實,咱們發現這裏是有規律可循的,好比,使用某個執行器在多久以後執行什麼,咱們能夠將executeDelay函數進行第一次柯里化

public Function3<ScheduledFuture, Runnable, Integer, TimeUnit> executeDelayBySomeScheduler(ScheduledExecutorService scheduler) {
    return (runnable, delay, timeunit) -> executeDelay(runable, scheduler, delay, timeunit);
}

Function3<ScheduledFuture, Runnable, Integer, TimeUnit> executeWithScheduler1 = executeDelayBySomeScheduler(scheduler1);

Function3<ScheduledFuture, Runnable, Integer, TimeUnit> executeWithScheduler2 = executeDelayBySomeScheduler(scheduler2);

executeWithScheduler1.apply(runnable11, 5, TimeUnit.SECONDS);
executeWithScheduler1.apply(runnable12, 5, TimeUnit.SECONDS);
executeWithScheduler1.apply(runnable13, 5, TimeUnit.SECONDS);

executeWithScheduler2.apply(runnable21, 15, TimeUnit.SECONDS);
executeWithScheduler2.apply(runnable22, 15, TimeUnit.SECONDS);
executeWithScheduler2.apply(runnable23, 15, TimeUnit.SECONDS);

函數executeDelay接收4個參數,函數executeWithScheduler1/executeWithScheduler2接收3個參數,咱們經過executeDelayBySomeScheduler將具體的執行器封裝在了executeWithScheduler1/executeWithScheduler2

進一步,咱們能夠作第二次柯里化,將延遲時間也封裝起來

public Function<ScheduledFuture, Runnable> executeDelayBySomeSchedulerOnDelay(ScheduledExecutorService scheduler, long delay, TimeUnit timeunit) {
    return (runnable) -> executeDelay(runable, scheduler, delay, timeunit);
}

Function<ScheduledFuture, Runnable> executeWithScheduler1After5M = executeDelayBySomeSchedulerOnDelay(scheduler1, 5, TimeUnit.SECONDS);

Function<ScheduledFuture, Runnable> executeWithScheduler2After15M = executeDelayBySomeSchedulerOnDelay(scheduler2, 15, TimeUnit.SECONDS);

Stream.of(runnable11, runnable12,runnable13).forEach(this::executeWithScheduler1After5M);
Stream.of(runnable21, runnable22,runnable23).forEach(this::executeWithScheduler2After15M);

將具體的執行器及延遲時間封裝在executeWithScheduler1After5M/executeWithScheduler2After15M中,調用的時候,只須要關心具體的執行邏輯便可

0x04 環繞執行(提取共性)

有時候咱們會發現,不少代碼塊十分類似,但又有些許不一樣

好比,目前有兩個接口能夠查詢匯率,queryExchangeRateAqueryExchangeRateB,咱們須要在開關exchangeRateSwitch打開的時候使用queryExchangeRateA查詢,不然使用queryExchangeRateB查詢,同時在一個接口異常失敗的時候,自動下降到另外一個接口進行查詢

一樣,目前有兩個接口能夠查詢關稅,queryTariffsAqueryTariffsB,一樣地,咱們須要在開關tariffsSwitch打開的時候使用queryTariffsA查詢,不然使用queryTariffsB查詢,同時在一個接口異常失敗的時候,自動下降到另外一個接口進行查詢

其實,以上兩種場景,除了開關及具體的接口邏輯外,總體流程是一致的

環繞模式

再分析,其實接口調用的降級邏輯也是同樣的

這裏再也不列舉如何使用抽象類的方法如解決該類問題,咱們直接使用Java8的Lambda表達式

首先,能夠將降級邏輯提取爲一個函數

@FunctionalInterface
interface ThrowingSupplier<T> {
    T get() throw Exception;
}

/**
 * 1. 執行A
 * 2. 若是A執行異常,則執行B
 */
public <T> ThrowingSupplier<T> executeIfThrowing(ThrowingSupplier<T> supplierA, ThrowingSupplier<T> supplierB) throws Exception {
    try {
        return supplierA.get();
    } catch(Exception e) {
        // dill with exception
        return supplierB.get();
    }
}

至此,咱們完成了降級的邏輯。接來下,將開關邏輯提取爲一個函數

/**
 * 1. switcher打開,執行A
 * 2. switcher關閉,執行B
 */
public <T> T invoke(Supplier<Boolean> switcher, ThrowingSupplier<T> executeA, ThrowingSupplier<T> executeB) throws Exception {
    return switcher.get() ? executeIfThrowing(this::queryExchangeRateA, this::queryExchangeRateB) : executeIfThrowing(this::queryExchangeRateB, this::queryExchangeRateA);
}

回到上邊的兩個需求,查詢匯率及關稅,咱們能夠

/**
 * 查詢匯率
 */
val exchangeRate = invoke(
    exchangeRateSwitch::isOpen, 
    this::queryExchangeRateA,
    this::queryExchangeRateB
)

/**
 * 查詢關稅
 */
val queryTariffs = invoke(
    tariffsSwitch::isOpen, 
    this::queryTariffsA,
    this::queryTariffsB
)
以上,用到了ThrowingSupplier,該點會在 《Lambda表達式裏的「陷阱」》一問中詳細介紹

0x05 設計模式

Lambda表達式,會給以往面向對象思想的設計模式帶來全新的設計思路,這部份內容但願在設計模式系列文章中詳細介紹。

關於Lambda表達式,還有很是多的內容及技巧,沒法使用有限的篇幅進行介紹,同時也但願與各位一同討論。

訂閱號

相關文章
相關標籤/搜索