Java函數式編程分析

原創文章, 轉載請私信. 訂閱號 tastejava 學習加思考, 仔細品味java之美

Java開發者一般都習慣面向對象編程, 實際項目中函數式編程出現頻率也不太高, 要理解函數式編程首先要理解一些前置概念, 我來總結一下Java中的函數式編程, 如果爲其他人節約了時間, 減輕了學習成本, 那就太好了.

什麼是函數式編程

函數式編程是一種編程範式, 允許將函數作爲參數傳遞給目標方法, 目標方法也可以返回一個函數.(將方法實現延後到調用方法傳遞參數的時刻, 讓參數或者返回結果包含邏輯)

Js中應用函數式編程

Javascript中函數式編程應用很廣泛, 由於js中函數本身也是一種變量, 所以js中很輕鬆就能實現將方法當做參數傳遞到方法中, 配合箭頭函數修復this指向問題, js中可以很輕鬆的應用函數式編程. Promise風格的Http庫Axios中經常會傳遞方法res => {}到then()中去處理請求到的數據.

約定

爲了方便描述, 我來定義幾個名詞

參數方法: 將方法當做參數傳遞給另一個方法
目標方法: 要調用的方法
結果方法: 將方法作爲結果返回

JDK8以前匿名內部類實現函數式編程

從函數式編程的含義可以瞭解到, 一種開發語言要支持函數式編程最首要的問題就是讓方法可以當做參數來傳遞. 即有能力做到將參數方法傳遞給目標方法, 目標方法內執行參數方法, 目標方法執行完畢後返回一個結果方法
在Js中方法可以賦值給變量, 自然方法可以當做實參傳遞, Java中雖然有Method來描述一個方法對象, 但是方法本身並不是一個Method類型的變量, 只不過Method對象包含着目標方法的有用信息.
由於方法在Java中不是一個變量對象. 所以我們在JDK8之前要把參數方法傳遞到目標方法中, 只能先定義一個包含參數方法的接口, 然後構造匿名內部類對象當做目標方法的參數.具體代碼實現如下:

/**
 * Author: GaoZl
 * Date: 2019/11/11
 * Time: 18:31
 * Description: 先定義一個接口, 接口中包含要當做參數的方法
 */
public interface NormalInterface {
    /**
     * 將兩個參數相加並返回
     * @param num1 數字1
     * @param num2 數字2
     * @return 返回參數之和
     */
    int add(int num1, int num2);
}
/**
 1. Author: GaoZl
 2. Date: 2019/11/11
 3. Time: 18:33
 4. Description: 利用匿名內部類實現函數式編程
 */
@Slf4j
public class TestNormalInterface {
    /**
     * 目標方法, 接收參數方法並執行
     * @param normalInterface
     * @param num1
     * @param num2
     */
    private void addAndPrint(NormalInterface normalInterface, int num1, int num2) {
        int result = normalInterface.add(num1, num2);
        log.info("兩數之和爲{}", result);
    }
    /**
     * 利用匿名內部類實現函數式編程
     */
    @Test
    public void testFunctionalProgrammingWidthEnclosingClass() {
        this.addAndPrint(new NormalInterface() {
            @Override
            public int add(int num1, int num2) {
                return num1 + num2;
            }
        }, 1, 2);
    }
}

JDK8及更高版本實現函數式編程

JDK8以前實現函數式編程有一些缺陷

  1. 匿名內部類參數方法比較繁瑣冗餘
  2. 已有的方法實現無法很方便的當做參數方法
  3. Lambda表達式方式也要預先定義函數式接口, 函數式接口參數相同可以複用, 是一種冗餘

在JDK8中支持的Lambda表達式解決了第一個缺陷, 方法引用解決了第二個缺陷, JDK內置的通用的函數式接口彌補了第三個缺陷. 雖然在這樣的條件下應用函數式編程還是沒有Js中便捷(還是需要定義參數方法對應的接口, 靜態類型語言甜蜜的包袱 😃 ), 但是已經很強大了.

Lambda表達式

