該文章已收錄 【修煉內功】躍遷之路java
Lambda表達式,能夠理解爲簡潔地表示可傳遞的匿名函數的一種方式:它沒有名稱,但它有參數列表、函數主體、返回類型。
這裏,默認您已對Java8的Lambda表達式有必定了解,而且知道如何使用。程序員
Java8中引入的Lambda表達式,爲編程體驗及效率帶來了極大的提高。express
行爲參數化,是理解函數式編程的一個重要概念。簡單來講即是,一個方法接受多個不一樣的行爲做爲參數,並在內部使用它們,完成不一樣行爲的能力。更爲通俗的講,行爲參數化是指,定義一段代碼,這段代碼並不會當即執行,而是能夠像普通變量/參數同樣進行傳遞,被程序的其餘部分調用。編程
咱們經過一個特別通用的篩選蘋果的例子,來逐步瞭解如何使用Lambda表達式實現行爲參數化。(若是對行爲參數化已十分了解,可直接跳過本節)segmentfault
咱們須要將倉庫中綠色的蘋果過濾出來,對於這樣的問題,大多數人來講都是手到擒來 (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);
對於這樣的需求變動,可能也不是很難閉包
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");
有了先前的教訓,可能會學聰明一些,不會把重量直接寫死到程序裏,而是提供一個入參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);
若是照此下去,程序將變得異常難於維護,每一次小的需求變動,都須要進行大範圍的改動。爲了不永無休止的加班,對於瞭解設計模式的同窗,可能會將篩選邏輯抽象出來 (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
的函數出來,貌似又陷入了無窮加班的怪圈~
還有沒有優化空間?可否將篩選條件抽離到單一屬性,如byColor
、byMinWeight
等,以後再作與或計算傳遞給篩選函數?
接下來就是咱們要介紹的高階函數
高階函數是一個函數,它接收函數做爲參數或將函數做爲輸出返回
- 接受至少一個函數做爲參數
- 返回的結果是一個函數
以上的定義聽起來可能有些繞口。結合上節示例,咱們的訴求是將蘋果的顏色、重量或者其餘篩選條件也抽離出來,而不是硬編碼到代碼中
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)));
至此,還能夠抽象出anyMatches
、nonMatches
等高階函數,組合使用
// 篩選出 顏色爲紅色 而且 重量在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表達式),一樣爲高階函數
關於默認函數的使用,會在以後的文章詳細介紹
閉包(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
時使用(更改)
柯里化(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
中,調用的時候,只須要關心具體的執行邏輯便可
有時候咱們會發現,不少代碼塊十分類似,但又有些許不一樣
好比,目前有兩個接口能夠查詢匯率,queryExchangeRateA
及queryExchangeRateB
,咱們須要在開關exchangeRateSwitch
打開的時候使用queryExchangeRateA
查詢,不然使用queryExchangeRateB
查詢,同時在一個接口異常失敗的時候,自動下降到另外一個接口進行查詢
一樣,目前有兩個接口能夠查詢關稅,queryTariffsA
及queryTariffsB
,一樣地,咱們須要在開關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表達式裏的「陷阱」》一問中詳細介紹
Lambda表達式,會給以往面向對象思想的設計模式帶來全新的設計思路,這部份內容但願在設計模式系列文章中詳細介紹。
關於Lambda表達式,還有很是多的內容及技巧,沒法使用有限的篇幅進行介紹,同時也但願與各位一同討論。