天天看點

一文帶你入門 Java 函數式程式設計

Java 在最開始是不支援函數式程式設計的,想來也好了解,因為在 Java 中類 Class 才是第一等公民,這就導緻在 Java 中實作程式設計不是件那麼容易的事兒,不過雖然難,但是結果我們也已經知道了,在 Java 8 這個大版本裡為了支援函數式程式設計,Java 引入了很多特重要特性,咱們在前面幾篇文章中,分别學習了其中的 Lambda 表達式和 Stream API 裡的各種流操作,今天這篇文章我們再來梳理一下 Java 内置給我們提供的函數式接口。

一文帶你入門 Java 函數式程式設計

Java 根據常用需求場景的用例,抽象出了幾個内置的函數式接口給開發者使用,比如​

​Function​

​、 ​

​Supplier​

​ 等等,Stream 中各種操作方法的參數或者是傳回值類型往往就是這些内置的函數式接口。

比如 Stream 中 map 操作方法的參數類型就是 ​

​Function​

<R> Stream<R> map(Function<? super T, ? extends R> mapper);      

那為什麼我們在平時使用 Stream 操作的 map 方法時,從來沒有見過聲明這個類型的參數呢?大家可以回顧一下我們 Stream API 操作那一篇文章裡使用 map 方法的例子,比如下面這個通過 map 方法把流中的每個元素轉換成大寫的例子。

List<String> list = new ArrayList<String>();
Stream<String> stream = list.stream();

Stream<String> streamMapped = stream.map((value) -> value.toUpperCase());      

map 方法的參數直接是一個 Lambada 表達式:

(value) -> value.toUpperCase()      

這個Lambda 表達式就是​

​Function​

​接口的實作。

函數式接口的載體通常是 Lambda 表達式,通過 Lambda 表達式,編譯器會根據 Lambda 表達式的參數和傳回值推斷出其實作的是哪個函數式接口。使用 Lambda 表達式實作接口,我們不必像匿名内部類那樣--指明類要實作的接口,是以像 Stream 操作中雖然參數或者傳回值類型很多都是 Java 的内置函數式接口,但是我們并沒有顯示的使用匿名類實作它們。

雖然Lambda 表達式使用起來很友善,不過這也從側面造成了咋一看到那些 Java 内置的函數式接口類型時,我們會有點迷惑“這貨是啥?這貨又是啥?”的感覺。

下面我們先說一下函數式程式設計、Java 的函數式接口、Lambda 為什麼隻能實作函數式接口這幾個問題,把這些東西搞清楚了再梳理 Java 内置提供了哪些函數式接口。

函數式程式設計

函數式程式設計中包含以下兩個關鍵的概念:

  • 函數是第一等公民
  • 函數要滿足一下限制
  • 函數的傳回值僅取決于傳遞給函數的輸入參數。
  • 函數的執行沒有副作用。

即使我們在寫程式的時候沒有一直遵循所有這些規則,但仍然可以從使用函數式程式設計思想編寫程式中獲益良多。

接下來,我們來看一下這兩個關鍵概念再 Java 函數程式設計中的落地。

函數是一等公民

在函數式程式設計範式中,函數是語言中的第一等公民。這意味着可以建立函數的“執行個體”,對函數執行個體的變量引用,就像對字元串、Map 或任何其他對象的引用一樣。函數也可以作為參數傳遞給其他函數。

在 Java 中,函數顯然不是第一等公民,類才是。是以 Java 才引入 Lambda 表達式,這個文法糖從表現層上讓 Java 擁有了函數,讓函數可以作為變量的引用、方法的參數等等。為啥說是從表現層呢?因為實際上在編譯的時候 Java 編譯器還是會把 Lambda 表達式編譯成類。

純函數

函數程式設計中,有個純函數(Pure Function)的概念,如果一個函數滿足以下條件,才是純函數:

  • 該函數的執行沒有副作用。
  • 函數的傳回值僅取決于傳遞給函數的輸入參數。

下面是一個 Java 中的純函數(方法)示例

public class ObjectWithPureFunction{

    public int sum(int a, int b) {
        return a + b;
    }
}      

上面這個​

​sum()​

​方法的傳回值僅取決于其輸入參數,而且​

​sum()​

​是沒有副作用的,它不會在任何地方修改函數之外的任何狀态(變量)。

相反,這裡是一個非純函數的例子:

public class ObjectWithNonPureFunction{
    private int value = 0;

    public int add(int nextValue) {
        this.value += nextValue;
        return this.value;
    }
}      

​add()​

​方法使用成員變量​

​value​

​來計算其傳回值,并且它還修改了​

​value​

​成員變量的狀态,這代表它有副作用,這兩個條件都導緻​

​add​

​方法不是一個純函數

正如我們看到的,函數式程式設計并不是解決所有問題的銀彈。尤其是“函數是沒有副作用的”這個原則就使得在一些場景下很難使用函數式程式設計,比如要寫入資料庫的場景,寫入資料庫就算是一個副作用。是以,我們需要做的是了解函數式程式設計擅長解決哪些問題,把它用在正确的地方。

函數式接口

Java中的函數式接口在 Lambda 表達式那篇文章裡提到過,這裡再詳細說說。函數式接口是隻有一個抽象方法的接口(抽象方法即未實作方法體的方法)。一個 Interface 接口中可以有多個方法,其中預設方法和靜态方法都自帶實作,但是隻要接口中有且僅有一個方法沒有被實作,那麼這個接口就可以被看做是一個函數式接口。

下面這個接口隻定義了一個抽象方法,顯然它是一個函數式接口:

public interface MyInterface {
    public void run();
}      

下面這個接口中,定義了多個方法,不過它也是一個函數式接口:

public interface MyInterface2 {
    public void run();

    public default void doIt() {
        System.out.println("doing it");
    }

    public static void doItStatically() {
        System.out.println("doing it statically");
    }
}      

因為​

​doIt​

​方法在接口中定義了預設實作,靜态方法也有實作,接口中隻有一個抽象方法​

​run​

​沒有提供實作,是以它滿足函數式接口的要求。

這裡要注意,如果接口中有多個方法沒有被實作,那麼接口将不再是函數式接口,是以也就沒辦法用 Java 的 Lambda 表達式實作接口了。

編譯器會根據 Lambda 表達式的參數和傳回值類型推斷出其實作的抽象方法,進而推斷出其實作的接口,如果一個接口有多個抽象方法,顯然是沒辦法用 Lambda 表達式實作該接口的。

@FunctionalInterface 注解

這裡擴充一個标注接口是函數式接口的注解​

​@FunctionalInterface​

@FunctionalInterface // 标明接口為函數式接口
public interface MyInterface {
    public void run(); //抽象方法
}      

一旦使用了該注解标注接口,Java 的編譯器将會強制檢查該接口是否滿足函數式接口的要求:“确實有且僅有一個抽象方法”,否則将會報錯。

需要注意的是,即使不使用該注解,隻要一個接口滿足函數式接口的要求,那它仍然是一個函數式接口,使用起來都一樣。該注解隻起到--标記接口訓示編譯器對其進行檢查的作用。

Java 内置的函數式接口

Java 語言内置了一組為常見場景的用例設計的函數式接口,這樣我們就不必每次用到Lambda 表達式、Stream 操作時先建立函數式接口了,Java 的接口本身也支援泛型類型,是以基本上 Java 内置的函數式接口就能滿足我們平時程式設計的需求,我自己在開發項目時,印象裡很少見過有人自定義函數式接口。

在接下來的部分中,我們詳細介紹下 Java 内置為我們提供了的函數式接口。

Function

​Function​

​接口(全限定名:java.util.function.Function)是Java中最核心的函數式接口。 ​

​Function​

​ 接口表示一個接受單個參數并傳回單個值的函數(方法)。以下是 Function 接口定義的:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
            return t -> t;
    }      

​Function​

​接口本身隻包含一個需要實作的抽象方法​

​apply​

​,其他幾個方法都已在接口中提供了實作,這正好符合上面我們講的函數式接口的定義:“有且僅有一個抽象方法的接口”。

Function 接口中的其他三個方法中​

​compse​

​、​

​andThen​

​ 這兩個方法用于函數式程式設計的組合調用,​

​identity​

​用于傳回調用實體對象本身,我們之前在把對象 List 轉換為 Map 的内容中提到過,可以回看前面講 List 的文章複習。

​Function​

​接口用Java 的類這麼實作

public class AddThree implements Function<Long, Long> {

    @Override
    public Long apply(Long aLong) {
        return aLong + 3;
    }

    public static void main(String[] args) {
        Function<Long, Long> adder = new AddThree();
        Long result = adder.apply(4L);
        System.out.println("result = " + result);
    }
}      

