簡介
概念
Lambda 表達式可以理解為簡潔地表示可傳遞的匿名函數的一種方式:它沒有名稱,但它有參數列表、函數主體、返回類型,可能還有一個可以拋出的異常列表。
- 匿名:它不像普通方法那樣有一個明確的名稱;
- 函數:Lambda 表達式是函數是因為它不像方法那樣屬于某個特定的類,但和方法一樣,Lambda 有參數列表、函數主體、返回類型,還可能有可以拋出的異常列表;
- 傳遞:Lambda 表達式可以作為參數傳遞給方法或存儲在變量中;
- 簡潔:無需像匿名類那樣寫很多模板代碼;
組成
Lambda 表達式由參數列表、箭頭和 Lambda 主體組成。
(Apple o1, Apple o2) -> Integer.valueOf(o1.getWeight()).compareTo(Integer.valueOf(o2.getWeight()))
- 參數列表:這里采用了 Comparator 中 compareTo 方法的參數;
- 箭頭:箭頭把參數列表和 Lambda 主體分開;
- Lambda 主體:表達式就是 Lambda 的返回值;
表達式
Java8中有效的 Lambda 表達式如下:
Lambda 表達式 | 含義 |
---|---|
(String s) -> s.length() |
表達式具有一個 String 類型的參數并返回一個 int。 Lambda 沒有 return 語句,因為已經隱含的 return,可以顯示調用 return。 |
(Apple a) -> a.getWeight() > 150 |
表達式有一個 Apple 類型的參數并返回一個 boolean 值 |
(int x, int y) -> { System.out.printn("Result"); System.out.printn(x + y)}
|
表達式具有兩個 int 類型的參數而沒有返回值(void返回),Lambda 表達式可以包含多行語句,但必須要使用大括號包起來。 |
() -> 42 |
表達式沒有參數,返回一個 int 類型的值。 |
(Apple o1, Apple o2) -> Integer.valueOf(o1.getWeight()) .compareTo (Integer.valueOf(o2.getWeight()))
|
表達式具有兩個 Apple 類型的參數,返回一個 int 比較重要。 |
下面提供一些 Lambda 表達式的使用案例:
使用案例 | Lambda 示例 |
---|---|
布爾表達式 | (List<String> list) -> list.isEmpty() |
創建對象 | () -> new Apple(10) |
消費對象 | (Apple a) -> { System.out.println(a.getWeight) } |
從一個對象中選擇/抽取 | (String s) -> s.lenght() |
組合兩個值 | (int a, int b) -> a * b |
比較兩個對象 | ``(Apple o1, Apple o2) -><br> Integer.valueOf(o1.getWeight())<br> .compareTo(Integer.valueOf(o2.getWeight()))` |
如何使用 Lambda
到底在哪里可以使用 Lambda 呢?你可以在函數式接口上使用 Lambda 表達式。
函數式接口
函數式接口就是只定義一個抽象方法的接口,比如 Java API 中的 Predicate、Comparator 和 Runnable 等。
public interface Predicate<T> {
boolean test(T t);
}
public interface Comparator<T> {
int compare(T o1, T o2);
}
public interface Runnable {
void run();
}
用函數式接口可以干什么呢?Lambda 表達式允許你直接以內聯的形式為函數式接口的抽象方法提供實現,并把整個表達式作為函數式接口的實例(具體說來,是函數式接口一個具體實現 的實例)。你用匿名內部類也可以完成同樣的事情,只不過比較笨拙:需要提供一個實現,然后 再直接內聯將它實例化。下面的代碼是有效的,因為Runnable是一個只定義了一個抽象方法run 的函數式接口:
//使用Lambda
Runnable r1 = () -> System.out.println("Hello World 1");
//匿名類
Runnable r2 = new Runnable(){
public void run(){
System.out.println("Hello World 2");
}
};
public static void process(Runnable r){
r.run();
}
process(r1); //打印 "Hello World 1"
process(r2); //打印 "Hello World 2"
//利用直接傳遞的 Lambda 打印 "Hello World 3"
process(() -> System.out.println("Hello World 3"));
函數描述符
函數式接口的抽象方法的簽名基本上就是 Lambda 表達式的簽名。我們將這種抽象方法叫作函數描述符。例如,Runnable 接口可以看作一個什么也不接受什么也不返回(void)的函數的簽名,因為它只有一個叫作 run 的抽象方法,這個方法什么也不接受,什么也不返回(void)。
Lambda 實踐
讓我們通過一個例子,看看在實踐中如何利用Lambda和行為參數化來讓代碼更為靈活,更為簡潔。
資源處理(例如處理文件或數據庫)時一個常見的模式就是打開一個資源,做一些處理,然后關閉資源。這個設置和清理階段總是很類似,并且會圍繞著執行處理的那些重要代碼。這就是所謂的環繞執行(execute around)模式。
例如,在以下代碼中,高亮顯示的BufferedReader reader = new BufferedReader(new FileReader("data.txt"))
就是從一個文件中讀取一行所需的模板代碼(注意你使用了Java 7中的帶資源的try語句,它已經簡化了代碼,因為你不需要顯式地關閉資源了)。
public static String processFile() throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
return reader.readLine();
}
}
第1步:行為參數化
現在上述代碼是有局限的。你只能讀文件的第一行。如果你想要返回頭兩行,甚至是返回使用最頻繁的詞, 該怎么辦呢?在理想的情況下, 你要重用執行設置和清理的代碼, 并告訴 processFile 方法對文件執行不同的操作。是的,你需要把 processFile 的行為參數化,你需要一種方法把行為傳遞給 processFile , 以便它可以利用 BufferedReader執行不同的行為。
傳遞行為正是 Lambda 的優勢。那要是想一次讀兩行,這個新的processFile方法看起來又該是什么樣的呢? 你需要一個接收BufferedReader并返回String的Lambda。例如,下面就是從 BufferedReader 中打印兩行的寫法:
String result = processFile((BufferedReader r) -> r.readLine() +r.readLine());
第2步:函數式接口傳遞行為
Lambda 僅可用于上下文是函數式接口的情況。你需要創建一個能匹配 BufferedReader -> String
,還可以拋出 IOException 異常的接口。讓我們把這一接口稱為 BufferedReaderProcessor。
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader reader) throws IOException;
}
第3步:執行一個行為
任何BufferedReader -> String
形式的 Lambda 都可以作為參數來傳遞,因為它們符合 BufferedReaderProcessor 接口中定義的 process 方法的簽名。現在只需要編寫一種方法在 processFile主體內執行 Lambda 所代表的代碼。
public static String processFile(BufferedReaderProcessor processor) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
return processor.process(reader); //處理 BufferedReader 對象
}
}
第4步:傳遞 Lambda
現在就可以通過傳遞不同的 Lambda 重用 processFile 方法,并以不同的方式處理文件了。
//打印一行
String result = processFile((BufferedReader r) -> r.readLine());
System.out.println(result);
//打印2行
result = processFile((BufferedReader r) -> r.readLine() +r.readLine());
使用函數式接口
Java 8的庫幫你在java.util.function
包中引入了幾個新的函數式接口。我們接下來介紹 Predicate、Consumer和Function 三種函數式接口。
Predicate
java.util.function.Predicate<T>
接口定義了一個名叫 test 的抽象方法,它接受泛型 T對象,并返回一個 boolean。這恰恰和你先前創建的一樣,現在就可以直接使用了。在你需要 表示一個涉及類型T的布爾表達式時,就可以使用這個接口。比如,你可以定義一個接受String 對象的Lambda表達式,如下所示。
@FunctionalInterface
public interface Predicate<T>{ boolean test(T t); }
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T s: list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer
java.util.function.Consumer<T>
定義了一個名叫 accept 的抽象方法,它接受泛型 T 的對象,沒有返回(void)。你如果需要訪問類型T的對象,并對其執行某些操作,就可以使用 這個接口。比如,你可以用它來創建一個forEach方法,接受一個Integers的列表,并對其中 每個元素執行操作。在下面的代碼中,你就可以使用這個forEach方法,并配合Lambda來打印 列表中的所有元素。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public static <T> void forEach(List<T> list, Consumer<T> c){
for(T i: list){
c.accept(i);
}
}
forEach(Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i) );
Function
java.util.function.Function<T, R>
接口定義了一個叫作apply的方法,它接受一個
泛型 T 的對象,并返回一個泛型 R 的對象。如果你需要定義一個Lambda,將輸入對象的信息映射到輸出,就可以使用這個接口(比如提取蘋果的重量,或把字符串映射為它的長度)。在下面的代碼中,我們向你展示如何利用它來創建一個map方法,以將一個String列表映射到包含每個 String長度的Integer列表。
@FunctionalInterface
public interface Function<T, R>{
R apply(T t);
}
public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for(T s: list) {
result.add(f.apply(s));
}
return result;
}
// [7, 2, 6]
List<Integer> l = map( Arrays.asList("lambdas","in","action"), (String s) -> s.length() );
原始類型特化
Java類型要么是引用類型(比如Byte、Integer、Object、List),要么是原始類型(比如int、double、byte、char)。但是泛型(比如Consumer<T>中的T)只能綁定到引用類型。這是由泛型內部的實現方式造成的。因此,在Java里有一個將原始類型轉換為對應的引用類型的機制。這個機制叫作裝箱(boxing)。相反的操作,也就是將引用類型轉換為對應的原始類型,叫作拆箱(unboxing)。Java還有一個自動裝箱機制來幫助程序員執行這一任務:裝箱和拆箱操作是自動完成的。比如一個int被裝箱成為 Integer。但這在性能方面是要付出代價的。裝箱后的值本質上就是把原始類型包裹起來,并保存在堆里。因此,裝箱后的值需要更多的內存,并需要額外的內存搜索來獲取被包裹的原始值。
Java 8為我們前面所說的函數式接口帶來了一個專門的版本,以便在輸入和輸出都是原始類型時避免自動裝箱的操作。比如,使用 IntPredicate 就避免了對值 1000 進行裝箱操作,但要是用 Predicate<Integer> 就會把參數 1000 裝箱到一個 Integer 對象中。一般來說,針對專門的輸入參數類型的函數式接口的名稱都要加上對應的原始類型前綴,比 如 DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function 接口還有針對輸出參數類型的變種:ToIntFunction<T>、IntToDoubleFunction等。
常用的函數式接口
下表中列出 Java 8 中常用的函數式接口:
函數式接口 | 函數描述符 | 原始類型特化 |
---|---|---|
Predicate<T> |
T -> boolean | IntPredicate,LongPredicate, DoublePredicate |
Consumer<T> |
T -> void | IntConsumer,LongConsumer, DoubleConsumer |
Function<T,R> |
T -> R | IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T> |
Supplier<T> |
() -> T | BooleanSupplier,IntSupplier, LongSupplier, DoubleSupplier |
UnaryOperator<T> |
T -> T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BinaryOperator<T> |
(T,T) -> T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate<L,R> |
(L,R) -> boolean | |
BiConsumer<T,U> |
(T,U) -> R | ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> |
BiFunction<T,U,R> |
(T,U) -> R | ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U> |
類型檢查、推斷以及限制
類型檢查
Lambda 的類型是從使用 Lambda 的上下文推斷出來的。上下文(比如接受它傳遞的方法的參數,或接受它的值的局部變量)中 Lambda 表達式需要的類型稱為目標類型。下圖表示了代碼的類型檢查過程:
類型檢查過程可以分解為如下所示:
- 首先,找出 filter 方法的聲明;
- 第二,找出目標類型
Predicate<Apple>
。 - 第三,
Predicate<Apple>
是一個函數式接口,定義了一個叫作 test 的抽象方法。 - 第四,test 方法描述了一個函數描述符,它可以接受一個 Apple,并返回一個 boolean。
- 最后,filter 的任何實際參數都必須匹配這個要求。
同樣的 Lambda,不同的函數式接口
用一個 Lambda 表達式就可以與不同的函數式接口聯系起來,只要它們的抽象方法簽名能夠兼容。比如,前面提到的 Callable 和 PrivilegeAction,這兩個接口都代表著什么也不接受且返回一個泛型 T 的函數。如下代碼所示兩個賦值時有效的:
Callable<Integer> c = () -> 42;
PrivilegeAction<Integer> p = () -> 42;
特殊的void兼容規則如果一個Lambda的主體是一個語句表達式, 它就和一個返回void的函數描述符兼容(當然需要參數列表也兼容)。例如,以下兩行都是合法的,盡管 List 的 add 方法返回了一個 boolean,而不是 Consumer 上下文(T -> void)所要求的void:
//Predicate 返回一個 boolean
Predicate<String> p = s -> list.add(s);
//Consumer 返回一個 void
Consumer<String> b = s -> list.add(s);
類型推斷
Java編譯器會從上下文(目標類型)推斷出用什么函數式接口來配合 Lambda 表達式,這意味著它也可以推斷出適合Lambda 的簽名,因為函數描述符可以通過目標類型來得到。這樣做的好處在于,編譯器可以了解Lambda表達式的參數類型,這樣就可以在Lambda語法中省去標注參數類型。
List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor())); //參數a沒有顯示類型
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); //無類型推斷
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); //類型推斷
使用局部變量
Lambda表達式也允許使用自由變量(不是參數,而是在外層作用域中定義的變量),就像匿名類一樣。 它們被稱作捕獲Lambda。例如,下面的Lambda捕獲了portNumber變量:
int num = 1337;
Runnable r = () -> System.out.println(num);
Lambda可以沒有限制地捕獲(也就是在其主體中引用)實例變量和靜態變量。但局部變量必須顯式聲明為final, 或事實上是final。換句話說,Lambda表達式只能捕獲指派給它們的局部變量一次。(注:捕獲 實例變量可以被看作捕獲最終局部變量this。) 例如,下面的代碼無法編譯,因為portNumber 變量被賦值兩次:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337; //錯誤:Lambda表達式引用的局 部變量必須是最終的(final) 或事實上最終的
為什么局部變量有這些限制?****第一,實例變量和局部變量背后的實現有一 個關鍵不同。實例變量都存儲在堆中,而局部變量則保存在棧上。如果Lambda可以直接訪問局部變量,而且Lambda是在一個線程中使用的,則使用Lambda的線程,可能會在分配該變量的線程將這個變量收回之后,去訪問該變量。因此,Java在訪問自由局部變量時,實際上是在訪問它的副本,而不是訪問原始變量。如果局部變量僅僅賦值一次那就沒有什么區別了——因此就有了這個限制。第二,這一限制不鼓勵你使用改變外部變量的典型命令式編程模式(這種模式會阻礙很容易做到的并行處理)。
方法引用
方法引用讓你可以重復使用現有的方法定義,并像Lambda一樣傳遞它們。在一些情況下,比起使用 Lambda 表達式,它們似乎更易讀,感覺也更自然。下面就是我們借助更新的Java 8 API,用方法引用寫的一個排序的例子:
lists.sort(comparing(Apple::getWeight);
如何使用
方法引用可以被看作僅僅調用特定方法的Lambda的一種快捷寫法。它的基本思想是,如果一個Lambda代表的只是“直接調用這個方法”,那最好還是用名稱來調用它,而不是去描述如何調用它。事實上,方法引用就是讓你根據已有的方法實現來創建 Lambda表達式。但是,顯式地指明方法的名稱,你的代碼的可讀性會更好。它是如何工作的呢? 當你需要使用方法引用時, 目標引用放在分隔符 :: 前, 方法的名稱放在后面。 例如, Apple::getWeight
就是引用了Apple類中定義的方法getWeight
。請記住,不需要括號,因為 你沒有實際調用這個方法。方法引用就是Lambda表達式(Apple a) -> a.getWeight()
的快捷寫法,下表給出了Java 8中方法引用的其他一些例子。
Lambda | 等效的引用方法 |
---|---|
(Apple a) -> a.getWeight() |
Apple::getWeight |
() -> Thread.currentThread().dumpStack() |
Thread.currentThread()::dumpStack |
(str,i) -> str.substring(i) |
String::substring |
(String i) -> System.out.println(s) |
System.out::println |
分類
方法引用主要分為三類:
- 指向靜態方法的引用(例如 Integer 的 parseInt 方法,寫作
Integer::parseInt
) - 指向任意類型實例方法的方法引用(例如 String 的 length 方法,寫作
String::length
) - 指向現有對象的實例方法的引用(假設有一個局部變量 expensiveTransaction 用于存放 Transaction 類型的對象,它支持實例方法 getValue,那么就可以寫
expensiveTransaction::getValue
)
注意,編譯器會進行一種與Lambda表達式類似的類型檢查過程,來確定對于給定的函數 式接口,這個方法引用是否有效:方法引用的簽名必須和上下文類型匹配。
構造函數引用
對于一個現有構造函數,可以利用它的名稱和關鍵字 new 來創建它的一個引用:ClassName::new
。它的功能與指向靜態方法的引用類似。
例如,假設有一個構造函數沒有參數。 它適合 Supplier 的簽名() -> Apple
。可以這樣做:
Supplier<Apple> c1 = Apple::new; //構造函數引用指向默認的 Apple() 構造函數
Apple a1 = c1.get(); //產生一個新的對象
//等價于:
Supplier<Apple> c1 = () -> new Apple(); //利用默認構造函數創建 Apple 的 Lambda 表達式
Apple a1 = c1.get();
如果你的構造函數的簽名是Apple(Integer weight)
,那么它就適合 Function 接口的簽名,于是可以這樣寫:
Function<Integer, Apple> c2 = Apple::new; //構造函數引用指向 Apple(Integer weight) 構造函數
Apple a2 = c2.apple(100);
//等價于:
Function<Integer, Apple> c2 = (Integer weight) -> new Apple(weight);
Apple a2 = c2.apple(100);
如果你有一個具有兩個參數的構造函數Apple(String color, Integer weight)
,那么它就適合BiFunction接口的簽名,于是可以這樣寫:
BiFunction<Integer, Integer, Apple> c3 = Apple::new;
Apple a3 = c23.apple("green", 100);
//等價于:
BiFunction<Integer, Apple> c3 = (String color, Integer weight) -> new Apple(color, weight);
Apple a3 = c3.apple("green", 100);
Lambda 和方法引用實戰
第1步:傳遞代碼
Java 8的API已經為你提供了一個 List 可用的 sort 方法,那么如何把排序策略傳遞給 sort 方法呢?sort方法的簽名是這樣的:
void sort(Comparator<? super E> c)
它需要一個 Comparator 對象來比較兩個Apple!這就是在Java中傳遞策略的方式:它們必須包裹在一個對象里。我們說 sort 的行為被參數化了:傳遞給它的排序策略不同,其行為也會 不同。
第一個解決方案可以是這樣的:
public class AppleComparator implements Comparator<Apple> {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
}
apples.sort(new AppleComparator())
第2步:使用匿名類
可以使用匿名類來改進方案,而不是實現一個 Comparator 卻只實例化一次:
apples.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});
第3步:使用 Lambda 表達式
接下來使用 Lambda 表達式來改進方案:
apples.sort((Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
Comparator 具有一個叫作 comparing 的靜態輔助方法,它可以接受一個 Function 來提取 Comparable 鍵值,并生成一個 Comparator 對象,它可以像下面這樣用(注意你現在傳遞的Lambda只有一 個參數:Lambda說明了如何從蘋果中提取需要比較的鍵值):
apples.sort(Comparator.comparing(((Apple apple) -> apple.getWeight())));
第4步:使用方法引用
方法引用就是替代那些轉發參數的 Lambda 表達式的語法糖。可以用方法引 用改進方案如下:
apples.sort(Comparator.comparing(Apple::getWeight));
復合 Lambda 表達式
比較器復合
- 逆序:Comparator 接口有一個默認方法 reversed 可以使給定的比較器逆序。
apples.sort(Comparator.comparing(Apple::getWeight).reversed()); //按重量遞減排序
- 比較器鏈:Comparator 接口的 thenComparing 方法接受一個函數作為參數(就像 comparing方法一樣),如果兩個對象用第一個Comparator比較之后是相等的,就提供第二個 Comparator。
apples.sort(Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getColor)); //按重量遞減排序,一樣重時,按顏色排序
謂詞復合
謂詞接口包括三個方法:negate、and和or。
//蘋果不是紅的
Predicate<Apple> notRedApple = redApple.negate();
//蘋果是紅色并且重量大于150
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
//要么是150g以上的紅蘋果,要么是綠蘋果
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150) .or(a -> "green".equals(a.getColor()));
函數復合
Function 接口的 andThen 方法Function<T, V> andThen(Function<? super R, ? extends V> after)
會返回一個函數,它先計算 andThen 的調用函數,將輸入函數的結果應用于 andThen 方法的 after 函數。
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g); //g(f(x))
int result = h.apply(1); //result = 4
Function 接口的 Compose 方法Function<V, R> compose(Function<? super V, ? extends T> before)
先計算 compose 的參數里面給的那個函數,然后再把結果用于 compose 的調用函數。
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g); //f(g(x))
int result = h.apply(1); //result = 3
小結
- Lambda表達式可以理解為一種匿名函數:它沒有名稱,但有參數列表、函數主體、返回 類型,可能還有一個可以拋出的異常的列表。
- Lambda表達式讓你可以簡潔地傳遞代碼。
- 函數式接口就是僅僅聲明了一個抽象方法的接口。
- 只有在接受函數式接口的地方才可以使用Lambda表達式。
- Lambda表達式允許你直接內聯,為函數式接口的抽象方法提供實現,并且將整個表達式作為函數式接口的一個實例。
- Java 8自帶一些常用的函數式接口,放在
java.util.function
包里,包括Predicate<T>、Function<T,R>、Supplier<T>、Consumer<T>
和BinaryOperator<T>
。 - 為了避免裝箱操作,對
Predicate<T>
和Function<T, R>
等通用函數式接口的原始類型特化:IntPredicate、IntToLongFunction等。 - 環繞執行模式(即在方法所必需的代碼中間,你需要執行點兒什么操作,比如資源分配 和清理)可以配合 Lambda 提高靈活性和可重用性。
- Lambda 表達式所需要代表的類型稱為目標類型。
- 方法引用讓你重復使用現有的方法實現并直接傳遞它們。
- Comparator、Predicate和Function等函數式接口都有幾個可以用來結合 Lambda 表達式的默認方法。
參考資料
《Java 8 實戰》