Lambda表達式的Java實現如下:

		// 一個接收兩個字符串參數, 並在方法體中操作參數的Lambda表達式
		(str1, str2) -> {
	           log.info("我有一個{}", str1);
	           log.info("你有一個{}", str2);
	           log.info("我們既有{}又有{}", str1, str2);
	           log.info("What a virtue to share!");
	       }
Lambda與函數式接口(FunctionalInterface)

Java中的Lambda表達式語法資料有很多, 不再贅述. 網絡上大多數博文有一個缺點, 它們的Lambda表達式真的只是介紹Lambda表達式的語法在這裏我來補充一下Lambda表達式的定義部分.
上方的Lambda表達式代碼只是Lambda表達式變量值, 那麼變量的類型或者變量的定義在哪呢, 在Java中, Lambda表達式的定義/類型是對應的函數式接口(FunctionalInterface)

函數式接口和註解@FunctionalInterface

函數式接口定義: 滿足只有一個抽象方法的接口, 就是函數式接口
@FunctionalInterface註解表名一個接口是函數式接口, 爲了兼容低版本JDK, 這個註解不是必須的, 也就是說沒有標明此註解的接口在滿足定義時也是函數式接口.
想詳細瞭解這個註解可以查看源代碼, 源代碼中註釋很全面和清晰, 此處總結幾句關鍵的註釋

① 函數式接口就是嚴格只有一個抽象方法, 可以有其他default方法或者static方法的接口
② 如果接口標記了此註解, 定義要滿足類型是接口, 滿足函數式定義, 否則編譯不通過
③ 函數式接口實例可以通過Lambda表達式, 方法引用或者構造器引用的方式創建
④ 如果一個接口滿足函數式接口定義(第①點), 那麼編譯器會將其視爲函數式接口

下面我們定義一個函數式接口:

/**
 * Author: GaoZl
 * Date: 2019/11/11
 * Time: 17:34
 * Description: 函數式接口示例
 */
// 註解@FunctionalInterface顯式說明此接口是函數式接口, 不滿足函數式接口定義將會編譯失敗
@FunctionalInterface
public interface MyFunctionalInterface {
    // Lambda表達式對應的方法定義, 用於當做參數方法
    void shareItWithYou(String mine, String yours);

    // 函數式接口中允許存在default方法
    // 此方法用於組合多個函數式接口實例
    default MyFunctionalInterface shareWidthAThirdPerson(MyFunctionalInterface after) {
        // 此方法本身不操作參數, 返回一個結果方法 (shareWidthYou方法)
        return (mine, yours) -> {
            // 結果方法被調用時會先調用參數方法
            shareItWithYou(mine, yours);
            // 然後調用後置方法, 類似先執行 A, 再執行 B, 再執行 C 的效果
            after.shareItWithYou(mine, yours);
        };
    }

    // 默認邏輯, 相當於shareWidthYou的默認實現 例如框架中提供的函數式接口默認實現
    // 通過方法引用此方法, 引用的類型也是MyFunctionalInterface, 類似接口中不但能實現自己的接口方法, 還能多實現
    static void defaultMethod(String s, String s1) {
        System.out.println("我來自默認實現defaultMethod方法, 我們所有人一起擁有" + s + "和" + s1);
    }
}

從代碼中可以看到, 註解@FunctionalInterface是爲了避免編碼錯誤, 明確提供編譯器級別的定義約束, 雖然不是必須註解, 但是明確要創建一個函數式接口那就應該加上此註解.

函數式接口四種實例化方式與執行細節

從@FunctionalInterface註解源碼註釋中可以看到, 函數式接口可以通過Lambda表達式, 方法引用或者構造方法引用實例化, 除此之外還可以通過顯式的匿名內部類實現.下面代碼演示這四種方式實例化函數式接口, 先準備如下類, 用於演示引用構造方法實例化函數式接口.

/**
 * Author: GaoZl
 * Date: 2019/11/12
 * Time: 14:10
 * Description: 擁有兩個字符串的構造方法, 與函數式接口MyFunctionalInterface抽象方法形參一致
 */
@Slf4j
public class TestConstructorReference {
    public TestConstructorReference(String mine, String yours) {
        log.info("來自一個普通類的構造方法, 與函數式接口參數恰好一致, 可以被引用成函數式接口實例");
        log.info("接收到參數{}和{}", mine, yours);
        log.info("調用時用相應函數式接口的方法簽名, 實際執行的是引用普通方法或引用構造方法的邏輯");
    }
}

具體演示和說明代碼如下:

/**
 * Author: GaoZl
 * Date: 2019/11/11
 * Time: 17:38
 * Description: 函數式接口四種實例化方式以及執行細節
 */
@Slf4j
public class TestFunctionalInterface {
    @Test
    public void testFunctionalInterface() {
        // Lambda表達式方式創建函數式接口實例
        MyFunctionalInterface functionalOne = (str1, str2) -> {
            log.info("我有一個{}", str1);
            log.info("你有一個{}", str2);
            log.info("我們既有{}又有{}", str1, str2);
            log.info("What a virtue to share!");
        };
        // 匿名內部類方式創建函數式接口實例
        MyFunctionalInterface functionalTwo = new MyFunctionalInterface() {
            @Override
            public void shareItWithYou(String mine, String yours) {
                log.info("現在第三人也有{}和{}啦", mine, yours);
            }
        };
        // 引用靜態方法方式創建函數式接口實例
        MyFunctionalInterface functionalThree = MyFunctionalInterface::defaultMethod;
        // 引用構造犯法創建函數式接口實例
        MyFunctionalInterface functionalFour = TestConstructorReference::new;
        // 第一個實例發起調用, 利用shareWidthThirdPerson方法組合第二個第三個實例
        // shareWidthThirdPerson組合方法主要邏輯是返回新的結果方法, 結果方法主要邏輯
        // 是先調用第一個實例方法, 然後再把要組合的實例放在其後調用, 類似責任鏈式調用
        // 執行順序爲functionalOne -> functionalTwo -> functionalThree各自的shareWidthYou方法
        functionalOne.shareWidthAThirdPerson(functionalTwo)
                .shareWidthAThirdPerson(functionalThree)
                .shareWidthAThirdPerson(functionalFour)
                .shareItWithYou("apple", "banana");
    }
}

至此我們就理清了Java中Lambda表達式和函數式接口的關係.下面我們來看一下常見的函數式接口

常見的函數式接口與函數式編程

由於Java是強類型語言, 也就意味着雖然Lambda表達式的邏輯可以在傳遞參數時實現, 但是其定義也就是函數式接口, 必須提前定義. 各個函數式接口間最大的區別是唯一的抽象方法形參列表不同, Java爲此提供了一批通用的函數式接口, 用於輔助開發者應用函數式編程.

JDK1.8之前的函數式接口

從前面函數式接口的定義可以知道, 即使沒有@FunctionalInterface註解時, 接口滿足函數式接口定義, 那麼在JDK1.8以及更高版本的編譯器下就會將其視爲函數式接口, 在接收函數式接口實例作爲參數的方法就可以應用Lambda表達式, JDK1.8之前就已經提供了幾個常見的函數式接口.下面三個常見的JDK1.8以前的函數式接口在1.8版本中已經用@FunctionalInterface註解修飾.其中最早一個函數式接口Runnable從JDK1.0版本就已經提供了.

  1. Runnable (since 1.0)
  2. Callable (since 1.5)
  3. Comparator (since 1.2)

與Comparator同樣是1.2版本提供的接口Comparable也符合函數式接口定義, 不過在1.8版本中並沒有加入註解修飾, 從用法上來看Comparator可以自定義邏輯當做參數傳遞給排序方法, 而Comparable接口用於被對象實現, 表明對象可以被比較, 單獨接收Comparable的Lambda表達式實例沒有意義.所以雖然Comparable接口符合函數式接口定義, 從編譯器角度也會被看作函數式接口, 但是並沒有實用性

JDK1.8內置的通用函數式接口

JDK內置的函數式接口, 五大類

