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