用好JAVA中的函數式介面,輕鬆從通用代碼框架中剝離掉業務定製邏輯

来源:https://www.cnblogs.com/softwarearch/archive/2022/08/11/16577569.html
-Advertisement-
Play Games

啥是函數式介面、它和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介面裡面提供了 compareequals兩個抽象方法。這是啥原因呢?回答這個問題前,我們可以先來做個試驗。

我們自己定義一個函數式介面,裡面提供兩個抽象方法測試一下,會發現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中有預置提供了很多的函數式介面,比如SupplierConsumerPredicate等,可又分別應用於不同場景的使用。當然咯,根據業務的實際需要,我們也可以去自定義需要的函數式介面,來方便我們自己的使用。

舉個例子,有這麼一個業務場景:

一個運維資源申請平臺,需要根據資源規格不同計算各自資源的價格,最終彙總價格、並計算稅額、含稅總金額。
比如:

  1. 不同CPU核數、不同記憶體、不同磁碟大小的虛擬機,價格也是不一樣的
  2. 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;
}

考慮到我們構建的平臺代碼的靈活性與可擴展性,能不能我們不要求所有資源都去實現指定介面類,也能將定製邏輯從平臺邏輯中剝離呢?這裡,就可以藉助自定義函數式介面來實現啦。

再來回顧下函數式介面的要素是什麼:

  1. 一個普通的JAVA interface類
  2. 此Interface類中有且僅有1個public類型的介面方法;
  3. (可選)添加個 @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使用,才能將函數式介面優勢發揮出來、才能將函數式編程的思想詮釋出來。

編程範式的演進思考

前面的章節中呢,我們一起探討了下函數式介面的一些內容,而函數式介面也是函數式編程中的一部分。這裡說的函數式編程,其實是常見編程範式中的一種,也就是一種編程的思維方式或者實現方式。主流編程範式有命令式編程與聲明式編程,而函數式編程也即是聲明式編程思想的具體實踐。

那麼,該如何理解命令式編程與聲明式編程呢?先看個例子。

假如周末的中午,我突然想吃雞翅了,然後我自己動手,一番忙活之後,終於吃上雞翅了(不容易啊)!

為了實現“吃雞翅”這個目的,然後是具體的一步一步的去做對應的事情,最終實現了目的,吃上了雞翅。——這就是 命令式編程

中午吃完烤雞翅,我晚上還想再吃烤雞腿,但我不想像中午那樣去忙活了,於是我:

照樣如願的吃上雞腿了(比中午容易多了)。這裡的我,只需要聲明要吃雞腿就行了,至於這個雞腿是怎麼做出來的,完全不用關心。——這就是 聲明式編程

從上面的例子中,可以看出兩種不同編程風格的區別:

  1. 命令式編程的主要思想是關註電腦執行的步驟,即一步一步告訴電腦先做什麼再做什麼。各種主流編程語言如C、C++、JAVA等都可以遵循這種方式去寫代碼。
  2. 聲明式編程的主要思想是告訴電腦應該做什麼,但不指定具體要怎麼做。典型的聲明式編程語言,比如: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具體是怎麼去過濾的,無需關心。

總結

好啦,關於函數式介面相關的內容,就介紹到這裡啦。那麼看到這裡,相信您應該有所收穫吧?那麼你對函數式編程如何看呢?評論區一起討論下吧、我會認真對待並探討每一個評論~~

此外

我是悟道,聊技術、又不僅僅聊技術~

如果覺得有用,請點贊 + 關註讓我感受到您的支持。也可以關註下我的公眾號【架構悟道】,獲取更及時的更新。

期待與你一起探討,一起成長為更好的自己。

