Java中的函數式編程(二)函數式接口Functional Interface


寫在前面

前面說過,判斷一門語言是否支持函數式編程,一個重要的判斷標准就是:它是否將函數看做是“第一等公民(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

Function

在代碼世界,最為常見的一種函數式接口是接收一個參數值,然后返回一個響應值。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。

假設一個應用場景,我們要構建一個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);
 
    ...
}

BiFunction的一個經典應用場景是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<T> 等同於 Function<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<T> 等同於 BiFunction<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)。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM