Consumer/Function/Predicate/Supplier

September 29, 2022

Lambda 表示式實際的型態要看函式介面,雖然可以自行定義所需的函式介面,只不過對於幾種函式介面的行為,JDK 已經定義了幾個通用的函式介面,可以先基於這些通用函式介面來撰寫程式,在必要時再考慮自訂函式介面。

JDK 定義的通用函式介面,基本上置放於 java.util.function 套件,就行為來說,基本上可以分為 ConsumerFunctionPredicateSupplier 四種。

Consumer

如果需要的行為是接受一個引數,然後處理後不傳回值,就可以使用 Consumer 介面,它的定義是:

package java.util.function;

import java.util.Objects;

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

接受 Consumer 的實際例子就是 IterableforEach 方法:

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

既然接受了引數但沒有傳回值,這行為就像純綷消耗了引數,也就是命名為 Consumer 的原因,如果產出,就是以副作用(Side effect)形式呈現,像是改變某物件狀態,或者是進行了輸入輸出,例如,使用 System.outprintln 進行輸出:

Arrays.asList("Justin", "Monica", "Irene").forEach(out::println);

Consumer 介面主要是接受單一物件實例作為引數,對於基本型態 intlongdouble,另外有 IntConsumerLongConsumerDoubleConsumer 三個函式介面;另外還有 ObjIntConsumerObjLongConsumerObjDoubleConsumer,這三個函式介面第一個參數接受物件實例,第二個參數接受的基本型態則對應至類別名稱。

Function

如果需要的是接受一個引數,然後以該引數進行計算後傳回結果,就可以使用 Function 介面,它的定義是:

package java.util.function;

import java.util.Objects;

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

因為這行為就像是數學函數 y = f(x),給予 x 值計算出 y 值的概念,因此命名為 Function,應用的例子之一是 Optionalmap 方法,如果 Optional 有包含值,那就會用指定的 Function 來取得值進行結果計算,如果結果不為 null,就建立 Optional 實例來包裹結果並傳回,如果結果為 null,或者是一開始的 Optional 沒有值,就傳回不包括值的 Optional 實例。例如:

Optional<String> nickOptional = getNickName("Justin");
out.println(nickOptional.map(String::toUpperCase));  // 顯示 CATERPILLAR

Function 的子介面為 UnaryOperator,特化為參數與傳回值都是相同型態(雖然 Java 不支援函數式語言中的運算子重載,不過這個命名顯然源自於函數式語言中,運算子也是個函數的概念):

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T,T>

對於基本型態,則有著 IntFunctionLongFunctionDoubleFunctionIntToDoubleFunctionIntToLongFunctionLongToDoubleFunctionLongToIntFunctionDoubleToIntFunctionDoubleToLongFunction 等函式介面,看看它們的名稱或API文件,作用應該都一目瞭然。

如果需要接受兩個引數而後傳回一個結果,則可以使用 BiFunction

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
    ...
}

類似地,BinaryOperatorBiFunction 的子介面,特化為兩個參數與傳回值都是相同型態,對於基本型態,也有一些對應的函式介面,只要是 BiFunction 或是 BinaryOperator 名稱結尾的,都是類似的東西,可以直接查詢 API 來瞭解。

Predicate

如果接受一個引數,然後只傳回boolean值,也就是根據傳入的引數直接論斷真假的行為,就可以使用 Predicate 函式介面,其定義為:

package java.util.function;

import java.util.Objects;

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

舉例來說,如果有個檔案名稱的 String 陣列 fileNames,想要知道其中副檔名為 .txt 的有幾個,可以如下:

long count = Stream.of(fileNames)
                   .filter(name -> name.endsWith("txt"))
                   .count();

之後還會介紹 Stream,此實例的 filter 方法接受 Predicate 實例,每個元素都會由 Predicate 來判斷是否被過濾出來保留。類似地,BiPredicate 是接受兩個引數,傳回 boolean 值,基本型態對應的函式介面,則有 IntPredicateLongPredicateDoublePredicate

Supplier

如果需要的行為是不接受任何引數,然後傳回值,可以使用 Supplier 函式介面:

package java.util.function;

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

既然不接受引數,就能傳回值,傳回值的來源就有幾個可能性,像是固定值、某個時間某個事物的狀態值、某個外部輸入值、某個要按需(On-demand)索取的(昂貴)運算等。舉例來說,也許你的某個方法需要產生亂數,而你需要不同的亂數產生方式,那可以設計為:

static void randomZero(Integer[] coins, Supplier<Integer> randomSupplier) {
    coins[randomSupplier.get()] = 0;
}

那麼就可以如此使用:

Integer[] coins = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10};     
randomZero(coins, () -> (int) (Math.random() * 10));

來看個更實際的應用之一,想想看,怎麼產生一個無限長度的數字清單呢?例如,PI 的小數是無限的,如果有個演算需要逐一走訪這些小數,不知道何時會停止,那該怎麼辦?之後會介紹到的 Stream 有個 generate 方法,可以這麼使用:

Stream<Integer> decimalNumbersOfPI = Stream.generate(() -> nextDecimalNumberOfPI());
decimalNumbersOfPI.map(n -> n + 10)
                  .filter(n -> n < 15)
                  .forEach(out::println);

使用 Stream 的原因是可以像個清單似地操作,而實際上在 forEach 真正消耗某個數字之前,並不會真正去呼叫 nextDecimalNumberOfPI,消耗掉的數字也不會被保留,因而不會耗費記憶體,因而可以實現無限長度清單的概念。

至於那些 BooleanSupplierDoubleSupplierIntSupplierLongSupplier,應該不用解釋了,真不知道就直接查詢一下 API。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter