函數(shù)式接口
在上個(gè)小節(jié)的最后,我們提到了函數(shù)式接口的概念,也知道了想要使用Lambda
表達(dá)式,則必須依賴函數(shù)式接口。本小節(jié)我們將學(xué)習(xí)函數(shù)式接口相關(guān)的知識(shí),包括什么是函數(shù)式接口,為什么需要函數(shù)式接口,如何自定義一個(gè)函數(shù)式接口,如何創(chuàng)建函數(shù)式接口的對(duì)象,以及一些 Java 內(nèi)置的函數(shù)式接口的詳細(xì)介紹等。本小節(jié)內(nèi)容較為簡(jiǎn)單,但需要讀者有Lambda
表達(dá)式前置知識(shí),學(xué)習(xí)重點(diǎn)是要了解 Java 內(nèi)置函數(shù)式接口。
1. 什么是函數(shù)式接口
函數(shù)是接口(Functional Interface
)的定義非常容易理解:只有一個(gè)抽象方法的接口,就是函數(shù)式接口??梢酝ㄟ^Lambda
表達(dá)式來創(chuàng)建函數(shù)式接口的對(duì)象。
我們來看一個(gè)在之前我們就經(jīng)常使用的Runnable
接口,Runnable
接口就是一個(gè)函數(shù)式接口,下面的截圖為 Java 源碼:

我們看到Runnable
接口中只包含一個(gè)抽象的run()
方法,并且在接口上標(biāo)注了一個(gè)@FuncationInterface
注解,此注解就是 Java 8 新增的注解,用來標(biāo)識(shí)一個(gè)函數(shù)式接口。
2. 為什么需要函數(shù)式接口
學(xué)習(xí)了這么久的 Java,我們對(duì) Java 是純種的面向?qū)ο蟮木幊陶Z言這一概念,可能有了一定的感觸,在 Java 中,一切皆是對(duì)象。但是隨著Python
、scala
等語言的興起,函數(shù)式編程的概念得到開發(fā)者們的推崇,Java 不得不做出調(diào)整以支持更廣泛的技術(shù)要求。
在面向函數(shù)編程的語言中,Lambda
表達(dá)式的類型就是函數(shù),但是在 Java 中,Lambda
表達(dá)式的類型是對(duì)象而不是函數(shù),他們必須依賴于一種特別的對(duì)象類型——函數(shù)式接口。所以說,Java 中的Lambda
表達(dá)式就是一個(gè)函數(shù)式接口的對(duì)象。我們之前使用匿名實(shí)現(xiàn)類表示的對(duì)象,都可以使用Lambda
表達(dá)式來表示。
3. 自定義函數(shù)式接口
想要自定義一個(gè)函數(shù)式接口也非常簡(jiǎn)單,在接口上做兩件事即可:
- 定義一個(gè)抽象方法:注意,接口中只能有一個(gè)抽象方法;
- 在接口上標(biāo)記
@FunctionalInterface
注解:當(dāng)然也可以不標(biāo)記,但是如果錯(cuò)寫了多個(gè)方法,編輯器就不能自動(dòng)檢測(cè)你定義的函數(shù)式接口是否有問題了,所以建議還是寫上吧。
/**
* 自定義函數(shù)式接口
* @author colorful@TaleLin
*/
@FunctionalInterface
public interface FunctionalInterfaceDemo {
void run();
}
由于標(biāo)記了@FunctionalInterface
注解,下面接口下包含兩個(gè)抽象方法的這種錯(cuò)誤寫法,編譯器就會(huì)給出提示:

