深入淺析Java函數語言程式設計

2022-11-10 18:02:15
本篇文章給大家帶來了關於的相關知識,其中主要介紹了關於函數語言程式設計的相關內容,java在最開始是不支援函數語言程式設計的,但是在java8這個大版本里為了支援函數語言程式設計,java引入了很多重要特性,下面一起來看一下,希望對大家有幫助。

程式設計師必備介面測試偵錯工具:

推薦學習:《》

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

本文大綱如下:

Java 根據常用需求場景的用例,抽象出了幾個內建的函數式介面給開發者使用,比如FunctionSupplier 等等,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 介面中的其他三個方法中compseandThen 這兩個方法用於函數語言程式設計的組合呼叫,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),表示提供某種值的函數。其定義如下:

@FunctionalInterfacepublic 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)表示一個函數,該函數接收一個引數,但是不返回任何值。

@FunctionalInterfacepublic 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"
登入後複製

Stream 操作中像 findAny、 findFirst這樣的操作方法都會返回一個 Optional 容器,意味著結果 Stream 可能為空,因此沒有返回任何元素。我們可以通過 Optional 的 isPresent() 方法檢查是否找到了元素。Java 程式設計那些繞不開的介面 這個子系列的文章已經更新完畢,感興趣的請持續關注,後面還有更多實用、精彩的內容。


推薦學習:《》

以上就是深入淺析Java函數語言程式設計的詳細內容,更多請關注TW511.COM其它相關文章!