大家好,又見面了,
今天我們一起聊一聊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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/501605.html
標籤:其他