本文來自博客園,作者:架構悟道,歡迎關註公眾號[架構悟道]持續獲取更多乾貨,轉載請註明原文鏈接:https://www.cnblogs.com/softwarearch/p/16577569.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 想要使用多個CPU核心來進行測試,可以使用 -n 參數( 或者 --numprocesses) (使用8個核心來跑測試用例) pytest -n 8 使用 -n auto 參數可以利用電腦的所有核心來跑測試用例 測試時使用的演算法可以根據--dist命令參數定製: --dist load(預設選項): ...
  • 關於Teamind Teamind 是新一代的遠程互動會議平臺。基於 Teamind 無限延伸、任意縮放的線上白板上,每一個參與者都可以利用便簽、圖形、手繪、圖片等各種各樣的元素進行創作,而主持人可以使用計時器、目錄、演講、投票等功能引導一場順暢而高效的會議。 Teamind 就像是一個「虛擬會議室 ...
  • 目錄 一.簡介 二.效果演示 三.源碼下載 四.猜你喜歡 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 轉場 零基礎 O ...
  • Java集合02 6.ArrayList ArrayList的註意事項: Permits all element , including null ,ArrayList 可以加入null ,並且可以加入多個 ArrayList是由數組來實現數據存儲的 ArrayList基本等同於Vector,除了A ...
  • 歡迎大家移步 我的博客 查看原文。 1. 前言 上機時遇到如下 C++ 代碼 ( C 代碼): //刪除帶頭結點的多項式單鏈表中繫數為 0 項 void DelZero(PolyNode *&L) { PolyNode *pre = L, *p = pre->next; while (p != NU ...
  • 基本類型 Rust 每個值都有其確切的數據類型,總的來說可以分為兩類:基本類型和複合類型。 基本類型意味著它們往往是一個最小化原子類型,無法解構為其它類型(一般意義上來說),由以下組成: 數值類型: 有符號整數 (i8, i16, i32, i64, isize)、 無符號整數 (u8, u16,  ...
  • ##網路協議 通過電腦網路可以使多台電腦實現連接,位於同一個網路中的電腦在進行連接和通信時需要遵守一定的規則,這就好比在道路中行駛的汽車一定要遵守交通規則一樣。在電腦網路中,這些連接和通信的規則被稱為網路通信協議,它對數據的傳輸格式、傳輸速率、傳輸步驟等做了統一規定,通信雙方必須同時遵守才能 ...
  • 《笨辦法學Python3 》免費下載地址 內容簡介 · · · · · · 本書是一本Python入門書籍,適合對電腦瞭解不多,沒有學過編程,但對編程感興趣的讀者學習使用。這本書以習題的方式引導讀者一步一步學習編程,從簡單的列印一直講到完整項目的實現,讓初學者從基礎的編程技術入手,最終體驗到軟體開 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 當別人做大數據用Java、Python的時候,我使用.NET做大數據、數據挖掘,這確實是值得一說的事。 寫的並不全面,但都是實際工作中的內容。 .NET在大數據項目中,可以做什麼? 寫腳本(使用控制台程式+頂級語句) 寫工具(使用Winform) 寫介面、寫服務 使用C#寫代碼的優點是什麼? ...
  • 前言 本文寫給想學C#的朋友,目的是以儘快的速度入門 C#好學嗎? 對於這個問題,我以前的回答是:好學!但仔細想想,不是這麼回事,對於新手來說,C#沒有那麼好學。 反而學Java還要容易一些,學Java Web就行了,就是SpringBoot那一套。 但是C#方向比較多,你是學控制台程式、WebAP ...
  • 某一日晚上上線,測試同學在回歸項目黃金流程時,有一個工單項目介面報JSF序列化錯誤,馬上升級對應的client包版本,編譯部署後錯誤消失。 線上問題是解決了,但是作為程式員要瞭解問題發生的原因和本質。但這都是為什麼呢? ...
  • 本文介紹基於Python語言中TensorFlow的Keras介面,實現深度神經網路回歸的方法。 1 寫在前面 前期一篇文章Python TensorFlow深度學習回歸代碼:DNNRegressor詳細介紹了基於TensorFlow tf.estimator介面的深度學習網路;而在TensorFl ...
  • 前段時間因業務需要完成了一個工作流組件的編碼工作。藉著這個機會跟大家分享一下整個創作過程,希望大家喜歡,組件暫且命名為"easyFlowable"。 接下來的文章我將從什麼是工作流、為什麼要自研這個工作流組件、架構設計三個維度跟大家來做個整體介紹。 ...
  • 1 簡介 我們之前使用了dapr的本地托管模式,但在生產中我們一般使用Kubernetes托管,本文介紹如何在GKE(GCP Kubernetes)安裝dapr。 相關文章: dapr本地托管的服務調用體驗與Java SDK的Spring Boot整合 dapr入門與本地托管模式嘗試 2 安裝GKE ...
  • 摘要:在jvm中有很多的參數可以進行設置,這樣可以讓jvm在各種環境中都能夠高效的運行。絕大部分的參數保持預設即可。 本文分享自華為雲社區《為什麼需要對jvm進行優化,jvm運行參數之標準參數》,作者:共飲一杯無。 我們為什麼要對jvm做優化? 在本地開發環境中我們很少會遇到需要對jvm進行優化的需 ...
  • 背景 我們的業務共使用11台(阿裡雲)伺服器,使用SpringcloudAlibaba構建微服務集群,共計60個微服務,全部註冊在同一個Nacos集群 流量轉發路徑: nginx->spring-gateway->業務微服務 使用的版本如下: spring-boot.version:2.2.5.RE ...
  • 基於php+webuploader的大文件分片上傳,帶進度條,支持斷點續傳(刷新、關閉頁面、重新上傳、網路中斷等情況)。文件上傳前先檢測該文件是否已上傳,如果已上傳提示“文件已存在”,如果未上傳則直接上傳。視頻上傳時會根據設定的參數(分片大小、分片數量)進行上傳,上傳過程中會在目標文件夾中生成一個臨 ...
  • 基於php大文件分片上傳至七牛雲,使用的是七牛雲js-sdk V2版本,引入js文件,配置簡單,可以暫停,暫停後支持斷點續傳(刷新、關閉頁面、重新上傳、網路中斷等情況),可以配置分片大小和分片數量,官方文檔https://developer.qiniu.com/kodo/6889/javascrip ...