Java中的函數語言程式設計(二)函式式介面Functional Interface

安員外發表於2021-10-20

寫在前面


前面說過,判斷一門語言是否支援函數語言程式設計,一個重要的判斷標準就是:它是否將函式看做是“第一等公民(first-class citizens)”。
函式是“第一等公民”,意味著函式和其它資料型別具備同等的地位——可以賦值給某個變數,可以作為另一個函式的引數,也可以作為另一個函式的返回值。

Java 8是通過函式式介面,賦予了函式“第一等公民”的特性。

本文將詳細介紹Java 8中的函式式介面。

本文的示例程式碼可從gitee上獲取:https://gitee.com/cnmemset/javafp

 

函式式介面

什麼是函式式介面(function interface)?只有一個抽象方法的介面都屬於函式式介面。

按照規範,我們強烈建議在定義函式式介面時,加上註解 @FunctionalInterface,這樣在編譯階段就可以判斷該介面是否符合函式式介面的規範。當然,也可以不加註解 @FunctionalInterface,這並不影響函式式介面的定義和使用。

以下是一個典型的函式式介面 Consumer:

// 強烈建議加上註解 @FunctionalInterface
@FunctionalInterface
public interface Consumer<T> {
    // 唯一的抽象方法
    void accept(T t);

    // 可以有多個非抽象方法(預設方法)
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

函式式介面本質是一個介面(interface),所以我們可以通過一個具體的類(包括匿名類)來實現一個函式式介面。但與普通介面不同,函式式介面的實現還可以是一個lambda表示式,甚至可以是一個方法引用(method reference)。

下面,我們逐一介紹JDK中內建的一些典型的函式式介面。

 

Java 8中內建的函式式介面

Java 8新增的內建函式式介面都在包 java.util.function 中定義,主要包括:

1. Functions

在程式碼世界,最為常見的一種函式式介面是接收一個引數值,然後返回一個響應值。JDK提供了一個標準的泛型函式式介面 Function:

@FunctionalInterface
public interface Function<T, R> {
    /**
     * 給定一個型別為 T 的引數 t ,返回一個型別為 R 的響應值。
     *
     * @param t 函式引數
     * @return 執行結果
     */
    R apply(T t);
    ...
}

Function的一個經典應用場景是Map的computeIfAbsent函式。

public V computeIfAbsent(K key,
                         Function<? super K, ? extends V> mappingFunction);

computeIfAbsent函式會先判斷對應key在map中是否存在,如果key不存在,則通過引數 mappingFunction 來計算得出一個value,並將這個鍵值對<key, value="">寫入到map中,並返回計算出來的value。如果key已存在,則返回map中key對應的value。</key,>

假設一個應用場景,我們要構建一個HashMap,key是某個單詞,value是單詞的字母長度。例項程式碼如下:

public static void testFunctionWithLambda() {
    // 構建一個HashMap,key是某個單詞,value是單詞的字母長度
    Map<String, Integer> wordMap = new HashMap<>();
    Integer wordLen = wordMap.computeIfAbsent("hello", s -> s.length());
    System.out.println(wordLen);
    System.out.println(wordMap);
}

上面的例項會輸出:
5
{hello=5}

注意到程式碼片段“s -> s.length()”,這是一個典型的lambda表示式,含義等同於函式:

public static int getStringLength(String s) {
    return s.length();
}

更詳盡具體的lambda表示式的介紹可以參考隨後的系列文章。

之前提到過,函式式介面也可以通過一個方法引用(method reference)來實現。例項程式碼如下:

public static void testFunctionWithMethodReference() {
    Map<String, Integer> wordMap = new HashMap<>();
    Integer wordLen = wordMap.computeIfAbsent("hello", String::length);
    System.out.println(wordLen);
    System.out.println(wordMap);
}

注意到方法引用“String::length”,Java 8允許我們將一個例項方法轉化成一個函式式介面的實現。 它的含義和 lambda 表示式 “s -> s.length()” 是相同的。

更詳盡具體的方法引用的介紹可以參考隨後的系列文章。

—BiFunction
Function 限制了只能有一個引數,但兩個引數的情形也非常常見,所以就有了BiFunction,它接收兩個引數值,然後返回一個響應值。

@FunctionalInterface
public interface BiFunction<T, U, R> {
    /**
     * 給定型別分別為 T 的引數 t 和 型別為 U 的引數 u,返回一個型別為 R 的響應值。
     *
     * @param t 第一個引數
     * @param u 第二個引數
     * @return 執行結果
     */
    R apply(T t, U u);

    ...
}


Function的一個經典應用場景是Map的replaceAll函式。

public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)

Map的replaceAll函式,會遍歷Map中的所有Entry,通過BiFunction型別的引數 function 計算出一個新值,然後用新值替換舊值。

假設一個應用場景,我們使用一個HashMap,記錄了一些單詞和它們的長度,接著產品經理提了一個新需求,要求對某些指定的單詞,長度統一記錄為0。例項程式碼如下:

public static void testBiFunctionWithLambda() {
    Map<String, Integer> wordMap = new HashMap<>();
    wordMap.put("hello", 5);
    wordMap.put("world", 5);
    wordMap.put("on", 2);
    wordMap.put("at", 2);

    // lambda表示式中的k和v,分別是Map中Entry的key和原值value。
    // lambda表示式的返回值是一個新值value。
    wordMap.replaceAll((k, v) -> {
        if ("on".equals(k) || "at".equals(k)) {
            // 對應單詞 on 和 at,單詞長度統一記錄為 0
            return 0;
        } else {
            // 其它單詞,單詞長度保持原值
            return v;
        }
    });

    System.out.println(wordMap);
}

上述程式碼的輸出為:
{world=5, at=0, hello=5, on=0}

 

2. Supplier
除了Function和BiFunction,還有一種常見的函式式介面是不需要任何引數,直接返回一個響應值。這就是Supplier:

@FunctionalInterface
public interface Supplier<T> {
    /**
     * 獲取一個型別為 T 的物件例項。
     *
     * @return 物件例項
     */
    T get();
}

Supplier的一個典型應用場景是快速實現了工廠類的生產方法,包括延時的或者非同步的生產方法。例項程式碼如下:

public class SupplierExample {
    public static void main(String[] args) {
        testSupplierWithLambda();
    }

    public static void testSupplierWithLambda() {
        final Random random = new Random();
        // 生成一個隨機整數
        lazyPrint(() -> {
            return random.nextInt(100);
        });

        // 延時3秒,生成一個隨機整數
        lazyPrint(() -> {
            try {
                System.out.println("waiting for 3s...");
                Thread.sleep(3*1000);
            } catch (InterruptedException e) {
                // do nothing
            }

            return random.nextInt(100);
        });
    }

    public static void lazyPrint(Supplier<Integer> lazyValue) {
        System.out.println(lazyValue.get());
    }
}

上述程式碼輸出類似:
26
waiting for 3s…
27

 

3. Consumers
如果說Supplier屬於生產者,那與之相對的是消費者Consumer。

Consumer
與Supplier相反,Consumer 接收一個引數,而不返回任何值。

@FunctionalInterface
public interface Consumer<T> {
    /**
     * 對給定的單一引數執行相關操作。
     *
     * @param t 輸入引數
     */
    void accept(T t);

    ...
}

示例程式碼:

public static void testConsumer() {
    List<String> list = Arrays.asList("Guangdong", "Zhejiang", "Jiangsu");

    // 消費 list 中的每一個元素
    list.forEach(s -> System.out.println(s));
}

上述程式碼的輸出為:
Guangdong
Zhejiang
Jiangsu

—BiConsumer
還有BiConsumer,語義和Consumer一致,不同的是BiConsumer接收2個引數。

@FunctionalInterface
public interface BiConsumer<T, U> {
    /**
     * 對給定的2個引數執行相關操作。
     *
     * @param t 第一個引數
     * @param u 第二個引數
     */
    void accept(T t, U u);