不過現實中沒有這麼用的,前面說過 Lambda 表達式是搭配函數式接口使用的,用Lambda表達式實作上Function 接口隻需要一行,上面那個例子用 Lambda 實作的形式是:

Function<Long, Long> adder = (value) -> value + 3;
Long resultLambda = adder.apply(8L);
System.out.println("resultLambda = " + resultLambda);      

是不是簡潔了很多。後面的接口示例統一用 Lambda 表達式舉例,不再用類實作占用太多篇幅。

​Function​

​接口的常見應用是 Stream API 中的 map 操作方法,該方法的參數類型是​

​Function​

​接口,表示參數是一個“接收一個參數,并傳回一個值的函數”。

<R> Stream<R> map(Function<? super T, ? extends R> mapper);      

是以我們在代碼裡常會見到這樣使用 map 操作:

stream.map((value) -> value.toUpperCase())      
Predicate

Predicate 接口 (全限定名:java.util.function.Predicate)表示一個接收單個參數,并傳回布爾值 true 或 false 的函數。以下是 Predicate 功能接口定義:

public interface Predicate<T> {
    boolean test(T t);
}      

Predicate 接口裡還有幾個提供了預設實作的方法,用于支援函數組合等功能,這裡不再贅述。 用 Lambda 表達式實作 Predicate 接口的形式如下:

Predicate predicate = (value) -> value != null;      

Stream API 中的 filter 過濾操作,接收的就是一個實作了 Predicate 接口的參數。

Stream<T> filter(Predicate<? super T> predicate);      

寫代碼時,會經常見到這樣編寫的 filter 操作:

Stream<String> longStringsStream = stream.filter((value) -> {
    // 元素長度大于等于3,傳回true,會被保留在 filter 産生的新流中。
    return value.length() >= 3;
});      
Supplier

Supplier 接口(java.util.function.Supplier),表示提供某種值的函數。其定義如下:

@FunctionalInterface
public interface Supplier<T> {
    T get();
}      

Supplier接口也可以被認為是工廠接口,它産生一個泛型結果。與 Function 不同的是,Supplier 不接受參數。

Supplier<Integer> supplier = () -> new Integer((int) (Math.random() * 1000D));      

上面這個 Lambda 表達式的 Supplier 實作,用于傳回一個新的 Integer 執行個體,其随機值介于 0 到 1000 之間。

Consume

Consumer 接口(java.util.function.Consume)表示一個函數,該函數接收一個參數,但是不傳回任何值。

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}      

Consumer 接口常用于表示:要在一個輸入參數上執行的操作,比如下面這個用Lambda 表達式實作的 Consumer,它将作為參數傳遞給它的​

​value​

​變量的值列印到​

​System.out​

​标準輸出中。

Consumer<Integer> consumer = (value) -> System.out.println(value);      

Stream API 中的 forEach、peek 操作方法的參數就是 Consumer 接口類型的。

Stream<T> peek(Consumer<? super T> action);
void forEach(Consumer<? super T> action);      

比如,Stream API 中的 forEach 操作,會像下面這樣使用 Consume 接口的實作

Stream<String> stream = stringList.stream();
// 下面是Lambda 的簡寫形式
// 完整形式為:value -> System.out.println(value);
stream.forEach(System.out::println);      
Optional

最後再介紹一下 Optional 接口,Optional 接口并不是一個函數式接口,這裡介紹它主要是因為它經常在一些 Stream 操作中出現,作為操作的傳回值類型,是以趁着學習函數式程式設計的契機也學習一下它。

Optional 接口是預防​

​NullPointerException​

​的好工具,它是一個簡單的容器,其值可以是 null 或非 null。比如一個可能傳回一個非空結果的方法,方法在有些情況下傳回值,有些情況不滿足傳回條件傳回空值,這種情況下使用 Optional 接口作為傳回類型,比直接無值時傳回 Null 要更安全。 接下來我們看看 Optional 怎麼使用:

// of 方法用于建構一個 Optional 容器
Optional<String> optional = Optional.of("bam");
// 判斷值是否為空
optional.isPresent();           // true
// 取出值,如果不存在直接取會抛出異常
optional.get();                 // "bam"
// 取值,值為空時傳回 orElse 提供的預設值
optional.orElse("fallback");    // "bam"
// 如果隻存在,執行ifPresent參數中指定的方法
optional.ifPresent((s) -> System.out.println(s.charAt(0)));// "b"      

總結