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