    ...
}

示例程式碼:

public static void testBiConsumer() {
    Map<String, String> cityMap = new HashMap<>();
    cityMap.put("Guangdong", "Guangzhou");
    cityMap.put("Zhejiang", "Hangzhou");
    cityMap.put("Jiangsu", "Nanjing");

    // 消費 map中的每一個(key, value)鍵值對
    cityMap.forEach((key, value) -> {
        System.out.println(String.format("%s 的省會是 %s", key, value));
    });
}

上述程式碼的輸出是:
Guangdong 的省會是 Guangzhou
Zhejiang 的省會是 Hangzhou
Jiangsu 的省會是 Nanjing

 

4. Predicate
Predicate 的含義是接收一個引數值,然後依據給定的斷言條件,返回一個boolean值。它實質上一個特殊的 Function,一個指定了返回值型別為boolean的 Function。

@FunctionalInterface
public interface Predicate<T> {
    /**
     * 根據給定引數,計算得到一個boolean結果。
     *
     * @param t 輸入引數
     * @return 如果引數符合斷言條件,返回 true,否則返回 false
     */
    boolean test(T t);

    ...
}

Predicate 的使用場景通常是用來作為某種過濾條件。例項程式碼:

public static void testPredicate() {
    List<String> provinces = new ArrayList<>(Arrays.asList("Guangdong", "Jiangsu", "Guangxi", "Jiangxi", "Shandong"));

    boolean removed = provinces.removeIf(s -> {
        return s.startsWith("G");
    });

    System.out.println(removed);
    System.out.println(provinces);
}

上述程式碼是過濾掉以字母 G 開頭的省份,輸出為:
true
[Jiangsu, Jiangxi, Shandong]

 

5. Operators
Operator 函式式介面是一種特殊的 Function,要求返回值型別和引數型別是相同的。
和 Function/BiFunction 一樣,Operators 也支援1個或2個引數。

—UnaryOperator
UnaryOperator 支援1個引數,UnaryOperator 等同於 Function<t, t="">:</t,>

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> { ... }

UnaryOperator的示例程式碼——將省份拼音轉換大寫與小寫字母:

public static void testUnaryOperator() {
    List<String> provinces = Arrays.asList("Guangdong", "Jiangsu", "Guangxi", "Jiangxi", "Shandong");

    // 將省份的字母轉換成大寫字母
    // 使用lambda表示式來實現 UnaryOperator
    provinces.replaceAll(s -> s.toUpperCase());
    System.out.println(provinces);

    // 將省份的字母轉換成小寫字母。
    // 使用方法引用(method reference)來實現 UnaryOperator
    provinces.replaceAll(String::toLowerCase);
    System.out.println(provinces);
}

上述程式碼輸出為:
[GUANGDONG, JIANGSU, GUANGXI, JIANGXI, SHANDONG]
[guangdong, jiangsu, guangxi, jiangxi, shandong]

—BinaryOperator
BinaryOperator 支援2個引數,BinaryOperator 等同於 BiFunction<t, t,="" t=""></t,>

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T, T, T> { ... }

BinaryOperator的示例程式碼——計算List中的所有整數的和:

public static void testBinaryOperator() {
    List<Integer> values = Arrays.asList(1, 3, 5, 7, 11);

    // 使用 reduce 方法進行求和:0+1+3+5+7+11 = 27
    int sum = values.stream()
            .reduce(0, (a, b) -> a + b);

    System.out.println(sum);
}

上述程式碼的輸出為:
27

 

6. Java 7及之前版本遺留的函式式介面
前面提到過函式式介面的定義:只有一個抽象方法的介面都屬於函式式介面。

按照這個定義,在Java 7或之前版本中定義的一些“老”介面也屬於函式式介面,包括:
Runnable、Callable、Comparator等等。

當然,這些遺留的函式式介面,在Java 8中也加上了註解 @FunctionalInterface 。

 

組合函式式介面

