前言
Hi,大家好,我是麥洛。今天我們以主人公阿呆的視角。來看看他如何將一個業務代碼一步步重構,最後使用函數式接口達到靈活實現。希望對大家理解lambda
表達式和函數式接口有所幫助.
很久很久以前,大約是21世紀時候。有一個天才程序員,名字叫阿呆。大學畢業以後,順利被一家知名電商網站錄取,開始了自己的偉大之路。
時間過得很快,不知不覺,他入職已經兩週了。這天,老闆讓他對接一個客戶。在交談中,阿呆得知這位客戶是做水果生意,主要經營各種瓜。想要開發一款電商小程序來做線上業務。經過簡單溝通之後,客戶起身離開。回到工作崗位的阿呆很快設計下面的類來定義瓜 Melon
類:
/**
* 瓜
* @author milogenius
* @date 2020-05-29 13:21
*/
public class Melon {
/**品種*/
private final String type;
/**重量*/
private final int weight;
/**產地*/
private final String origin;
public Melon(String type, int weight, String origin) {
this.type = type;
this.weight = weight;
this.origin = origin;
}
// getters, toString(), and so on omitted for brevity
}
經過一個月奮戰,阿呆成功上線了這個項目。
第一次 按類型篩選瓜類
有一天,客戶向阿呆提了一個需求,能夠按瓜類型對瓜進行過濾。阿呆腦袋一想,這不很簡單嗎?於是,阿呆創建了一個 Filters
類, 實現了一個filterByType
方法
/**
* @author milogenius
* @date 2020-05-29 13:25
*/
public class Filters {
/**
* 根據類型篩選瓜類
* @param melons 瓜類
* @param type 類型
* @return
*/
public static List<Melon> filterByType(List<Melon> melons, String type) {
List<Melon> result = new ArrayList<>();
for (Melon melon: melons) {
if (melon != null && type.equalsIgnoreCase(melon.getType())) {
result.add(melon);
}
}
return result;
}
}
搞定,我們來測試一下
public static void main(String[] args) {
ArrayList<Melon> melons = new ArrayList<>();
melons.add(new Melon("羊角蜜", 1, "泰國"));
melons.add(new Melon("西瓜", 2, "三亞"));
melons.add(new Melon("黃河蜜", 3, "蘭州"));
List<Melon> melonType = Filters.filterByType(melons, "黃河蜜");
melonType.forEach(melon->{
System.out.println("瓜類型:"+melon.getType());
});
}
沒毛病,下班了,溜了溜了
第二次 按重量篩選瓜類
過了幾天,客戶又提了一個需求,要求按重量篩選瓜類(例如:所有1200克的瓜)。阿呆心想:天天提需求,天天改,就不能一次提完啊?上次我已經實現了按類型篩選瓜類,那我給他copy
一份改改吧!如下所示:
/**
* 按照重量過濾瓜類
* @param melons
* @param weight
* @return
*/
public static List<Melon> filterByWeight(List<Melon> melons, int weight) {
List<Melon> result = new ArrayList<>();
for (Melon melon: melons) {
if (melon != null && melon.getWeight() == weight) {
result.add(melon);
}
}
return result;
}
public static void main(String[] args) {
ArrayList<Melon> melons = new ArrayList<>();
melons.add(new Melon("羊角蜜", 1, "泰國"));
melons.add(new Melon("西瓜", 2, "三亞"));
melons.add(new Melon("黃河蜜", 3, "蘭州"));
List<Melon> melonType = Filters.filterByType(melons, "黃河蜜");
melonType.forEach(melon->{
System.out.println("瓜類型:"+melon.getType());
});
List<Melon> melonWeight = Filters.filterByWeight( melons,3);
melonWeight.forEach(melon->{
System.out.println("瓜重量:"+melon.getWeight());
});
}
[]:
雖然這個需求對應阿呆很簡單,他也很快就搞定了,但是作爲一個有追求的程序員,他覺得不開心。因爲他發現filterByWeight()
與 filterByType()
非常相似,就是過濾條件不同。阿呆心想,如果客戶技術這樣提需求,那麼 Filters
將會有很多這樣類似的方法,也就是說寫了很多樣板代碼(代碼冗餘但又不得不寫);
第三次 按類型和重量篩選瓜
果不其然,事情變得越來越糟。客戶又要求我們添加一個新的過濾方式,該過濾方式可以按類型和重量過濾瓜類。爲了滿足客戶需求,阿呆很快寫了如下的代碼
/**
* 按照類型和重量來篩選瓜類
* @param melons
* @param type
* @param weight
* @return
*/
public static List<Melon> filterByTypeAndWeight(List<Melon> melons, String type, int weight) {
List<Melon> result = new ArrayList<>();
for (Melon melon: melons) {
if (melon != null && type.equalsIgnoreCase(melon.getType()) && melon.getWeight() == weight) {
result.add(melon);
}
}
return result;
}
在阿呆看來,這是不能接受的。如果客戶急需添加新的過濾條件,則代碼將變得難以維護且容易出錯。
第四次 將行爲作爲參數傳遞
做完第三次需求上線之後,阿呆心想,他不能在這樣去添加更多的過濾條件。理論上Melon
類的任何屬性都有可能作爲過濾條件,這樣的話我們的Filter類將會有大量的樣板代碼,而且有些方法會非常複雜。
經過一番研究,阿呆發現我們在樣板代碼中具有不同的行爲。因此,我們只需要編寫一次樣板代碼 並將行爲作爲參數傳遞。我們可以將任何過濾條件定型爲行爲,然後作爲參數進行傳遞。這樣代碼將變得更加清晰,靈活,易於維護並且具有更少的參數。阿呆給它取了一個名字:行爲參數化,在下圖中進行了說明(左側顯示了我們現在擁有的;右側顯示了我們想要的):
如果我們將過濾條件視爲一種行爲,那麼將每種行爲視爲接口的實現是非常直觀的。阿呆經過分析認爲所有這些行爲都有一個共同點:過濾條件和boolean
類型的返回 。於是阿呆寫下了如下的代碼:
public interface MelonPredicate {
boolean test(Melon melon);
}
此外,他還編寫了一個實現 GacMelonPredicate
。例如,過濾 Gac
瓜可以這樣寫:
public class GacMelonPredicate implements MelonPredicate {
@Override
public boolean test(Melon melon) {
return "gac".equalsIgnoreCase(melon.getType());
}
}
以此類推,可以過濾掉所有重於5000g
的瓜:
public class HugeMelonPredicate implements MelonPredicate {
@Override
public boolean test(Melon melon) {
return melon.getWeight() > 5000;
}
}
其實熟悉設計模式的同學應該知道這就是:策略設計模式。主要思想就是讓系統在運行時動態選擇需要調用的方法。所以我們可以認爲 MelonPredicate
接口統一了所有專用於篩選瓜類的算法,並且每種實現都是一種策略,我們也可以把它理解爲一種行爲。
目前,我們已經有了策略,但是沒有任何方法可以接收 MelonPredicate
參數。於是阿呆定義了 filterMelons()
方法,如下圖所示:
public static List<Melon> filterMelons(List<Melon> melons, MelonPredicate predicate) {
List<Melon> result = new ArrayList<>();
for (Melon melon: melons) {
if (melon != null && predicate.test(melon)) {
result.add(melon);
}
}
return result;
}
大功告成,阿呆舒了一口氣,然後測了一番,果然比以前好用很多
List<Melon> gacs = Filters.filterMelons(melons, new GacMelonPredicate());
List<Melon> huge = Filters.filterMelons(melons, new HugeMelonPredicate());
第五次 一次性加了100個過濾條件
就在阿呆沾沾自喜時候,客戶又給他潑了一盆冷水。這傢伙最近和一個東南亞大鱷打上了關係,成功引進各種類型東南亞瓜類,浩浩蕩蕩列了100個過濾條件讓阿呆開發。阿呆心裏一萬個草泥馬在奔騰啊!雖然經過上次改造,我們有足夠的靈活性來完成此任務,但是我們仍然需要編寫100個策略類來實現 每一個過濾標準。然後我們需要將策略傳遞給 filterMelons()
方法。
有沒有不需要創建這些類的辦法那?聰明的阿呆很快發現可以使用java
匿名內部類。如下所示
List<Melon> europeans = Filters.filterMelons(melons, new MelonPredicate() {
@Override
public boolean test(Melon melon) {
return "europe".equalsIgnoreCase(melon.getOrigin());
}
});
雖然我們向前跨了一大步,但好像無濟於事。我們還是需要編寫大量的代碼實現此次需求。
有時候,匿名內部類看這比較複雜,我們可以用lambda表達式來簡化它
List<Melon> europeansLambda = Filters.filterMelons(
melons, m -> "europe".equalsIgnoreCase(m.getOrigin()));
果然看這帥多了!!!,就這樣,阿呆又一次成功完成了任務.
第六次 提取列表類型
隨着客戶成功搭上東南亞大鱷的女兒,東南亞大鱷對他越來越放心。將他旗下的瓜果生意都交給他做。客戶端的日子好過,阿呆的日子就不好過了。果不其然,他又提需求了。他提出了需要銷售除了瓜之外的其他水果,但是我們的MelonPredicate
僅支持 Melon
實例。
這傢伙怎麼搞?說不定哪天他要買蔬菜、海蔘可怎麼搞,總不能給他再創建好多類似MelonPredicate
的接口吧
於是阿呆想到了泛型;
我們首先定義一個新接口,然後 Predicate
將Melon
其命名(從名稱中刪除 ):
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
接下來,我們重寫該 filterMelons()
方法並將其重命名爲 filter()
:
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T t: list) {
if (t != null && predicate.test(t)) {
result.add(t);
}
}
return result;
}
現在,我們可以這樣過濾瓜類 :
List<Melon> watermelons = Filters.filter(
melons, (Melon m) -> "Watermelon".equalsIgnoreCase(m.getType()));
同樣的,我們也可以對數字做同樣的事情:
List<Integer> numbers = Arrays.asList(1, 13, 15, 2, 67);
List<Integer> smallThan10 = Filters.filter(numbers, (Integer i) -> i < 10);
現在我們回過頭來看看,從哪裏開始我們的代碼發送巨大變化?發現是使用Java 8
函數式接口和lambda
表達式後,兩者之間發生巨大的變化。不知道細心的夥伴有沒有發現我們上面的 Predicate
接口上面多了一個@FunctionalInterface
上的註解,它就是標記函數式接口。
從概念上講,函數式接口僅具有一個抽象方法。
jdk 8 中有另一個新特性:default, 被 default 修飾的方法會有默認實現,不是必須被實現的方法,所以不影響 Lambda 表達式的使用。
而且,你會發現我們定義的Predicate
接口已經在Java 8
中作爲java.util.function.Predicate
接口存在 。該 java.util.function
包下包含40多個此類接口。因此,在定義一個新的函數式接口之前,建議先檢查該包的內容。大多數情況下,六個標準的內置函數式接口可以完成任務。這些列出如下:
Predicate<T>
Consumer<T>
Supplier<T>
Function<T, R>
UnaryOperator<T>
BinaryOperator<T>
函數式接口和lambda
表達式組成了一個強大的團隊。Lambda
表達式支持直接內聯函數式接口的抽象方法的實現。基本上,整個表達式被視爲函數式接口的具體實現的一個實例,比如:
Predicate<Melon> predicate = (Melon m)
-> "Watermelon".equalsIgnoreCase(m.getType());
簡而言之Lambda
lambda
表達式由三部分組成,如下圖所示:
以下是lambda
表達式各部分的描述:
- 在箭頭的左側,是在
lambda
主體中使用的lambda
參數。這些是FilenameFilter.accept (File folder, String fileName)
方法的參數 。 - 在箭頭的右側,是
lambda
主體,在上面的例子中,該主體檢查文件夾是否可讀以及文件是否以.pdf
後綴結尾 。 - 箭頭只是
lambda
參數和主體的分隔符。
此lambda
的匿名類版本如下:
FilenameFilter filter = new FilenameFilter() {
@Override
public boolean accept(File folder, String fileName) {
return folder.canRead() && fileName.endsWith(".pdf");
}
};
現在,如果我們查看lambda
及其匿名類版本,可以從下面四方面來描述lambda
表達式:
我們可以將 lambda
表達式定義爲一種 簡潔、可傳遞的匿名函數,首先我們需要明確 lambda
表達式本質上是一個函數,雖然它不屬於某個特定的類,但具備參數列表、函數主體、返回類型,甚至能夠拋出異常;其次它是匿名的,lambda
表達式沒有具體的函數名稱;lambda
表達式可以像參數一樣進行傳遞,從而簡化代碼的編寫。
Lambda
支持行爲參數化,在前面的例子中,我們已經證明這一點。最後,請記住,lambda
只能在函數式接口的上下文中使用。
總結
在本文中,我們重點介紹了函數式接口的用途和可用性,我們將研究如何將代碼從開始的樣板代碼現演變爲基於功能接口的靈活實現。希望對大家理解函數式接口有所幫助,謝謝大家。
關於作者
筆者麥洛是java
開發者和技術愛好者,目前關注java
、Spring
、微服務、雲原生方向。爲了將內容精準推給喜歡我的小夥伴,在大家建議下開通了公衆號。喜歡的小夥伴可以關注我,第一時間獲取文章信息。也可以後臺回覆加羣加入交流羣,大家一起進步!謝謝大家對我的支持。