啥是函數式介面、它和JAVA中普通的介面有啥區別?函數式介面有啥用?如何在實際編碼中使用函數式介面?帶著這些問題,我們一起來認識下函數式介面的廬山真面目。 ...
大家好,又見面了。
今天我們一起聊一聊JAVA中的函數式介面。那我們首先要知道啥是函數式介面、它和JAVA中普通的介面有啥區別?其實函數式介面也是一個Interface
類,是一種比較特殊的介面類,這個介面類有且僅有一個抽象方法(但是可以有其餘的方法,比如default
方法)。
當然,我們看源碼的時候,會發現JDK中提供的函數式介面,都會攜帶一個 @FunctionalFunction
註解,這個註釋是用於標記此介面類是一個函數式介面,但是這個註解並非是實現函數式介面的必須項。說白了,加了這個註解,一方面可以方便代碼的理解,告知這個代碼是按照函數式介面來定義實現的,另一方面也是供編譯器協助檢查,如果此方法不符合函數式介面的要求,直接編譯失敗,方便程式員介入處理。
所以歸納下來,一個函數式介面應該具備如下特性:
- 是一個JAVA interface類
- 有且僅有1個公共抽象方法
- 有
@FunctionalFunction
標註(可選)
比如我們在多線程場景中都很熟悉的Runnable
介面,就是個典型的函數式介面,符合上面說的2個特性:
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
但是,我們在看JDK源碼的時候,也會看到有些函數式介面裡面有多個
抽象方法。比如JDK中的 Comparator
介面的定義如下:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
// 其他方法省略...
}
可以看到,Comparator介面裡面提供了 compare
和 equals
兩個抽象方法。這是啥原因呢?回答這個問題前,我們可以先來做個試驗。
我們自己定義一個函數式介面,裡面提供兩個抽象方法測試一下,會發現IDEA中直接就提示編譯失敗了:
同樣是這個自定義的函數式介面,我們修改下裡面的抽象方法名稱,改為 equals
方法,會發現這樣就不報錯了:
在IDEA中可能更容易看出端倪來,在上面的圖中,註意到12行代碼前面那個 @
符號了嗎?我們換種寫法,改為如下的方式,原因就更加清晰了:
原來,這個 equals
方法,其實是繼承自父類的方法,因為所有的類最終都是繼承自Object類,所以 equals
方法只能算是對父類介面的一個覆寫,而不算是此介面類自己的抽象方法,所以此方法裡面實際上還是只有 1個
抽象方法,並沒有違背函數式介面的約束條件。
函數式介面在JDK中的大放異彩
JDK源碼 java.util.function
包下麵提供的一系列的預置的函數式介面定義:
部分使用場景比較多的函數式介面的功能描述歸納如下:
介面類 | 功能描述 |
---|---|
Runnable | 直接執行一段處理函數,無任何輸出參數,也沒有任何輸出結果。 |
Supplier<T> |
執行一段處理函數,無任務輸入參數,返回一個T類型的結果。與Runnable的區別在於Supplier執行完之後有返回值。 |
Consumer<T> |
執行一段處理函數,支持傳入一個T類型的參數,執行完沒有任何返回值。 |
BiConsumer<T, U> | 與Consumer類型相似,區別點在於BiConsumer支持傳入兩個不同類型的參數,執行完成之後依舊沒有任何返回值。 |
Function<T, R> | 執行一段處理函數,支持傳入一個T類型的參數,執行完成之後,返回一個R類型的結果。與Consumer的區別點就在於Function執行完成之後有輸出值。 |
BiFunction<T, U, R> | 與Function相似,區別點在於BiFunction可以傳入兩個不同類型的參數,執行之後可以返回一個結果。與BiConsumer也很類似,區別點在於BiFunction可以有返回值。 |
UnaryOperator<T> |
傳入一個參數對象T,允許對此參數進行處理,處理完成後返回同樣類型的結果對象T。繼承Function介面實現,輸入輸出對象的類型相同。 |
BinaryOperator<T> |
允許傳入2個相同類型的參數,可以對參數進行處理,最後返回一個仍是相同類型的結果T。繼承BiFunction介面實現,兩個輸入參數以及最終輸出結果的對象類型都相同。 |
Predicate<T> |
支持傳入一個T類型的參數,執行一段處理函數,最後返回一個布爾類型的結果。 |
BiPredicate<T, U> | 支持傳入2個相同類型T的參數,執行一段處理函數,最後返回一個布爾類型的結果。 |
JDK中 java.util.function
包內預置了這麼多的函數式介面,很多場景下其實都是給JDK中其它的類或者方法中使用的,最典型的就是Stream
了——可以說有一大半預置的函數式介面類,都是為適配Stream相關能力而提供的。也正是基於函數式介面的配合使用,才是使得Stream的靈活性與擴展性尤其的突出。
下麵我們一起來看幾個Stream的方法實現源碼,來感受下函數式介面使用的魅力。
比如,Stream中的 filter
過濾操作,其實就是傳入一個元素對象,然後經過一系列的處理與判斷邏輯,最後需要給定一個boolean的結果,告知filter操作是應該保留還是丟棄此元素,所以filter方法傳入的參數就是一個 Predicate
函數式介面的具體實現(因為Predicate介面的特點就是傳入一個T對象,輸出一個boolean結果):
/**
* Returns a stream consisting of the elements of this stream that match
* the given predicate.
*/
Stream<T> filter(Predicate<? super T> predicate);
又比如,Stream中的 map
操作,是通過遍歷的方式,將元素逐個傳入函數中進行處理,並支持輸出為一個新的類型對象結果,所以map方法要求傳入一個 Function
函數式介面的具體實現:
/**
* Returns a stream consisting of the results of applying the given
* function to the elements of this stream.
*/
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
再比如,Stream中的終止操作 forEach
方法,其實就是通過迭代的方式去對元素進行逐個處理,最終其並沒有任何返回值生成,所以forEach方法定義的時候,要求傳入的是一個 Consumer
函數式介面的具體實現:
/**
* Performs an action for each element of this stream.
*/
void forEach(Consumer<? super T> action);
具體使用的時候,每個方法中都需要傳入具體函數式介面的實現邏輯,這個時候結合Lambda表達式,可以讓代碼更加的簡潔幹練(不熟悉的話,也可能會覺得更加晦澀難懂~),比如:
public void testStreamUsage(@NotNull String sentence) {
Arrays.stream(sentence.split(" "))
.filter(word -> word.length() > 5)
.sorted((o1, o2) -> o2.length() - o1.length())
.forEach(System.out::println);
}
利用函數式介面提升框架靈活度
前面章節中我們提到,JDK中有預置提供了很多的函數式介面,比如Supplier
、Consumer
、Predicate
等,可又分別應用於不同場景的使用。當然咯,根據業務的實際需要,我們也可以去自定義需要的函數式介面,來方便我們自己的使用。
舉個例子,有這麼一個業務場景:
一個運維資源申請平臺,需要根據資源規格不同計算各自資源的價格,最終彙總價格、並計算稅額、含稅總金額。
比如:
- 不同CPU核數、不同記憶體、不同磁碟大小的虛擬機,價格也是不一樣的
- 1M、2M、4M等不同規格的網路帶寬的費用也是不一樣的
在寫代碼前,我們先分析下這個處理邏輯,並分析分類出其中的通用邏輯與定製可變邏輯,如下所示:
因為我們要做的是一個通用框架邏輯,且申請的資源類型很多,所以我們顯然不可能直接在平臺框架代碼裡面通過if else
的方式來判斷類型併在框架邏輯裡面去寫每個不同資源的計算邏輯。
那按照常規的思路,我們要將定製邏輯從公共邏輯中剝離,會定義一個介面類型,要求不同資源實體類都繼承此介面類,實現介面類中的calculatePirce
方法,這樣在平臺通用計算邏輯的時候,就可以通過泛型介面調用的方式來實現我們的目的:
public PriceInfo calculatePriceInfo(List<IResource> resources) {
// 計算總價
double price = resources.stream().collect(Collectors.summarizingDouble(IResource::calculatePrice)).getSum();
// 執行後續處理策略
PriceInfo priceInfo = new PriceInfo();
priceInfo.setPrice(price);
priceInfo.setTaxRate(0.15);
priceInfo.setTax(price * 0.15);
priceInfo.setTotalPay(priceInfo.getPrice() + priceInfo.getTax());
return priceInfo;
}
考慮到我們構建的平臺代碼的靈活性與可擴展性,能不能我們不要求所有資源都去實現指定介面類,也能將定製邏輯從平臺邏輯中剝離呢?這裡,就可以藉助自定義函數式介面來實現啦。
再來回顧下函數式介面的要素是什麼:
- 一個普通的JAVA interface類
- 此Interface類中有且僅有1個public類型的介面方法;
- (可選)添加個
@FunctionalInterface
註解標識。
所以,滿足上述3點的一個自定義函數式介面,我們可以很easy的就寫出來:
@FunctionalInterface
public interface PriceComputer<T> {
double computePrice(List<T> objects);
}
然後我們在實現計算總價格的實現方法中,就可以將PriceComputer
函數介面類作為一個參數傳入,並直接調用函數式介面方法,獲取到計算後的price信息,然後進行一些後續的處理邏輯:
public <T> PriceInfo calculatePriceInfo(List<T> resources, PriceComputer<T> priceComputer) {
// 調用函數式介面獲取計算結果
double price = priceComputer.computePrice(resources);
// 執行後續處理策略
PriceInfo priceInfo = new PriceInfo();
priceInfo.setPrice(price);
priceInfo.setTaxRate(0.15);
priceInfo.setTax(price * 0.15);
priceInfo.setTotalPay(priceInfo.getPrice() + priceInfo.getTax());
return priceInfo;
}
具體調用的時候,對於不同資源的計算,具體各個資源單獨計費的邏輯可以自行傳入,無需耦合到上述的基礎方法裡面。例如需要計算一批不同規格的虛擬機的總價時,可以這樣:
// 計算虛擬機總金額
functionCodeTest.calculatePriceInfo(vmDetailList, objects -> {
double result = 0d;
for (VmDetail vmDetail : objects) {
result += 100 * vmDetail.getCpuCores() + 10 * vmDetail.getDiskSizeG() + 50 * vmDetail.getMemSizeG();
}
return result;
});
同樣地,如果想要計算一批帶寬資源的費用信息,我們可以這麼來實現:
// 計算磁碟總金額
functionCodeTest.calculatePriceInfo(networkDetailList, objects -> {
double result = 0d;
for (NetworkDetail networkDetail : objects) {
result += 20 * networkDetail.getBandWidthM();
}
return result;
});
單看調用的邏輯,也許你會有個疑問,這也沒看出代碼會有啥特別的優化改進啊,跟我直接封裝兩個私有方法似乎也沒啥差別?甚至還更複雜了?但是看calculatePriceInfo
方法會發現其作為基礎框架的能力更加通用了,將可變部分的邏輯抽象出去由業務調用方自行傳入,而無需耦合到框架裡面了(很像回調介面的感覺)。
函數式介面與Lambda的完美搭配
Lambda語法是JAVA8開始引入的一種全新的語法糖,可以進一步的簡化編碼的邏輯。在函數式介面的具體使用場景,如果結合Lambda表達式,可以使得編碼更加的簡潔、不拖沓。
我們都知道,在JAVA中的介面類是不能直接使用的,必須要有對應的實現類,然後使用具體的實現類。而有些時候如果沒有必要創建一個獨立的類時,則需要創建內部類或者匿名實現類來使用:
public void testNonLambdaUsage() {
new Thread() {
@Override
public void run() {
System.out.println("new thread executing...");
}
}.start();
}
這裡使用了匿名類的方式,先實現一個Runnable函數式介面的具體實現類,然後執行此實現類的 start()
方法。而使用Lambda語法來實現,整個代碼就會顯得很清晰了:
public void testLambdaUsage() {
new Thread(() -> System.out.println("new thread executing...")).start();
}
所以說,Lambda不是使用函數式編程的必需品,但是只有結合Lambda使用,才能將函數式介面優勢發揮出來、才能將函數式編程的思想詮釋出來。
編程範式的演進思考
前面的章節中呢,我們一起探討了下函數式介面的一些內容,而函數式介面也是函數式編程中的一部分。這裡說的函數式編程,其實是常見編程範式中的一種,也就是一種編程的思維方式或者實現方式。主流編程範式有命令式編程與聲明式編程,而函數式編程也即是聲明式編程思想的具體實踐。
那麼,該如何理解命令式編程與聲明式編程呢?先看個例子。
假如周末的中午,我突然想吃雞翅了,然後我自己動手,一番忙活之後,終於吃上雞翅了(不容易啊)!
為了實現“吃雞翅”這個目的,然後是具體的一步一步的去做對應的事情,最終實現了目的,吃上了雞翅。——這就是 命令式編程
。
中午吃完烤雞翅,我晚上還想再吃烤雞腿,但我不想像中午那樣去忙活了,於是我:
照樣如願的吃上雞腿了(比中午容易多了)。這裡的我,只需要聲明要吃雞腿就行了,至於這個雞腿是怎麼做出來的,完全不用關心。——這就是 聲明式編程
。
從上面的例子中,可以看出兩種不同編程風格的區別:
- 命令式編程的主要思想是關註電腦執行的步驟,即一步一步告訴電腦先做什麼再做什麼。各種主流編程語言如C、C++、JAVA等都可以遵循這種方式去寫代碼。
- 聲明式編程的主要思想是告訴電腦應該做什麼,但不指定具體要怎麼做。典型的聲明式編程語言,比如:SQL語言、正則表達式等。
回到代碼中,現在有個需求:
從給定的一個數字列表collection裡面,找到所有大於5的元素,用命令式編程的風格來實現,代碼如下:
List<Integer> results = new ArrayList<>();
for (int num : collection) {
if (num > 5) {
results.add(num);
}
}
而使用聲明式編程的時候,代碼如下:
List<Integer> results =
collection.stream().filter(num -> num > 5).collect(Collectors.toList());
聲明式編程的優勢,在於其更關註於“要什麼”、而會忽略掉具體怎麼做。這樣整個代碼閱讀起來會更加的接近於具體實際的訴求,比如我只需要告訴 filter
要按照 num > 5
這個條件來過濾,至於這個filter具體是怎麼去過濾的,無需關心。
總結
好啦,關於函數式介面相關的內容,就介紹到這裡啦。那麼看到這裡,相信您應該有所收穫吧?那麼你對函數式編程如何看呢?評論區一起討論下吧、我會認真對待並探討每一個評論~~
此外:
- 關於本文中涉及的演示代碼的完整示例,我已經整理並提交到github中,如果您有需要,可以自取:https://github.com/veezean/JavaBasicSkills
我是悟道,聊技術、又不僅僅聊技術~
如果覺得有用,請點贊 + 關註讓我感受到您的支持。也可以關註下我的公眾號【架構悟道】,獲取更及時的更新。
期待與你一起探討,一起成長為更好的自己。
本文來自博客園,作者:架構悟道,歡迎關註公眾號[架構悟道]持續獲取更多乾貨,轉載請註明原文鏈接:https://www.cnblogs.com/softwarearch/p/16577569.html