我們在第一篇提到過:函數語言程式設計是一種程式設計正規化(programming paradigm),追求的目標是整個程式都由函式呼叫以及函式組合構成的。

函式組合(function composing),指的是將一系列簡單函式組合起來形成一個複合函式。

Java 8中的函式式介面也提供了函式組合的功能。大家注意觀察,可以發現基本每個內建的函式式介面都有一個非抽象的方法 andThen。andThen方法的功能是將多個函式式介面組合在一起,以序列的順序逐一執行,從而形成一個新的函式式介面。

以Consumer.andThen方法為例,它返回一個新的Consumer例項。新的Consumer例項會先執行當前的accpet方法,然後再執行 after 的accpet方法。原始碼片段如下:

@FunctionalInterface
public interface Consumer<T> {
    ... 

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);

        // 先執行當前Consumer的accept方法,再執行 after 的accept方法
        // 特別要注意的是,accept(t) 不能寫在 return 語句之前,否則accept(t)將會被提前執行
        return (T t) -> { accept(t); after.accept(t); };
    }

    ...
}

示例程式碼如下:

public static void testConsumerAndThen() {
    Consumer<String> printUpperCase = s -> System.out.println(s.toUpperCase());
    Consumer<String> printLowerCase = s -> System.out.println(s.toLowerCase());

    // 組合得到一個新的 Consumer :先列印大寫樣式,再列印小寫樣式
    Consumer<String> prints = printUpperCase.andThen(printLowerCase);

    List<String> list = Arrays.asList("Guangdong", "Zhejiang", "Jiangsu");
    list.forEach(prints);
}

上述程式碼的輸出是:
GUANGDONG
guangdong
ZHEJIANG
zhejiang
JIANGSU
jiangsu

Function.andThen 方法則更復雜一些,它返回一個新的Function例項,在新的Function中,會先用型別為 T 的引數 t 執行當前的apply方法,得到一個型別為 R 的返回值 r,然後將 r 作為輸入引數,繼續執行 after 的apply方法,最終得到一個型別為 V 的返回值:

@FunctionalInterface
public interface Function<T, R> {
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);

        // 先用型別為 T 的引數 t 執行當前的apply方法,得到一個型別為 R 的返回值 r ;
        // 然後將 r 作為輸入引數,繼續執行 after 的apply方法,最終得到一個型別為 V 的返回值;
        // 特別要注意的是,apply(t) 不能寫在 return 語句之前,否則apply(t)將會被提前執行。
        return (T t) -> after.apply(apply(t));
    }

程式碼示例:

public static void testFunctionAndThen() {
    // wordLen 計算單詞的長度
    Function<String, Integer> wordLen = s -> s.length(); // 等同於 s -> { return s.length(); }

    // effectiveWord 單詞長度大於等於4,才認為是有效單詞
    Function<Integer, Boolean> effectiveWordLen = len -> len >= 4;

    // Function<String, Integer> 和 Function<Integer, Boolean> 組合得到一個新的 Function<String, Boolean> ,
    // 像是消消樂: <String, Integer> 遇到了 <Integer, Boolean> ,消去了 Integer 型別後,得到了 <String, Boolean> 。
    Function<String, Boolean> effectiveWord = wordLen.andThen(effectiveWordLen);

    Map<String, Boolean> wordMap = new HashMap<>();
    wordMap.computeIfAbsent("hello", effectiveWord);
    wordMap.computeIfAbsent("world", effectiveWord);
    wordMap.computeIfAbsent("on", effectiveWord);
    wordMap.computeIfAbsent("at", effectiveWord);

    System.out.println(wordMap);
}

上述程式碼輸出為:
{at=false, world=true, hello=true, on=false}

 

結語

Java 8是通過函式式介面,賦予了函式“第一等公民”的特性。

通過函式式介面,使得函式和其它資料型別一樣,可以賦值給某個變數、可以作為另一個函式的引數、也可以作為另一個函式的返回值。

函式式介面的實現,可以是一個類(包括匿名類),但更多的是一個lambda表示式或者一個方法引用(method reference)。

相關文章