Java8函數(shù)式編程真有這么神奇?- 案例展示lambda表達(dá)式對編碼效率提升到底有多大
這是一個(gè)新的系列,主要講Java8的lambda編程以及Stream流式編程相關(guān)的用法和案例。
這個(gè)系列脫胎于一個(gè)內(nèi)部的分享,由于篇幅較長,內(nèi)容較多,因此拆分成多篇文章進(jìn)行發(fā)布,方便自己后續(xù)參考,也希望能夠幫到讀者朋友。
本文是Java8函數(shù)式編程系列的第一篇,我們一起學(xué)習(xí)一下Java8函數(shù)式編程的基本概念及操作。
1.概述Lambda表達(dá)式
?Lambda 表達(dá)式,也可稱為閉包,它是推動(dòng) Java 8 發(fā)布的最重要新特性。
Lambda 允許把函數(shù)作為一個(gè)方法的參數(shù)(函數(shù)作為參數(shù)傳遞進(jìn)方法中)。
?
使用 Lambda 表達(dá)式可以使代碼變的更加簡潔緊湊。
1.1 lambda表達(dá)式語法
????(parameters)?->?expression
????或
????(parameters)?->?{statement};
我們可以將lambda表達(dá)式理解為一種代替原先匿名函數(shù)的新的編程方式
通過使用lambda表達(dá)式替換匿名函數(shù)的形式,將lambda表達(dá)式作為方法參數(shù),實(shí)現(xiàn)判斷邏輯參數(shù)化傳遞的目的。
1.2 lambda表達(dá)式形式
無參數(shù)
????()?->?System.out.println("code");
只有一個(gè)參數(shù)
????name?->?System.out.println("hello:"?+?name?+?"!");
沒有參數(shù),且邏輯復(fù)雜,需要通過大括號將多個(gè)語句括起來
????()?->?{????
????????System.out.println("hello");????
????????System.out.println("lambda");
????}
包含兩個(gè)參數(shù)的方法
????BinaryOperator?functionAdd?=?(x,?y)?->?x?+?y;
????Long?result?=?functionAdd.apply(1L,?2L);
包含兩個(gè)參數(shù)且對參數(shù)顯式聲明
????BinaryOperator?functionAdd?=?(Long?x,?Long?y)?->?x?+?y;
????Long?result?=?functionAdd.apply(1L,?2L);
1.3 函數(shù)式接口
定義:
?一個(gè)接口有且只有一個(gè)抽象方法;
?
函數(shù)式接口的實(shí)例可以通過 lambda 表達(dá)式、方法引用或者構(gòu)造方法引用來創(chuàng)建;
「注意」:
如果一個(gè)接口只有一個(gè)抽象方法,那么該接口就是一個(gè)函數(shù)式接口
如果我們在某個(gè)接口上聲明了 「@FunctionalInterface」 注解,那么編譯器就會(huì)按照函數(shù)式接口的定義來要求該接口
@FunctionInterface是Java8函數(shù)式接口注解,屬于聲明式注解,幫助編譯器校驗(yàn)被標(biāo)注的接口是否符合函數(shù)式接口定義
1.4 方法引用
我們通過Lambda表達(dá)式來實(shí)現(xiàn)匿名方法。
有些情況下,使用Lambda表達(dá)式僅僅是調(diào)用一些已經(jīng)存在的方法;除了調(diào)用動(dòng)作外,沒有其他任何多余的動(dòng)作,在這種情況下我們傾向于通過方法名來調(diào)用它,而Lambda表達(dá)式可以幫助我們實(shí)現(xiàn)這一要求。它使得Lambda在調(diào)用那些已經(jīng)擁有方法名的方法的代碼更簡潔、更容易理解。
方法引用可以理解為Lambda表達(dá)式的另外一種表現(xiàn)形式。
?方法引用是調(diào)用特定方法的lambda表達(dá)式的一種快捷寫法,可以讓你重復(fù)使用現(xiàn)有的方法定義,并像lambda表達(dá)式一樣傳遞他們。
?
注意:
使用方法引用時(shí),只寫方法名,不寫括號
1.4.1 方法引用格式:
????格式:?目標(biāo)引用????雙冒號分隔符???方法名
????eg:??String???????::????????valueOf
1.4.2 方法引用分類:
1.指向靜態(tài)方法的方法引用:當(dāng)Lambda表達(dá)式的結(jié)構(gòu)體是一個(gè)對象,并且調(diào)用其靜態(tài)方法時(shí),使用如下方式
????表達(dá)式:
????????(args)?->?ClassName::staticMethod(args);
????格式:????ClassName?::?staticMethodName
????eg:?????Integer???::?valueOf
2.指向任意類型實(shí)例方法的方法引用:當(dāng)直接調(diào)用對象的實(shí)例方法,則使用如下方式進(jìn)行調(diào)用
????表達(dá)式:
????????????(args)?->?args.instanceMethod();
????格式:??ClassName::instanceMethod;
????eg:???String::indexOf??
????????????????String::toString
3.指向現(xiàn)有對象的實(shí)例方法的方法引用:通過對象實(shí)例,方法引用實(shí)例方法
????表達(dá)式:
????????(args)?->?object.instanceMethod(args);
????????改寫為
????????(args)?->??object::instanceMethod;
????eg:
????????StringBuilder?sb?=?new?StringBuilder();
????????Consumer?consumer?=?(String?str)?->?stringBuilder.append(str);
就可以改寫為
????Consumer?consumer?=?(String?str)?->?stringBuilder::append;
2.從一個(gè)案例入手
我們先看一個(gè)例子,宏觀感受一下Java8 Lambda編程帶來的便利(后續(xù)講解Stream同樣使用該案例)
2.1 案例:直觀體驗(yàn)Java8Stream操作
Sku實(shí)體類: ?標(biāo)識一個(gè)電商下單商品信息對象
????public?class?Sku?{
????????//?商品編號
????????private?Integer?skuId;
????????//?商品名稱
????????private?String?skuName;
????????//?單價(jià)
????????private?Double?skuPrice;
????????//?購買個(gè)數(shù)
????????private?Integer?totalNum;
????????//?總價(jià)
????????private?Double?totalPrice;
????????//?商品類型
????????private?Enum?skuCategory;
????????public?Sku()?{
????????}
????????public?Sku(Integer?skuId,?String?skuName,?Double?skuPrice,?Integer?totalNum,?Double?totalPrice,?Enum?skuCategory)?{
????????????this.skuId?=?skuId;
????????????this.skuName?=?skuName;
????????????this.skuPrice?=?skuPrice;
????????????this.totalNum?=?totalNum;
????????????this.totalPrice?=?totalPrice;
????????????this.skuCategory?=?skuCategory;
????????}
????????????...省略getter??setter...
????}
SkuCategoryEnum枚舉類: 商品類型枚舉
????public?enum?SkuCategoryEnum?{
????????CLOTHING(10,?"服務(wù)類"),
????????ELECTRONICS(20,?"數(shù)碼產(chǎn)品類"),
????????SPORTS(30,?"運(yùn)動(dòng)類"),
????????BOOKS(40,?"圖書類")
????????;
????????//?商品類型編號
????????private?Integer?code;
????????//?商品名稱
????????private?String?name;
????????SkuCategoryEnum(Integer?code,?String?name)?{
????????????this.code?=?code;
????????????this.name?=?name;
????????}
????????...省略getter...
????}
CartService類: 初始化一批數(shù)據(jù),模擬購物車
????public?class?CartService?{
????????//?初始化購物車
????????private?static?List?cartSkuList?=?new?ArrayList<>();
????????static?{
????????????cartSkuList.add(new?Sku(2,?"無人機(jī)",?1000.00,?10,?1000.00,?SkuCategoryEnum.ELECTRONICS));
????????????cartSkuList.add(new?Sku(1,?"VR一體機(jī)",?2100.00,?10,?2100.00,?SkuCategoryEnum.ELECTRONICS));
????????????cartSkuList.add(new?Sku(4,?"牛仔褲",?60.00,?10,?60.00,?SkuCategoryEnum.CLOTHING));
????????????cartSkuList.add(new?Sku(13,?"襯衫",?120.00,?10,?120.00,?SkuCategoryEnum.CLOTHING));
????????????cartSkuList.add(new?Sku(121,?"Java編程思想",?100.00,?10,?100.00,?SkuCategoryEnum.BOOKS));
????????????cartSkuList.add(new?Sku(3,?"程序化廣告",?80.00,?10,?80.00,?SkuCategoryEnum.BOOKS));
????????}
????????public?static?List?getCartSkuList()?{
????????????return?cartSkuList;
????????}
????}
我們直接看這個(gè)案例
????private?static?List?cartSkuList?=?CartService.getCartSkuList();
????@Test
????public?void?show()?{
????????List?collect?=?cartSkuList.stream()
????????????????//?方法引用
????????????????.map(Sku::getSkuId)
????????????????.distinct()
????????????????.sorted()
????????????????.collect(Collectors.toList());
????????collect.stream().forEach(skuId?->?{
????????????System.out.println(skuId.toString());
????????});
????}
簡單解釋下這段代碼的意圖:
首先獲取購物車中商品列表,將該列表轉(zhuǎn)換為流;收集商品列表中的所有商品編號(skuId),對商品編號進(jìn)行去重,并進(jìn)行自然排序(升序排列),最后收集為一個(gè)商品編號集合,并對該集合進(jìn)行遍歷打印。
我并沒有加注釋,但是相信聰明的你也一定能讀懂上面這段代碼,這正是Stream編程的特點(diǎn):方法名見名知意,流式編程方式符合人類思考邏輯
運(yùn)行該用例,打印如下:
????1
????2
????3
????4
????13
????121
打印結(jié)果符合我們的預(yù)期意圖。
想象一下,如果不使用lambda+Stream方式,而是使用java7及之前的傳統(tǒng)集合操作,實(shí)現(xiàn)上述操作我們的代碼量有多少?保守估計(jì)至少是上述代碼段的1.5倍。
這個(gè)案例可能還不具備說服力,接下來的文章中,我將通過一個(gè)對比案例來比較lambda編程與傳統(tǒng)方式對集合操作的效率提升。
?那么,使用了lambda函數(shù)式編程之后,對我們的開發(fā)真有顯著的提升么?
?
在接下來的章節(jié)中,我們通過一個(gè)實(shí)戰(zhàn)案例對比原始集合操作與Stream集合操作具體有哪些不同,直觀地展示Stream集合操作對編程效率的提升。
案例:對比原始集合操作與Stream集合操作
需求場景:
針對上面的購物車,我們想要
全局查看購物車中都有哪些商品 將購物車中的圖書類商品進(jìn)行過濾(刪除圖書類商品) 在其余商品中挑選兩件最貴的 打印出上述兩件商品的名稱和總價(jià)
原始集合操作:
????@Test
????public?void?traditionalWay()?{
????????//?1.?打印所有商品
????????List?skus?=?CartService.getCartSkuList();
????????for?(Sku?sku?:?skus)?{
????????????System.out.println(JSON.toJSONString(sku,?true));
????????}
????????//?2.?過濾圖書類商品
????????List?notIncludeBooksList?=?new?ArrayList<>();
????????for?(Sku?sku?:?skus)?{
????????????if?(!sku.getSkuCategory().equals(SkuCategoryEnum.BOOKS))?{
????????????????notIncludeBooksList.add(sku);
????????????}
????????}
????????//?3.?其余商品中挑選兩件最貴的?價(jià)格倒排序,取top2
????????//?3.1?先排序
????????notIncludeBooksList.sort(new?Comparator()?{
????????????@Override
????????????public?int?compare(Sku?sku0,?Sku?sku1)?{
????????????????if?(sku0.getTotalPrice()?>?sku1.getTotalPrice())?{
????????????????????return?-1;
????????????????}
????????????????if?(sku0.getTotalPrice()?????????????????????return?1;
????????????????}
????????????????return?0;
????????????}
????????});
????????//?3.2?取top2
????????List?top2SkuList?=?new?ArrayList<>();
????????for?(int?i?=?0;?i?2;?i++)?{
????????????top2SkuList.add(notIncludeBooksList.get(i));
????????}
????????//?4.?打印出上述兩件商品的名稱和總價(jià)
????????//?4.1?求兩件商品總價(jià)
????????double?totalMoney?=?0.0;
????????for?(Sku?sku?:?top2SkuList)?{
????????????totalMoney?+=?sku.getTotalPrice();
????????}
????????//?4.2?獲取兩件商品名稱
????????List?resultSkuNameList?=?new?ArrayList<>();
????????for?(Sku?sku?:?top2SkuList)?{
????????????resultSkuNameList.add(sku.getSkuName());
????????}
????????//?打印輸出結(jié)果
????????System.out.println("結(jié)果商品名稱:?"?+?JSON.toJSONString(resultSkuNameList,?true));
????????System.out.println("商品總價(jià):"?+?totalMoney);
????}
運(yùn)行結(jié)果:
????{"skuCategory":"ELECTRONICS","skuId":2,"skuName":"無人機(jī)","skuPrice":1000.0,"totalNum":10,"totalPrice":1000.0}
????{"skuCategory":"ELECTRONICS","skuId":1,"skuName":"VR一體機(jī)","skuPrice":2100.0,"totalNum":10,"totalPrice":2100.0}
????{"skuCategory":"CLOTHING","skuId":4,"skuName":"牛仔褲","skuPrice":60.0,"totalNum":10,"totalPrice":60.0}
????{"skuCategory":"CLOTHING","skuId":13,"skuName":"襯衫","skuPrice":120.0,"totalNum":10,"totalPrice":120.0}
????{"skuCategory":"BOOKS","skuId":121,"skuName":"Java編程思想","skuPrice":100.0,"totalNum":10,"totalPrice":100.0}
????{"skuCategory":"BOOKS","skuId":3,"skuName":"程序化廣告","skuPrice":80.0,"totalNum":10,"totalPrice":80.0}
????結(jié)果商品名稱:?[
????????"VR一體機(jī)",
????????"無人機(jī)"
????]
????商品總價(jià):3100.0
我們可以看到傳統(tǒng)的集合操作還是寫了比較多的代碼,而且在編碼過程中為了滿足各種要求,我們通過聲明新的容器來接受過程中的操作結(jié)果,這帶來了內(nèi)存使用量的增加。
接下來看一下Stream方式下如何編碼實(shí)現(xiàn)我們的需求:
Stream集合操作:
????@Test
????public?void?streamWay()?{
????????AtomicReference?money?=?new?AtomicReference<>(Double.valueOf(0.0));
????????List?resultSkuNameList?=?CartService.getCartSkuList()
????????????//?獲取集合流
????????????????.stream()
????????????????/**1.?打印商品信息*/
????????????????.peek(sku?->?System.out.println(JSON.toJSONString(sku)))
????????????????/**2.?過濾掉所有的圖書類商品*/
????????????????.filter(sku?->?!SkuCategoryEnum.BOOKS.equals(sku.getSkuCategory()))
????????????????/**3.?價(jià)格進(jìn)行排序,默認(rèn)是從小到大,調(diào)用reversed進(jìn)行翻轉(zhuǎn)排序即從大到小*/
????????????????.sorted(Comparator.comparing(Sku::getTotalPrice).reversed())
????????????????/**4.?取top2*/
????????????????.limit(2)
????????????????/**累加金額*/
????????????????.peek(sku?->?money.set(money.get()?+?sku.getTotalPrice()))
????????????????/**獲取商品名稱*/
????????????????.map(sku?->?sku.getSkuName())
????????????????.collect(Collectors.toList());
????????System.out.println("商品總價(jià):"?+?money.get());
????????System.out.println("商品名列表:"?+?JSON.toJSONString(resultSkuNameList));
????}
運(yùn)行結(jié)果:
????{"skuCategory":"ELECTRONICS","skuId":2,"skuName":"無人機(jī)","skuPrice":1000.0,"totalNum":10,"totalPrice":1000.0}
????{"skuCategory":"ELECTRONICS","skuId":1,"skuName":"VR一體機(jī)","skuPrice":2100.0,"totalNum":10,"totalPrice":2100.0}
????{"skuCategory":"CLOTHING","skuId":4,"skuName":"牛仔褲","skuPrice":60.0,"totalNum":10,"totalPrice":60.0}
????{"skuCategory":"CLOTHING","skuId":13,"skuName":"襯衫","skuPrice":120.0,"totalNum":10,"totalPrice":120.0}
????{"skuCategory":"BOOKS","skuId":121,"skuName":"Java編程思想","skuPrice":100.0,"totalNum":10,"totalPrice":100.0}
????{"skuCategory":"BOOKS","skuId":3,"skuName":"程序化廣告","skuPrice":80.0,"totalNum":10,"totalPrice":80.0}
????商品總價(jià):3100.0
????商品名列表:["VR一體機(jī)","無人機(jī)"]
我們可以看到,通過Stream集合操作,運(yùn)行結(jié)果與傳統(tǒng)集合操作完全一致。但是編碼量卻能夠顯著減少。
「辯證的分析一下」,如果對Stream操作沒有一個(gè)較為明確的了解,閱讀這段代碼確實(shí)有些難度,但是只要有一點(diǎn)了解,Stream集合操作代碼帶來的無論是編碼量顯著降低還是可讀性提升,亦或是內(nèi)存空間的節(jié)約都是可觀的。
階段小結(jié)
可見,學(xué)習(xí)并運(yùn)用Lambda及Stream編程,對于提升我們的編碼效率以及提升代碼可讀性都有著明顯的收益。
參考資料
「《告別996,開啟Java高效編程之門》慕課網(wǎng)」
