Java 函数式编程和 lambda 表达式
為什么要使用函數式編程
函數式編程更多時候是一種編程的思維方式,是種方法論。函數式與命令式編程的區別主要在于:函數式編程是告訴代碼你要做什么,而命令式編程則是告訴代碼要怎么做。說白了,函數式編程是基于某種語法或調用API去進行編程。例如,我們現在需要從一組數字中,找出最小的那個數字,若使用用命令式編程實現這個需求的話,那么所編寫的代碼如下:
public?static?void?main(String[]?args)?{int[]?nums?=?new?int[]{1,?2,?3,?4,?5,?6,?7,?8};int?min?=?Integer.MAX_VALUE;for?(int?num?:?nums)?{if?(num?<?min)?{min?=?num;}}System.out.println(min); }而使用函數式編程進行實現的話,所編寫的代碼如下:
public?static?void?main(String[]?args)?{int[]?nums?=?new?int[]{1,?2,?3,?4,?5,?6,?7,?8};int?min?=?IntStream.of(nums).min().getAsInt();System.out.println(min); }從以上的兩個例子中,可以看出,命令式編程需要自己去實現具體的邏輯細節。而函數式編程則是調用API完成需求的實現,將原本命令式的代碼寫成一系列嵌套的函數調用,在函數式編程下顯得代碼更簡潔、易懂,這就是為什么要使用函數式編程的原因之一。所以才說函數式編程是告訴代碼你要做什么,而命令式編程則是告訴代碼要怎么做,是一種思維的轉變。
說到函數式編程就不得不提一下lambda表達式,它是函數式編程的基礎。在Java還不支持lambda表達式時,我們需要創建一個線程的話,需要編寫如下代碼:
public?static?void?main(String[]?args)?{new?Thread(new?Runnable()?{@Overridepublic?void?run()?{System.out.println("running");}}).start(); }而使用lambda表達式一句代碼就能完成線程的創建,lambda強調了函數的輸入輸出,隱藏了過程的細節,并且可以接受函數當作輸入(參數)和輸出(返回值):
public?static?void?main(String[]?args)?{new?Thread(()?->?System.out.println("running")).start(); }注:箭頭的左邊是輸入,右邊則是輸出
該lambda表達式的作用其實就是返回了Runnable接口的實現對象,這與我們調用某個方法獲取實例對象類似,只不過是將實現代碼直接寫在了lambda表達式里。我們可以做個簡單的對比:
public?static?void?main(String[]?args)?{Runnable?runnable1?=?()?->?System.out.println("running");Runnable?runnable2?=?RunnableFactory.getInstance(); }JDK8接口新特性
1.函數接口,接口只能有一個需要實現的方法,可以使用@FunctionalInterface?注解進行聲明。如下:
@FunctionalInterface interface?Interface1?{int?doubleNum(int?i); }使用lambda表達式獲取該接口的實現實例的幾種寫法:
public?static?void?main(String[]?args)?{//?最常見的寫法Interface1?i1?=?(i)?->?i?*?2;Interface1?i2?=?i?->?i?*?2;//?可以指定參數類型Interface1?i3?=?(int?i)?->?i?*?2;//?若有多行代碼可以這么寫Interface1?i4?=?(int?i)?->?{System.out.println(i);return?i?*?2;}; }2.比較重要的一個接口特性是接口的默認方法,用于提供默認實現。默認方法和普通實現類的方法一樣,可以使用this等關鍵字:
@FunctionalInterface interface?Interface1?{int?doubleNum(int?i);default?int?add(int?x,?int?y)?{return?x?+?y;} }之所以說默認方法這個特性比較重要,是因為我們借助這個特性可以在以前所編寫的一些接口上提供默認實現,并且不會影響任何的實現類以及既有的代碼。例如我們最熟悉的List接口,在JDK1.2以來List接口就沒有改動過任何代碼,到了1.8之后才使用這個新特性增加了一些默認實現。這是因為如果沒有默認方法的特性的話,修改接口代碼帶來的影響是巨大的,而有了默認方法后,增加默認實現可以不影響任何的代碼。
3.當接口多重繼承時,可能會發生默認方法覆蓋的問題,這時可以去指定使用哪一個接口的默認方法實現,如下示例:
@FunctionalInterface interface?Interface1?{int?doubleNum(int?i);default?int?add(int?x,?int?y)?{return?x?+?y;} }@FunctionalInterface interface?Interface2?{int?doubleNum(int?i);default?int?add(int?x,?int?y)?{return?x?+?y;} }@FunctionalInterface interface?Interface3?extends?Interface1,?Interface2?{@Overridedefault?int?add(int?x,?int?y)?{//?指定使用哪一個接口的默認方法實現return?Interface1.super.add(x,?y);} }函數接口
我們本小節來看看JDK8里自帶了哪些重要的函數接口:
Java函數式編程和lambda表達式
可以看到上表中有好幾個接口,而其中最常用的是Function接口,它能為我們省去定義一些不必要的函數接口,減少接口的數量。我們使用一個簡單的例子演示一下 Function 接口的使用:
import?java.text.DecimalFormat; import?java.util.function.Function;class?MyMoney?{private?final?int?money;public?MyMoney(int?money)?{this.money?=?money;}public?void?printMoney(Function<Integer,?String>?moneyFormat)?{System.out.println("我的存款:?"?+?moneyFormat.apply(this.money));} }public?class?MoneyDemo?{public?static?void?main(String[]?args)?{MyMoney?me?=?new?MyMoney(99999999);Function<Integer,?String>?moneyFormat?=?i?->?new?DecimalFormat("#,###").format(i);//?函數接口支持鏈式操作,例如增加一個字符串me.printMoney(moneyFormat.andThen(s?->?"人民幣?"?+?s));} }運行以上例子,控制臺輸出如下:
我的存款:?人民幣?99,999,999若在這個例子中不使用Function接口的話,則需要自行定義一個函數接口,并且不支持鏈式操作,如下示例:
import?java.text.DecimalFormat;//?自定義一個函數接口 @FunctionalInterface interface?IMoneyFormat?{String?format(int?i); }class?MyMoney?{private?final?int?money;public?MyMoney(int?money)?{this.money?=?money;}public?void?printMoney(IMoneyFormat?moneyFormat)?{System.out.println("我的存款:?"?+?moneyFormat.format(this.money));} }public?class?MoneyDemo?{public?static?void?main(String[]?args)?{MyMoney?me?=?new?MyMoney(99999999);IMoneyFormat?moneyFormat?=?i?->?new?DecimalFormat("#,###").format(i);me.printMoney(moneyFormat);} }然后我們再來看看Predicate接口和Consumer接口的使用,如下示例:
public?static?void?main(String[]?args)?{//?斷言函數接口Predicate<Integer>?predicate?=?i?->?i?>?0;System.out.println(predicate.test(-9));//?消費函數接口Consumer<String>?consumer?=?System.out::println;consumer.accept("這是輸入的數據"); }運行以上例子,控制臺輸出如下:
false 這是輸入的數據這些接口一般有對基本類型的封裝,使用特定類型的接口就不需要去指定泛型了,如下示例:
public?static?void?main(String[]?args)?{//?斷言函數接口IntPredicate?intPredicate?=?i?->?i?>?0;System.out.println(intPredicate.test(-9));//?消費函數接口IntConsumer?intConsumer?=?(value)?->?System.out.println("輸入的數據是:"?+?value);intConsumer.accept(123); }運行以上代碼,控制臺輸出如下:
false 輸入的數據是:123有了以上接口示例的鋪墊,我們應該對函數接口的使用有了一個初步的了解,接下來我們演示剩下的函數接口使用方式:
public?static?void?main(String[]?args)?{//?提供數據接口Supplier<Integer>?supplier?=?()?->?10?+?1;System.out.println("提供的數據是:"?+?supplier.get());//?一元函數接口UnaryOperator<Integer>?unaryOperator?=?i?->?i?*?2;System.out.println("計算結果為:"?+?unaryOperator.apply(10));//?二元函數接口BinaryOperator<Integer>?binaryOperator?=?(a,?b)?->?a?*?b;System.out.println("計算結果為:"?+?binaryOperator.apply(10,?10)); }運行以上代碼,控制臺輸出如下:
提供的數據是:11 計算結果為:20 計算結果為:100而BiFunction接口就是比Function接口多了一個輸入而已,如下示例:
class?MyMoney?{private?final?int?money;private?final?String?name;public?MyMoney(int?money,?String?name)?{this.money?=?money;this.name?=?name;}public?void?printMoney(BiFunction<Integer,?String,?String>?moneyFormat)?{System.out.println(moneyFormat.apply(this.money,?this.name));} }public?class?MoneyDemo?{public?static?void?main(String[]?args)?{MyMoney?me?=?new?MyMoney(99999999,?"小明");BiFunction<Integer,?String,?String>?moneyFormat?=?(i,?name)?->?name?+?"的存款:?"?+?new?DecimalFormat("#,###").format(i);me.printMoney(moneyFormat);} }運行以上代碼,控制臺輸出如下:
小明的存款:?99,999,999方法引用
在學習了lambda表達式之后,我們通常會使用lambda表達式來創建匿名方法。但有的時候我們僅僅是需要調用一個已存在的方法。如下示例:
Arrays.sort(stringsArray,?(s1,?s2)?->?s1.compareToIgnoreCase(s2));在jdk8中,我們可以通過一個新特性來簡寫這段lambda表達式。如下示例:
Arrays.sort(stringsArray,?String::compareToIgnoreCase);這種特性就叫做方法引用(Method Reference)。方法引用的標準形式是:類名::方法名。(注意:只需要寫方法名,不需要寫括號)。
目前方法引用共有以下四種形式:
| 引用靜態方法 | ContainingClass::staticMethodName | String::valueOf | (s) -> String.valueOf(s) |
| 引用某個對象的實例方法 | containingObject::instanceMethodName | x::toString() | () -> this.toString() |
| 引用某個類型的任意對象的實例方法 | ContainingType::methodName | String::toString | (s) -> s.toString |
| 引用構造方法 | ClassName::new | String::new | () -> new String() |
下面我們用一個簡單的例子來演示一下方法引用的幾種寫法。首先定義一個實體類:
public?class?Dog?{private?String?name?=?"二哈";private?int?food?=?10;public?Dog()?{}public?Dog(String?name)?{this.name?=?name;}public?static?void?bark(Dog?dog)?{System.out.println(dog?+?"叫了");}public?int?eat(int?num)?{System.out.println("吃了"?+?num?+?"斤");this.food?-=?num;return?this.food;}@Overridepublic?String?toString()?{return?this.name;} }通過方法引用來調用該實體類中的方法,代碼如下:
package?org.zero01.example.demo;import?java.util.function.*;/***?@ProjectName?demo*?@Author:?zeroJun*?@Date:?2018/9/21?13:09*?@Description:?方法引用demo*/ public?class?MethodRefrenceDemo?{public?static?void?main(String[]?args)?{//?方法引用,調用打印方法Consumer<String>?consumer?=?System.out::println;consumer.accept("接收的數據");//?靜態方法引用,通過類名即可調用Consumer<Dog>?consumer2?=?Dog::bark;consumer2.accept(new?Dog());//?實例方法引用,通過對象實例進行引用Dog?dog?=?new?Dog();IntUnaryOperator?function?=?dog::eat;System.out.println("還剩下"?+?function.applyAsInt(2)?+?"斤");//?另一種通過實例方法引用的方式,之所以可以這么干是因為JDK默認會把當前實例傳入到非靜態方法,參數名為this,參數位置為第一個,所以我們在非靜態方法中才能訪問this,那么就可以通過BiFunction傳入實例對象進行實例方法的引用Dog?dog2?=?new?Dog();BiFunction<Dog,?Integer,?Integer>?biFunction?=?Dog::eat;System.out.println("還剩下"?+?biFunction.apply(dog2,?2)?+?"斤");//?無參構造函數的方法引用,類似于靜態方法引用,只需要分析好輸入輸出即可Supplier<Dog>?supplier?=?Dog::new;System.out.println("創建了新對象:"?+?supplier.get());//?有參構造函數的方法引用Function<String,?Dog>?function2?=?Dog::new;System.out.println("創建了新對象:"?+?function2.apply("旺財"));} }類型推斷
通過以上的例子,我們知道之所以能夠使用Lambda表達式的依據是必須有相應的函數接口。這一點跟Java是強類型語言吻合,也就是說你并不能在代碼的任何地方任性的寫Lambda表達式。實際上Lambda的類型就是對應函數接口的類型。Lambda表達式另一個依據是類型推斷機制,在上下文信息足夠的情況下,編譯器可以推斷出參數表的類型,而不需要顯式指名。
如果大家想學習以上路線內容,在此我向大家推薦一個架構學習交流群。交流學習群號874811168 里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化、分布式架構等這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多
所以說 Lambda 表達式的類型是從 Lambda 的上下文推斷出來的,上下文中 Lambda 表達式需要的類型稱為目標類型,如下圖所示:
Java函數式編程和lambda表達式
接下來我們使用一個簡單的例子,演示一下 Lambda 表達式的幾種類型推斷,首先定義一個簡單的函數接口:
@FunctionalInterface interface?IMath?{int?add(int?x,?int?y); }示例代碼如下:
public?class?TypeDemo?{public?static?void?main(String[]?args)?{//?1.通過變量類型定義IMath?iMath?=?(x,?y)?->?x?+?y;//?2.數組構建的方式IMath[]?iMaths?=?{(x,?y)?->?x?+?y};//?3.強轉類型的方式Object?object?=?(IMath)?(x,?y)?->?x?+?y;//?4.通過方法返回值確定類型IMath?result?=?createIMathObj();//?5.通過方法參數確定類型test((x,?y)?->?x?+?y);}public?static?IMath?createIMathObj()?{return?(x,?y)?->?x?+?y;}public?static?void?test(IMath?iMath){return;} }變量引用
Lambda表達式類似于實現了指定接口的內部類或者說匿名類,所以在Lambda表達式中引用變量和我們在匿名類中引用變量的規則是一樣的。如下示例:
public?static?void?main(String[]?args)?{String?str?=?"當前的系統時間戳是:?";Consumer<Long>?consumer?=?s?->?System.out.println(str?+?s);consumer.accept(System.currentTimeMillis()); }值得一提的是,在JDK1.8之前我們一般會將匿名類里訪問的外部變量設置為final,而在JDK1.8里默認會將這個匿名類里訪問的外部變量給設置為final。例如我現在改變str變量的值,ide就會提示錯誤:
Java函數式編程和lambda表達式
至于為什么要將變量設置final,這是因為在Java里沒有引用傳遞,變量都是值傳遞的。不將變量設置為final的話,如果外部變量的引用被改變了,那么最終得出來的結果就會是錯誤的。
下面用一組圖片簡單演示一下值傳遞與引用傳遞的區別。以列表為例,當只是值傳遞時,匿名類里對外部變量的引用是一個值對象:
Java函數式編程和lambda表達式
若此時list變量指向了另一個對象,那么匿名類里引用的還是之前那個值對象,所以我們才需要將其設置為final防止外部變量引用改變:
Java函數式編程和lambda表達式
而如果是引用傳遞的話,匿名類里對外部變量的引用就不是值對象了,而是指針指向這個外部變量:
Java函數式編程和lambda表達式
所以就算list變量指向了另一個對象,匿名類里的引用也會隨著外部變量的引用改變而改變:
Java函數式編程和lambda表達式
級聯表達式和柯里化
在函數式編程中,函數既可以接收也可以返回其他函數。函數不再像傳統的面向對象編程中一樣,只是一個對象的工廠或生成器,它也能夠創建和返回另一個函數。返回函數的函數可以變成級聯 lambda 表達式,特別值得注意的是代碼非常簡短。盡管此語法初看起來可能非常陌生,但它有自己的用途。
級聯表達式就是多個lambda表達式的組合,這里涉及到一個高階函數的概念,所謂高階函數就是一個可以返回函數的函數,如下示例:
//?實現了?x?+?y?的級聯表達式 Function<Integer,?Function<Integer,?Integer>>?function1?=?x?->?y?->?x?+?y; System.out.println("計算結果為:?"?+?function1.apply(2).apply(3));??//?計算結果為:?5這里的?y -> x + y?是作為一個函數返回給上一級表達式,所以第一級表達式的輸出是?y -> x + y這個函數,如果使用括號括起來可能會好理解一些:
x?->?(y?->?x?+?y)級聯表達式可以實現函數柯里化,簡單來說柯里化就是把本來多個參數的函數轉換為只有一個參數的函數,如下示例:
Function<Integer,?Function<Integer,?Function<Integer,?Integer>>>?function2?=?x?->?y?->?z?->?x?+?y?+?z; System.out.println("計算結果為:?"?+?function2.apply(1).apply(2).apply(3));??//?計算結果為:?6如果大家想學習以上路線內容,在此我向大家推薦一個架構學習交流群。交流學習群號874811168 里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化、分布式架構等這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多
函數柯里化的目的是將函數標準化,函數可靈活組合,方便統一處理等,例如我可以在循環里只需要調用同一個方法,而不需要調用另外的方法就能實現一個數組內元素的求和計算,代碼如下:
public?static?void?main(String[]?args)?{Function<Integer,?Function<Integer,?Function<Integer,?Integer>>>?f3?=?x?->?y?->?z?->?x?+?y?+?z;int[]?nums?=?{1,?2,?3};for?(int?num?:?nums)?{if?(f3?instanceof?Function)?{Object?obj?=?f3.apply(num);if?(obj?instanceof?Function)?{f3?=?(Function)?obj;}?else?{System.out.println("調用結束,?結果為:?"?+?obj);??//?調用結束,?結果為:?6}}} }級聯表達式和柯里化一般在實際開發中并不是很常見,所以對其概念稍有理解即可,這里只是簡單帶過,若對其感興趣的可以查閱相關資料。
總結
以上是生活随笔為你收集整理的Java 函数式编程和 lambda 表达式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一个简单案例,5 分钟看懂 Java L
- 下一篇: Nginx 学习 —— 负载均衡