關于 Lambda 表達式的一些事
從1998年 Java 發布以來,Java 版本從1.1到目前的 Java8,Java一直在升級,在增加新功能。這也就很好的解釋了為什么 Java 在編程語言排行榜上一直占據前排位置。不斷的創新才是保持優勢的正確方式。Java8 在2014年被發布,提供了更多的編程工具和概念,讓開發人員以更加簡潔、易維護的方式去解決問題。Lambda 表達式作為 Java8 中一個重要的特性,是不可不學的一部分。
為什么需要 Lambda 表達式
怎么解決點外賣的問題
都說人生有三大難題:早飯吃什么?午飯吃什么?晚飯吃什么?每當到了
飯點,在決定點哪家外賣的時候,都像是在作出人生抉擇。我們就從點外賣這件事上來講一講為什么需要引入 Lambda 表達式。
首先,我們定義一下我們供我們點外賣的店鋪:
publicclassStore{
privateString name;//商店名稱
privatedoubleavePrice;//平均價格
privateintaveDeliveryTime;//平均送餐時間
privateString type;//商店類型
publicStore(){
}
publicStore(String name,doubleavePrice,intaveDeliveryTime,String type){
this.name = name;
this.avePrice = avePrice;
this.aveDeliveryTime = aveDeliveryTime;
this.type = type;
}
@Override
publicStringtoString(){
returnString.format("店鋪名稱:%s 平均價格:%d 平均送餐時間:%d 店鋪類型:%s",
this.name,this.avePrice,this.aveDeliveryTime,this.type);
}
/*省略get和set方法*/
}
接著,我們開始定外賣,現在到月底了,又沒錢了,點外賣也不挑什么了,就找最便宜的,省錢是硬道理。找最便宜的外賣店應該怎么找呢,很自然的就是對店鋪的價格進行排序。
publicstaticvoidmain(String[] args){
List<Store> stores = newArrayList<Store>();
stores.add(newStore("店鋪一",20,10,"快餐"));
stores.add(newStore("店鋪二",30,20,"燒烤"));
stores.add(newStore("店鋪三",10,20,"披薩"));
stores.add(newStore("店鋪四",15,30,"炸雞"));
stores.add(newStore("店鋪五",45,20,"甜品"));
Collections.sort(stores, newComparator<Store>() {
publicintcompare(Store o1, Store o2){
returnDouble.compare(o1.getAvePrice(),o2.getAvePrice());
}
});
}
可以看到這樣的確解決了問題,但是當我們有新的需求的時候,比如說我想找出送餐時間最短的店鋪,或者是我只想找出賣快餐的店鋪時,這個代碼就變得不適用了,我們需要更改代碼來實現新的功能。這個還是簡單的情況,真實開發中情況更加復雜,顯然每次遇到新的需要就大改代碼這樣很不優雅,該怎么解決這個問題。熟悉設計模式的朋友很快就會想到,這很符合策略模式的情況,我們每次只是更改不同的選擇店鋪策略去選擇店鋪。
用策略模式來解決點外賣的問題
下面我們重新用策略模式來完成我們挑選外賣店鋪的任務。我們將挑選店鋪的策略抽象出來:
publicinterfaceSelector{
publicList<Store>select(List<Store> stroes);
}
然后實現這個選擇:
//將店鋪按照價格排序
classPriceSelectorimplementsSelector{
publicList<Store>select(List<Store> stroes){
Collections.sort(stroes, newComparator<Store>() {
publicintcompare(Store o1, Store o2){
returnDouble.compare(o1.getAvePrice(),o2.getAvePrice());
}
});
returnstroes;
}
}
//將店鋪按照送餐時間排序
classTimeSelectorimplementsSelector{
publicList<Store>select(List<Store> stroes){
Collections.sort(stroes, newComparator<Store>() {
publicintcompare(Store o1, Store o2){
returno1.getAveDeliveryTime() - o2.getAveDeliveryTime();
}
});
returnstroes;
}
}
//選擇所有的快餐店
classTypeSelectorimplementsSelector{
publicList<Store>select(List<Store> stroes){
List<Store> result = newArrayList<Store>();
for(Store store:stroes){
if("快餐".equals(store.getType())){
result.add(store);
}
}
returnresult;
}
}
之后我們就可以同傳遞不同的選擇方式來選擇外賣店:
publicstaticList<Store>selectStore(List<Store> stores,Selector s){
returns.select(stores);
}
publicstaticvoidmain(String[] args){
List<Store> stores = newArrayList<Store>();
stores.add(newStore("店鋪一",20,10,"快餐"));
stores.add(newStore("店鋪二",30,20,"燒烤"));
stores.add(newStore("店鋪三",10,20,"披薩"));
stores.add(newStore("店鋪四",15,30,"炸雞"));
stores.add(newStore("店鋪五",45,20,"甜品"));
Selector s = newTypeSelector();
stores = selectStore(stores,s);
for(Store store : stores){
System.out.println(store.toString());
}
}
這樣一來看上去貌似是解決了我們的問題,當有新的選擇方式的時候,我們只需要實現一個新的選擇類,用戶端基本上不用改變任何代碼。但是仔細一考慮,當選擇方式越來越多的時候,每次都需要重新實現新的類,而且仔細觀察發現每個選擇類中真正不同的只有 select() 方法中的代碼而已。
Lambda 在點外賣為題中的應用
有什么辦法可以讓我們不用每次都寫那么繁瑣的代碼呢,這個時候就需要 Lambda 表達式登場了。等了這么久,終于寫到文章的主題了(我的文章就是這么又臭又長)。為什么 Lambda 表達式可以簡化我們的代碼呢,因為 Lambda 提出的是一個 行為參數化 的概念,首先 行為 就是我們上面講到的 select() 方法中的代碼,這是選擇的具體行為, 參數化 就是向方法中傳遞參數一樣。 行為參數化 就是定義了一種新的編程方式,讓我們可以將代碼像參數一樣傳遞到方法中使用。
看一下我們使用 Lambda 表達式完成店鋪選擇的功能是怎么樣的:
publicstaticvoidmain(String[] args){
List<Store> stores = newArrayList<Store>();
stores.add(newStore("店鋪一",20,10,"快餐"));
stores.add(newStore("店鋪二",30,20,"燒烤"));
stores.add(newStore("店鋪三",10,20,"披薩"));
stores.add(newStore("店鋪四",15,30,"炸雞"));
stores.add(newStore("店鋪五",45,20,"甜品"));
stores.sort((Store o1,Store o2) -> Double.compare(o1.getAvePrice(),o2.getAvePrice()));
for(Store store : stores){
System.out.println(store.toString());
}
}
stores.sort((Store o1,Store o2) -> Double.compare(o1.getAvePrice(),o2.getAvePrice())); 就這么一行代碼代替了我們之前那么多的代碼,可以看到 Lambda 表達式可以使得我們的程序變得多么簡潔吧。 (Store o1,Store o2) -> Double.compare(o1.getAvePrice(),o2.getAvePrice()) 這行跟表達式一樣的東西就這么傳遞給了 sort() 方法。這就是所謂的行為參數化。
什么是 Lambda 表達式
Lambda 表達式可以理解為 簡潔地表示可傳遞的匿名函數的一種方式 :他沒有名稱,但他有參數列表、函數主體、返回類型,還有一個可以拋出的異常列表。
Lambda 表達式由三個部分組成:參數列表、箭頭、Lambda 主體。具體的形式表現為 (parameters) -> expression 或 (parameters) -> {statements;} 。
可以看到我們之前的 (Store o1,Store o2) -> Double.compare(o1.getAvePrice(),o2.getAvePrice()) 是 (parameters) -> expression 的形式, (Store o1,Store o2) 是參數列表 , Double.compare(o1.getAvePrice(),o2.getAvePrice()) 是表達式。當 Lambda 主體中有多條語句的時候,就需要符合 (parameters) -> {statements;} 的形式了( 注意{} )。
怎么使用 Lambda 表達式
在介紹 Lambda 表達式的使用之前,先提出一個概念:函數式接口。函數式接口定義:只定義一個抽象方法的接口。為什么需要這個概念呢,在 Java 中,”123” 是 String 類型; 12.3 是 Double 類型;true 是 boolean 類型…那 Lambda 這個實現行為參數化的新規范是什么類型呢,為了更符合我們的思維方式,Java8 給 Lambda 歸為函數式接口類型。這樣,我們能以我們更加理解的方式去學習 Lambda。
明確行為參數化
這其實就是一個去同存異的步驟,從上面的代碼中我們可以發現,真正不同的地方就是選擇店鋪的策略上,所以我們需要做的就是將 select() 的行為參數化。
定義函數式接口來傳遞行為
知道我們需要參數化的行為之后,我們需要定義一個函數式接口,以便于我們之后傳遞 Lambda 表達式。
@FunctionalInterface
publicinterfaceStoreSelect{
booleanselect(Store store);
}
我們將對店鋪的判斷邏輯抽取出來,將其進行行為參數化。 @FunctionalInterface 這個注解并不是必須的,但是為了將函數式接口與其他普通的接口區分開來,最好加上這個注解。
執行行為
定義完函數式接口之后,我們需要定義一個方法來接受 Lambda 表達式的參數,不然我們應該從哪邊傳入 Lambda 參數呢。
publicstaticList<Store>selectStore(List<Store> stores,StoreSelect s){
List<Store> result = newArrayList<>();
for(Store store:stores){
if(s.select(store)){
result.add(store);
}
}
returnresult;
}
可以看到我們調用了 StoreSelect 接口中的 select() 方法,但是我們并沒有哪邊實現過這個接口,這就需要我們在下一步中傳入 Lambda 表達式來實現這個接口。
使用 Lambda
傳入 Lambda 表達式,有種類似于用匿名方式實現接口:
publicstaticvoidmain(String[] args){
List<Store> stores = newArrayList<Store>();
stores.add(newStore("店鋪一",20,10,"快餐"));
stores.add(newStore("店鋪二",30,20,"燒烤"));
stores.add(newStore("店鋪三",10,20,"披薩"));
stores.add(newStore("店鋪四",15,30,"炸雞"));
stores.add(newStore("店鋪五",45,20,"甜品"));
stores = selectStore(stores,(Store store) -> store.getAvePrice() <= 20.0);
for(Store store : stores){
System.out.println(store.toString());
}
}
使用了一個 (Store store) -> store.getAvePrice() <= 20.0 Lambda 表達式,可以看到在傳入 Lambda表達式的時候,并不能亂寫,我們 Lambda 表達式中的參數列表必須與函數式接口的方法的參數列表一致,Lambda 主體中的返回結果必須與函數式接口中的方法的返回類型一致。Java 程序在編譯的過程中會檢查我們的代碼是否滿足這些要求,不然編譯不會通過。
Java8 提出了一個新的概念: 方法引用 ,這是一種 Lambda 表達式的簡略寫法。具體可以參照這篇文章,我在這就不啰嗦了。
使用 Java8 為我們準備的函數式接口
如果每次都使用 Lambda 表達式,都需要完成上面的這些步驟,會覺得很麻煩,人都是有惰性的。所以 Java 已經為我們準備好了許多內置的函數式接口,當我們需要函數式接口的時候,只需要尋找一下有沒有我們需要的,如果找不到再去自己定義。
使用復合 Lambda 表達式
上面的過程中,我們每次使用 Lambda 表達式的時候傳遞進的只是一個邏輯,當需要的邏輯更加復雜,簡單的一句話已經不能描述清楚的時候,應該怎么辦,多定義幾個函數式接口?并不需要,我們可以對 Lambda 表達式進行復合使用,就像 if 語句中出傳入的條件一樣,可以傳入多個。
在介紹如何復合使用 Lambda 表達式之前,我們先來升華一下 函數式接口 這個定義,之前的定義是函數式接口中只有一個抽象方法。Java 中引入一個叫 默認方法 的機制,讓我們可以在接口中實現方法。只需要在方法前加上 default 關鍵字就能為方法提供實現了。為什么要講這個呢,因為默認方法為我們復合使用 Lambda 表達式提供了可能。
Java 中內置的函數式接口中的 Comparator 、 Function 、 Predicate 都提供了允許進行表達式復合的方法,這些方法都是默認方法,已經為我們提供了實現。
Function
T -> R
Predicate T -> boolean
比較器復合
我們可以通過使用靜態方法 Comparator.comparing ,根據提取用于比較的 Function 來返回一個 Comparator。 Comparator.comparing((Store store) -> store.getAvePrice()) 這樣就可以獲得一個根據店鋪平均價格進行排序的 Comparator。如果我們想逆序排序,Compator 函數式接口中定義了一個 reversed() 函數,這可以返回一個逆序排序的 Comparator,類似于:
Comparator.comparing((Store store) -> store.getAvePrice()).reversed();
當兩家店鋪的價格一樣的時候,我們希望根據送餐時間進行排序又該怎么辦呢,Comparator 很貼心的又準備了一個 thenComparing() 方法,用法如下:
Comparator.comparing((Store store) -> store.getAvePrice())
.reversed()
.thenComparing((Store store) -> store.getAveDeliveryTime());
謂詞復合
謂詞接口 Predicate 提供了三個方法: and 、 or 、 negate ,分別代表與、或、非。
首先,定義一個 Predicate 接口,選擇出所有的快餐店:
Predicate<Store> typeStores = (Store store) -> "快餐".equals(store.getType());
那要找出所有的非快餐店呢:
Predicate<Store> notTypeStores = typeStores.negate();
找出非快餐店中,價格低于20的呢:
Predicate<Store> priceAndTypeStores = typeStores.negate()
.and((Store store) -> store.getAvePrice() < 20);
找出非快餐店中,價格低于20或者送餐時間低于30的呢:
Predicate<Store> priceAndTypeStores1 = typeStores.negate()
.and((Store store) -> store.getAvePrice() < 20)
.or((Store store) -> store.getAveDeliveryTime() < 30);
函數復合
函數復合和數學中的函數復合基本上概念是一致的,提供了 andThen 和 compose 兩個方法:
Function<String,String> f1 = (String s) -> s.trim();
Function<String,String> f2 = (String s) -> s.substring(4);
f1.andThen(f2) 表示 f2(f1(s)) , f1.compose(f2) 表示 f1(f2(s)) 。
小結
啰啰嗦嗦的講 Lambda 的基本概念講完了,最后只想表明一個觀點:實踐出真知。光看懂 Lambda 表達式的概念,不在實際中使用,這一切都是空談,Lambda 的出現就是為了簡化我們的代碼,將它用到平時的代碼中去才能說是真正掌握了知識。
來自:https://win-man.github.io/2016/09/24/關于 Lambda 表達式的一些事/