4.創(chuàng)建函數(shù)式接口對(duì)象
在上面,我們自定義了一個(gè)函數(shù)式接口,那么如何創(chuàng)建它的對(duì)象實(shí)例呢?
我們可以使用匿名內(nèi)部類來創(chuàng)建該接口的對(duì)象,實(shí)例代碼如下:
/**
* 測(cè)試創(chuàng)建函數(shù)式接口對(duì)象
* @author colorful@TaleLin
*/
public class Test {
public static void main(String[] args) {
// 使用匿名內(nèi)部類方式創(chuàng)建函數(shù)式接口
FunctionalInterfaceDemo functionalInterfaceDemo = new FunctionalInterfaceDemo() {
@Override
public void run() {
System.out.println("匿名內(nèi)部類方式創(chuàng)建函數(shù)式接口");
}
};
functionalInterfaceDemo.run();
}
}
運(yùn)行結(jié)果:
匿名內(nèi)部類方式創(chuàng)建函數(shù)式接口
現(xiàn)在,我們學(xué)習(xí)了Lambda
表達(dá)式,也可以使用Lambda
表達(dá)式來創(chuàng)建,這種方法相較匿名內(nèi)部類更加簡(jiǎn)潔,也更推薦這種做法。實(shí)例代碼如下:
/**
* 測(cè)試創(chuàng)建函數(shù)式接口對(duì)象
* @author colorful@TaleLin
*/
public class Test {
public static void main(String[] args) {
// 使用 Lambda 表達(dá)式方式創(chuàng)建函數(shù)式接口
FunctionalInterfaceDemo functionalInterfaceDemo = () -> System.out.println("Lambda 表達(dá)式方式創(chuàng)建函數(shù)式接口");
functionalInterfaceDemo.run();
}
}
運(yùn)行結(jié)果:
Lambda 表達(dá)式方式創(chuàng)建函數(shù)式接口
當(dāng)然,還有一種更笨的方法,寫一個(gè)接口的實(shí)現(xiàn)類,通過實(shí)例化實(shí)現(xiàn)類來創(chuàng)建對(duì)象。由于比較簡(jiǎn)單,而且不符合我們學(xué)習(xí)函數(shù)式接口的初衷,這里就不再做實(shí)例演示了。
5. 內(nèi)置的函數(shù)式接口介紹
通過上面一系列介紹和演示,相信對(duì)于函數(shù)式接口的概念和使用,你已經(jīng)爛熟于心了。但是只知道這些還不夠用,下面的內(nèi)容才是本小節(jié)的重點(diǎn),Java 中內(nèi)置了豐富的函數(shù)式接口,位于java.util.function
包下,學(xué)習(xí)這些函數(shù)式接口有助于我們理解 Java 函數(shù)式接口的真正用途和意義。
Java 內(nèi)置了 4 個(gè)核心函數(shù)式接口:
Comsumer<T>
消費(fèi)型接口: 表示接受單個(gè)輸入?yún)?shù)但不返回結(jié)果的操作,包含方法:void accept(T t)
,可以理解為消費(fèi)者,只消費(fèi)(接收單個(gè)參數(shù))、不返回(返回為void
);Supplier<T>
供給型接口:表示結(jié)果的供給者,包含方法T get()
,可以理解為供給者,只提供(返回T
類型對(duì)象)、不消費(fèi)(不接受參數(shù));Function<T, R>
函數(shù)型接口:表示接受一個(gè)T
類型參數(shù)并返回R
類型結(jié)果的對(duì)象,包含方法R apply(T t)
;Predicate<T>
斷言型接口:確定T
類型的對(duì)象是否滿足約束,并返回boolean
值,包含方法boolean test(T t)
。
我們?cè)?Java 的 api
文檔中可以看到有一些方法的形參,會(huì)出現(xiàn)上面幾類接口,我們?cè)趯?shí)例化這些接口的時(shí)候,就可以使用Lambda
表達(dá)式的方式來實(shí)例化。
我們下面看幾個(gè)實(shí)例,消費(fèi)型接口使用實(shí)例:
import java.util.function.Consumer;
/**
* Java 內(nèi)置4大核心h函數(shù)式接口 —— 消費(fèi)型接口
* Consumer<T> void accept(T t)
* @author colorful@TaleLin
*/
public class FunctionalInterfaceDemo1 {
public static void main(String[] args) {
Consumer<String> consumer = s -> System.out.println(s);
consumer.accept("只消費(fèi),不返回");
}
}
運(yùn)行結(jié)果:
只消費(fèi),不返回
供給型接口使用實(shí)例:
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* Java 內(nèi)置4大核心h函數(shù)式接口 —— 供給型接口
* Supplier<T> T get()
* @author colorful@TaleLin
*/
public class FunctionalInterfaceDemo2 {
public static void main(String[] args) {
Supplier<String> supplier = () -> "只返回,不消費(fèi)";
String s = supplier.get();
System.out.println(s);
}
}
運(yùn)行結(jié)果:
只返回,不消費(fèi)
下面我們使用斷言型接口,來實(shí)現(xiàn)一個(gè)根據(jù)給定的規(guī)則,來過濾字符串列表的方法,實(shí)例如下:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
/**
* Java 內(nèi)置4大核心函數(shù)式接口 —— 斷言型接口
* Predicate<T> boolean test(T t)
* @author colorful@TaleLin
*/
public class FunctionalInterfaceDemo3 {
/**
* 根據(jù) Predicate 斷言的結(jié)果,過濾 list 中的字符串
* @param list 待過濾字符串
* @param predicate 提供規(guī)則的接口實(shí)例
* @return 過濾后的列表
*/
public static List<String> filterStringList(List<String> list, Predicate<String> predicate) {
// 過濾后的字符串列表
ArrayList<String> arrayList = new ArrayList<>();
for (String string: list) {
if (predicate.test(string)) {
// 如果 test 是 true,則將元素加入到過濾后的列表中
arrayList.add(string);
}
}
return arrayList;
}
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Java");
arrayList.add("PHP");
arrayList.add("Python");
arrayList.add("JavaScript");
System.out.println("過濾前:");
System.out.println(arrayList);
List<String> filterResult = filterStringList(arrayList, new Predicate<String>() {
@Override
public boolean test(String s) {
// 返回字符串中是否包含 P
return s.contains("P");
}
});
System.out.println("過濾后:");
System.out.println(filterResult);
}
}
運(yùn)行結(jié)果:
過濾前:
[Java, PHP, Python, JavaScript]
過濾后:
[PHP, Python]
當(dāng)然,我們學(xué)習(xí)了Lambda
表達(dá)式,在main()
方法中就可以不再使用匿名內(nèi)部類了,改寫main()
方法中調(diào)用filterStringList()
方法的代碼:
List<String> filterResult = filterStringList(arrayList, s -> s.contains("P"));
上面的實(shí)例代碼可能有些難以理解,跟著我的節(jié)奏來解讀一下:
- 先定義一個(gè)方法
List<String> filterStringList(List<String> list, Predicate<String> predicate)
,此方法用于根據(jù)指定的規(guī)則過濾字符串列表,接收的第一個(gè)參數(shù)為待過濾列表,第二個(gè)參數(shù)是一個(gè)函數(shù)式接口類型的規(guī)則,注意,這個(gè)參數(shù)就是規(guī)則的制定者; - 再看
filterStringList()
方法的方法體,方法體內(nèi)部對(duì)待過濾列表進(jìn)行了遍歷,會(huì)調(diào)用Predicate<T>
接口下的boolean test(T t)
方法,判斷每一個(gè)字符串是否符合規(guī)則,符合規(guī)則就追加到新的列表中,最終返回一個(gè)新的過濾后的列表; - 在
main()
方法中,我們調(diào)用了上面定義的filterStringList()
方法,第一個(gè)參數(shù)就是待過濾列表,這里的第二個(gè)參數(shù),是我們創(chuàng)建的一個(gè)斷言型接口的對(duì)象,其重寫的test(String s)
方法就是過濾規(guī)則關(guān)鍵所在,方法體就是判斷s
字符串是否包含P
字符,并一個(gè) boolean 類型的結(jié)果; - 理解了第二個(gè)參數(shù)通過匿名內(nèi)部類創(chuàng)建對(duì)象的方式,再改寫成通過
Lambda
表達(dá)式的方式創(chuàng)建對(duì)象,就不難理解了。
上面我們介紹了核心的內(nèi)置函數(shù)式接口,理解了這些接口的使用,其他接口就不難理解了??煞?a >官方文檔來查看更多。
6. 小結(jié)
通過本小節(jié)的學(xué)習(xí),我們知道了函數(shù)式接口就是只有一個(gè)抽象方法的接口,要使用Lambda
表達(dá)式,就必須依賴函數(shù)式接口;自定義函數(shù)接口建議使用@FunctionalInterface
注解來進(jìn)行標(biāo)注,當(dāng)然如果通過 Java 內(nèi)置的函數(shù)式接口就可以滿足我們的需求,就不需要我們自己自定義函數(shù)式接口了。本小節(jié)的最后,我們通過一個(gè)較為復(fù)雜的函數(shù)式接口實(shí)例,實(shí)現(xiàn)了一個(gè)過濾字符串列表的方法,如果還是不能完全理解,建議同學(xué)下面多加練習(xí)。