大家好,又見面了。
今天我們一起聊一聊JAVA中的函數式介面。那我們首先要知道啥是函數式介面、它和JAVA中普通的介面有啥區別?其實函數式介面也是一個Interface
類,是一種比較特殊的介面類,這個介面類有且僅有一個抽象方法(但是可以有其餘的方法,比如default
方法)。
當然,我們看原始碼的時候,會發現JDK中提供的函數式介面,都會攜帶一個 @FunctionalFunction
註解,這個註釋是用於標記此介面類是一個函數式介面,但是這個註解並非是實現函數式介面的必須項。說白了,加了這個註解,一方面可以方便程式碼的理解,告知這個程式碼是按照函數式介面來定義實現的,另一方面也是供編譯器協助檢查,如果此方法不符合函數式介面的要求,直接編譯失敗,方便程式設計師介入處理。
所以歸納下來,一個函數式介面應該具備如下特性:
@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原始碼 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;
}
考慮到我們構建的平臺程式碼的靈活性與可延伸性,能不能我們不要求所有資源都去實現指定介面類,也能將客製化邏輯從平臺邏輯中剝離呢?這裡,就可以藉助自定義函數式介面來實現啦。
再來回顧下函數式介面的要素是什麼:
@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語法是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使用,才能將函數式介面優勢發揮出來、才能將函數語言程式設計的思想詮釋出來。
前面的章節中呢,我們一起探討了下函數式介面的一些內容,而函數式介面也是函數語言程式設計中的一部分。這裡說的函數語言程式設計,其實是常見程式設計正規化中的一種,也就是一種程式設計的思維方式或者實現方式。主流程式設計正規化有指令式程式設計與宣告式程式設計,而函數語言程式設計也即是宣告式程式設計思想的具體實踐。
那麼,該如何理解指令式程式設計與宣告式程式設計呢?先看個例子。
假如週末的中午,我突然想吃雞翅了,然後我自己動手,一番忙活之後,終於吃上雞翅了(不容易啊)!
為了實現「吃雞翅」這個目的,然後是具體的一步一步的去做對應的事情,最終實現了目的,吃上了雞翅。——這就是 指令式程式設計
。
中午吃完烤雞翅,我晚上還想再吃烤雞腿,但我不想像中午那樣去忙活了,於是我:
照樣如願的吃上雞腿了(比中午容易多了)。這裡的我,只需要宣告要吃雞腿就行了,至於這個雞腿是怎麼做出來的,完全不用關心。——這就是 宣告式程式設計
。
從上面的例子中,可以看出兩種不同程式設計風格的區別:
回到程式碼中,現在有個需求:
從給定的一個數位列表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具體是怎麼去過濾的,無需關心。
好啦,關於函數式介面相關的內容,就介紹到這裡啦。那麼看到這裡,相信您應該有所收穫吧?那麼你對函數語言程式設計如何看呢?評論區一起討論下吧、我會認真對待並探討每一個評論~~
此外:
我是悟道,聊技術、又不僅僅聊技術~
如果覺得有用,請點贊 + 關注讓我感受到您的支援。也可以關注下我的公眾號【架構悟道】,獲取更及時的更新。
期待與你一起探討,一起成長為更好的自己。
本文來自部落格園,作者:架構悟道,歡迎關注公眾號[架構悟道]持續獲取更多幹貨,轉載請註明原文連結:https://www.cnblogs.com/softwarearch/p/16577569.html