Consumer 消費者類型的函數式接口, 接收參數無返回值.

  1. Consumer<T> 接收一個參數輸入, 無返回值
  2. BiConsumer<T,U> 接收兩個參數, 無返回值
  3. DoubleConsumer 接收一個Double參數, 無返回值
  4. IntConsumer 接收一個Integer類型參數, 無返回值
  5. LongConsumer 接收一個Long類型參數, 無返回值
  6. ObjDoubleConsumer<T> 接收一個對象和一個Double參數, 無返回值
  7. ObjIntConsumer<T> 接收一個對象和一個Integer參數, 無返回值
  8. ObjLongConsumer<T> 接收一個對象和一個Long類型參數, 無返回值

Supplier 供應者類型的函數式接口, 無參數, 有返回值

  1. Supplier<T> 無參數, 返回一個結果
  2. BooleanSupplier 無參數, 返回一個Boolean值
  3. DoubleSupplier 無參數, 返回一個Double值
  4. IntSupplier 無參數, 返回一個Integer值
  5. LongSupplier 無參數, 返回一個Long類型值

Predicate 斷言類型的函數式接口, 接收輸入參數, 返回Boolean類型, 進行是否斷言

  1. Predicate<T> 接收一個輸入參數, 返回一個Boolean結果
  2. BiPredicate<T,U> 接收兩個參數, 返回一個Boolean結果
  3. DoublePredicate 接收一個Double參數, 返回一個Boolean結果
  4. IntPredicate 接收一個Integer參數, 返回一個Boolean結果
  5. LongPredicate 接收一個Long參數, 返回一個Boolean結果

Function 描述方法類型的函數式接口

  1. Function<T,R> 接收一個參數T, 返回結果R
  2. BiFunction<T,U,R> 接收兩個參數, 返回一個結果
  3. DoubleFunction<R> 接收一個Double參數, 返回一個結果
  4. DoubleToIntFunction 接收一個Double參數, 返回一個Integer結果
  5. DoubleToLongFunction 接收一個Double參數, 返回一個Long結果
  6. IntFunction<R> 接收一個Integer參數, 返回一個結果
  7. IntToDoubleFunction 接收一個Integer參數, 返回一個Double結果
  8. IntToLongFunction 接收一個Integer參數, 返回一個Long結果
  9. LongFunction<R> 接收一個Long參數, 返回一個結果
  10. LongToDoubleFunction 接收一個Long參數, 返回一個Double結果
  11. LongToIntFunction 接受一個Long參數返回一個Integer結果
  12. ToDoubleBiFunction<T,U> 接收兩個參數, 返回一個Double結果
  13. ToDoubleFunction<T> 接收一個參數, 返回一個Double結果
  14. ToIntBiFunction<T,U> 接收兩個參數, 返回一個Integer結果
  15. ToIntFunction<T> 接收一個參數, 返回一個Integer結果
  16. ToLongBiFunction<T,U> 接收兩個參數, 返回一個Long結果
  17. ToLongFunction<T> 接收一個參數, 返回一個Long結果

Operator 操作符類型的函數式接口

  1. UnaryOperator<T> 一元操作符, 接收參數T, 返回結果T
  2. LongUnaryOperator 一元操作符, 接收一個Long, 返回一個Long
  3. IntUnaryOperator 一元操作符, 接收一個Integer, 返回一個Integer
  4. DoubleUnaryOperator 一元操作符, 接收一個Double, 返回一個Double
  5. BinaryOperator<T> 二元操作符, 接收兩個同類型操作符, 返回類型也爲同類型操作符
  6. DoubleBinaryOperator 二元操作符, 接收兩個Double, 返回一個Double
  7. IntBinaryOperator 二元操作符, 接收兩個Integer, 返回Integer
  8. LongBinaryOperator 二元操作符, 接收兩個Long, 返回一個Long

總結

網絡上大多數資料比較分散, 單純的講Lambda表達式語法, 想在Java開發中應用函數式編程, 重要的是理解函數式接口與Lambda表達式的關係. 要了解函數式編程概念, 函數式接口概念, Lambda表達式概念, 再瞭解一些JDK內置的函數式接口, 就能比較順暢的使用函數式編程啦

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章