1. 程式人生 > 其它 >Java中的函數語言程式設計(二)函式式介面Functional Interface

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

寫在前面


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

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

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

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

函式式介面

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

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

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

//強烈建議加上註解@FunctionalInterface
@FunctionalInterface
publicinterfaceConsumer<T>{
//唯一的抽象方法
voidaccept(Tt);

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

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

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

Java 8中內建的函式式介面

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

1. Functions

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

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

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

publicVcomputeIfAbsent(Kkey,
Function<?superK,?extendsV>mappingFunction);

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

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

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

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

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

publicstaticintgetStringLength(Strings){
returns.length();
}

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

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

publicstaticvoidtestFunctionWithMethodReference(){
Map<String,Integer>wordMap=newHashMap<>();
IntegerwordLen=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
publicinterfaceBiFunction<T,U,R>{
/**
*給定型別分別為T的引數t和型別為U的引數u,返回一個型別為R的響應值。
*
*@paramt第一個引數
*@paramu第二個引數
*@return執行結果
*/
Rapply(Tt,Uu);

...
}


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

publicvoidreplaceAll(BiFunction<?superK,?superV,?extendsV>function)

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

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

publicstaticvoidtestBiFunctionWithLambda(){
Map<String,Integer>wordMap=newHashMap<>();
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
return0;
}else{
//其它單詞,單詞長度保持原值
returnv;
}
});

System.out.println(wordMap);
}

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

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

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

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

publicclassSupplierExample{
publicstaticvoidmain(String[]args){
testSupplierWithLambda();
}

publicstaticvoidtestSupplierWithLambda(){
finalRandomrandom=newRandom();
//生成一個隨機整數
lazyPrint(()->{
returnrandom.nextInt(100);
});

//延時3秒,生成一個隨機整數
lazyPrint(()->{
try{
System.out.println("waitingfor3s...");
Thread.sleep(3*1000);
}catch(InterruptedExceptione){
//donothing
}

returnrandom.nextInt(100);
});
}

publicstaticvoidlazyPrint(Supplier<Integer>lazyValue){
System.out.println(lazyValue.get());
}
}

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

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

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

@FunctionalInterface
publicinterfaceConsumer<T>{
/**
*對給定的單一引數執行相關操作。
*
*@paramt輸入引數
*/
voidaccept(Tt);

...
}

示例程式碼:

publicstaticvoidtestConsumer(){
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
publicinterfaceBiConsumer<T,U>{
/**
*對給定的2個引數執行相關操作。
*
*@paramt第一個引數
*@paramu第二個引數
*/
voidaccept(Tt,Uu);

...
}

示例程式碼:

publicstaticvoidtestBiConsumer(){
Map<String,String>cityMap=newHashMap<>();
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
publicinterfacePredicate<T>{
/**
*根據給定引數,計算得到一個boolean結果。
*
*@paramt輸入引數
*@return如果引數符合斷言條件,返回true,否則返回false
*/
booleantest(Tt);

...
}

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

publicstaticvoidtestPredicate(){
List<String>provinces=newArrayList<>(Arrays.asList("Guangdong","Jiangsu","Guangxi","Jiangxi","Shandong"));

booleanremoved=provinces.removeIf(s->{
returns.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
publicinterfaceUnaryOperator<T>extendsFunction<T,T>{...}

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

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

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

//將省份的字母轉換成小寫字母。
//使用方法引用(methodreference)來實現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
publicinterfaceBinaryOperator<T>extendsBiFunction<T,T,T>{...}

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

publicstaticvoidtestBinaryOperator(){
List<Integer>values=Arrays.asList(1,3,5,7,11);

//使用reduce方法進行求和:0+1+3+5+7+11=27
intsum=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
publicinterfaceConsumer<T>{
...

defaultConsumer<T>andThen(Consumer<?superT>after){
Objects.requireNonNull(after);

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

...
}

示例程式碼如下:

publicstaticvoidtestConsumerAndThen(){
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
publicinterfaceFunction<T,R>{
default<V>Function<T,V>andThen(Function<?superR,?extendsV>after){
Objects.requireNonNull(after);

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

程式碼示例:

publicstaticvoidtestFunctionAndThen(){
//wordLen計算單詞的長度
Function<String,Integer>wordLen=s->s.length();//等同於s->{returns.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=newHashMap<>();
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)。