轉(zhuǎn)載自:《深入理解Java 8 Lambda(語(yǔ)言篇——lambda,方法引用,目標(biāo)類(lèi)型和默認(rèn)方法)》——Lucida
注:本文是筆者在上述地址學(xué)習(xí) Java SE 8 Lambda 表達(dá)式的筆記。筆者的學(xué)習(xí)習(xí)慣,是在學(xué)習(xí)過(guò)程中將內(nèi)容敲打一遍,記憶會(huì)更加深刻。本文只節(jié)選了原文一部分,更多內(nèi)容詳見(jiàn)原文。
一. 背景
不過(guò)有些 Java 對(duì)象只是對(duì)單個(gè)函數(shù)的封裝。例如下面這個(gè)典型用例:Java API 中定義了一個(gè)接口(一般被稱(chēng)為回調(diào)接口),用戶通過(guò)提供這個(gè)接口的實(shí)例來(lái)傳入指定行為,例如:
public interface ActionListener {
void actionPerformed(ActionEvent e);
}
這里并不需要專(zhuān)門(mén)定義一個(gè)類(lèi)來(lái)實(shí)現(xiàn) ActionListener 接口,因?yàn)樗粫?huì)在調(diào)用處被使用一次。用戶一般會(huì)使用匿名類(lèi)型把行為內(nèi)聯(lián)(inline):
button.addActionListener(new ActionListener) {
public void actionPerformed(ActionEvent e) {
ui.dazzle(e.getModifiers());
}
}
很多庫(kù)都依賴(lài)于上面的模式。對(duì)于并行 API 更是如此,因?yàn)槲覀冃枰汛龍?zhí)行的代碼提供給并行API,并行編程是一個(gè)非常值得研究的領(lǐng)域,因?yàn)樵谶@里摩爾定律得到了重生:盡管我們沒(méi)有更快的 CPU,但是我們有更多的 CPU。
隨著回調(diào)模式和函數(shù)式編程風(fēng)格的日益流行,我們需要在 Java 中提供一種盡可能輕量級(jí)的將代碼封裝為數(shù)據(jù)的方法。但匿名內(nèi)部類(lèi)并不是一個(gè)好的選擇,因?yàn)椋?/p>
- 語(yǔ)法過(guò)于冗余;
- 匿名類(lèi)中的this和變量名容易使人產(chǎn)生誤解;
- 類(lèi)型載入和實(shí)例創(chuàng)建語(yǔ)義不夠靈活;
- 無(wú)法捕獲非final的局部變量;
- 無(wú)法對(duì)控制流進(jìn)行抽象;
對(duì)于上述問(wèn)題,在 Java 8 中大多都被解決:
- 提供更簡(jiǎn)潔的語(yǔ)法和局部作用域規(guī)則 -> 解決了問(wèn)題 1 和問(wèn)題 2
- 提供更加靈活而且便于優(yōu)化的表達(dá)式語(yǔ)義 -> 繞開(kāi)了問(wèn)題 3
- 允許編譯器推斷變量的“常量性” -> 減輕了問(wèn)題 4
二. 函數(shù)式接口
上面提到的 ActionListener 接口只有一個(gè)方法,大多數(shù)回調(diào)接口都擁有這個(gè)特征。比如 Runnable 接口和 Comparator 接口。我們把這些只擁有一個(gè)方法的接口稱(chēng)為函數(shù)式接口。編譯器會(huì)根據(jù)接口的結(jié)構(gòu)自行判斷。
注:
- 判斷過(guò)程并非簡(jiǎn)單的對(duì)接口方法計(jì)數(shù);
- API 作者們可以通過(guò) @FunctionalInterface 注解來(lái)顯式指定一個(gè)接口是函數(shù)式接口,加上這個(gè)注解之后,編譯器就會(huì)驗(yàn)證該接口是否滿足函數(shù)式接口的要求。
函數(shù)式類(lèi)型的另一種方式,是引入一個(gè)全新的結(jié)構(gòu)化函數(shù)類(lèi)型:“箭頭”類(lèi)型。例如,一個(gè)接收 String 和 Object 并返回 int 的函數(shù)類(lèi)型可以被表示為:
(String, Object) -> int
但 Sun 公司最終出于下面的原因?qū)⑵浞穸ǎ?/p>
- 它會(huì)為Java類(lèi)型系統(tǒng)引入額外的復(fù)雜度,并帶來(lái)結(jié)構(gòu)類(lèi)型和指名類(lèi)型的混用。而 Java 幾乎全部使用指名類(lèi)型;
- 它會(huì)導(dǎo)致類(lèi)庫(kù)風(fēng)格的分歧——一些類(lèi)庫(kù)會(huì)繼續(xù)使用回調(diào)接口,而另一些類(lèi)庫(kù)會(huì)使用結(jié)構(gòu)化函數(shù)類(lèi)型;
- 它的語(yǔ)法會(huì)變得十分笨拙;
- 每個(gè)函數(shù)類(lèi)型很難擁有其運(yùn)行時(shí)表示,使開(kāi)發(fā)者受到類(lèi)型擦除 (erasure) 的困擾和局限。例如:我們無(wú)法對(duì)方法 m(T->U) 和 m(X->Y) 進(jìn)行重載;
所以 Sun 公司最終選擇了“使用已知類(lèi)型”這種方法。因?yàn)楝F(xiàn)有的類(lèi)庫(kù)大量使用了函數(shù)式接口,通過(guò)沿用這種模式,我們使得現(xiàn)有類(lèi)庫(kù)能夠直接使用 lambda 表達(dá)式。
Java SE 7 中已經(jīng)存在的函數(shù)式接口如下:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.beans.PropertyChangeListener
除此之外,Java SE 8 中增加了一個(gè)新的包:java.util.function。它里面包含了常用的函數(shù)式接口,例如:
- Predicate<T>: 接收 T 對(duì)象并返回 boolean;
- Consume<T>: 接收 T 對(duì)象,不返回值;
- Functio<T, R>: 接收 T 對(duì)象,返回 R 對(duì)象;
- Supplie<T>: 提供 T 對(duì)象(例如工廠),不接收值;
- UnaryOperato<T>: 接收 T 對(duì)象,返回 T 對(duì)象;
- BinaryOperator<T>: 接收兩個(gè) T 對(duì)象,返回 T 對(duì)象;
除了上面的這些基本的函數(shù)式接口,還有一些針對(duì)原始類(lèi)型的特化函數(shù)式接口,例如 IntSupplier 和 LongBinaryOperator。(只為 int, long, double 提供了特化函數(shù)式接口,如果需要使用其它原始類(lèi)型則需要進(jìn)行類(lèi)型轉(zhuǎn)換)
同樣,還有一些針對(duì)多個(gè)參數(shù)的函數(shù)式接口,例如 BiFunction<T, U, R>,它接收 T 對(duì)象和 U 對(duì)象,返回 R 對(duì)象。
三. lambda 表達(dá)式
lambda 表達(dá)式是匿名方法,它提供了輕量級(jí)的語(yǔ)法,從而解決了匿名內(nèi)部類(lèi)帶來(lái)的冗余語(yǔ)法問(wèn)題(又被稱(chēng)為“高度問(wèn)題”)。下面是一些lambda表達(dá)式:
(int x, int y) -> x + y
() -> 42
(String s) -> { System.out.println(s); }
這幾個(gè)表達(dá)式的意義如下:
- 第一個(gè):lambda 表達(dá)式接收 x 和 y 這兩個(gè)整形參數(shù)并返回它們的和;
- 第二個(gè):lambda 表達(dá)式不接收參數(shù),返回整數(shù)'42';
- 第三個(gè):lambda 表達(dá)式接收一個(gè)字符串并把它打印到控制臺(tái),不返回值。
lambda 表達(dá)式的語(yǔ)法由參數(shù)列表、箭頭符號(hào)->和函數(shù)體組成。其中函數(shù)體既可以是一個(gè)表達(dá)式,也可以是一個(gè)語(yǔ)句塊:
- 表達(dá)式:表達(dá)式會(huì)被執(zhí)行然后返回執(zhí)行結(jié)果;
- 語(yǔ)句塊:語(yǔ)句塊中的語(yǔ)句會(huì)被依次執(zhí)行,就像方法中的語(yǔ)句一樣;
- return語(yǔ)句會(huì)把控制權(quán)交給匿名方法的調(diào)用者;
- break和continue只能在循環(huán)中使用;
- 如果函數(shù)體有返回值,那么函數(shù)體內(nèi)部的每一條路徑都必須返回值;
lambda 表達(dá)式也會(huì)經(jīng)常出現(xiàn)在嵌套環(huán)境中,比如說(shuō)作為方法的參數(shù)。為了使 lambda 表達(dá)式在這些場(chǎng)景下盡可能簡(jiǎn)潔,我們?nèi)コ瞬槐匾姆指舴?。不過(guò)在某些情況下我們也可以把它分為多行,然后用括號(hào)包起來(lái),就像其它普通表達(dá)式一樣。
下面是一些出現(xiàn)在語(yǔ)句中的lambda表達(dá)式:
FileFilter java = (File f) -> f.getName().endsWith("*.java");
String user = doPrivileged(() -> System.getProperty("user.name"));
new Thread(() -> {
connectToService();
sendNotification();
}).start();
四. 目標(biāo)類(lèi)型
對(duì)于給定的 lambda 表達(dá)式,它的類(lèi)型是由其上下文推導(dǎo)而來(lái)。例如,下面代碼中的 lambda 表達(dá)式類(lèi)型是 ActionListener:
ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());
這就意味著,同樣的 lambda 表達(dá)式在不同上下文里可以擁有不同的類(lèi)型。例如第一個(gè) lambda 表達(dá)式 () -> "done" 是 Callable 的實(shí)例,而第二個(gè) lambda 表達(dá)式則是 PrivilegedAction 的實(shí)例。
Callable<String> c = () -> "done";
PrivilegedAction<String> a = () -> "done";
編譯器負(fù)責(zé)推導(dǎo) lambda 表達(dá)式的類(lèi)型。它利用 lambda 表達(dá)式所在上下文所期待的類(lèi)型進(jìn)行推導(dǎo),這個(gè)被期待的類(lèi)型被稱(chēng)為目標(biāo)類(lèi)型。lambda 表達(dá)式只能出現(xiàn)在目標(biāo)類(lèi)型為函數(shù)式接口的上下文中。
當(dāng)然,lambda 表達(dá)式對(duì)目標(biāo)類(lèi)型也是有要求的。編譯器會(huì)檢查 lambda 表達(dá)式的類(lèi)型和目標(biāo)類(lèi)型的方法簽名是否一致。當(dāng)且僅當(dāng)下面所有條件均滿足時(shí),lambda 表達(dá)式才可以被賦給目標(biāo)類(lèi)型 T:
- T 是一個(gè)函數(shù)式接口;
- lambda 表達(dá)式的參數(shù)和 T 的方法參數(shù)在數(shù)量和類(lèi)型上一一對(duì)應(yīng)
- lambda 表達(dá)式的返回值和 T 的方法返回值相兼容;
- lambda 表達(dá)式內(nèi)所拋出的異常和 T 的方法 throws 類(lèi)型相兼容;
由于函數(shù)式接口的目標(biāo)類(lèi)型已經(jīng)了解 lambda 表達(dá)式的形式參數(shù)類(lèi)型,所以我們沒(méi)有必要把已知類(lèi)型再重復(fù)一遍,即 lambda 表達(dá)式的參數(shù)類(lèi)型可以從目標(biāo)類(lèi)型中得出。例如:
Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);
編譯器可以推導(dǎo)出 s1 和 s2 的類(lèi)型是 String。此外,當(dāng) lambda 的參數(shù)只有一個(gè)而且它的類(lèi)型可以被推導(dǎo)得知時(shí),該參數(shù)列表外面的括號(hào)可以被省略。例如:
FileFilter java = f -> f.getName().endsWith(".java");
button.addActionListener(e -> ui.dazzle(e.getModifiers()));
這些改進(jìn)展示了我們的設(shè)計(jì)目標(biāo):“不要把高度問(wèn)題轉(zhuǎn)化成寬度問(wèn)題?!闭Z(yǔ)法元素能夠盡可能的少,以便代碼的讀者能夠直達(dá) lambda 表達(dá)式的核心部分。
五. 目標(biāo)類(lèi)型的上下文
前文提到,lambda 表達(dá)式只能出現(xiàn)在擁有目標(biāo)類(lèi)型的上下文中。這些帶有目標(biāo)類(lèi)型的上下文有:
- 變量聲明
- 賦值
- 返回語(yǔ)句
- 數(shù)組初始化器
- 方法和構(gòu)造方法的參數(shù)
- lambda 表達(dá)式函數(shù)體
- 條件表達(dá)式(? :)
- 轉(zhuǎn)型(Cast)表達(dá)式
在變量聲明、賦值、返回語(yǔ)句里,目標(biāo)類(lèi)型即是被賦值或被返回的類(lèi)型:
Comparator<String> c;
c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);
public Runnable toDoLater() {
return () -> {
System.out.println("later");
}
}
數(shù)組初始化器和賦值類(lèi)似,只是這里的“變量”變成了數(shù)組元素,而類(lèi)型是從數(shù)組類(lèi)型中推導(dǎo)得知的:
filterFiles(new FileFilter[] {
f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith("q")
});
方法參數(shù)的類(lèi)型推導(dǎo)要相對(duì)復(fù)雜,涉及到其它兩個(gè)語(yǔ)言特性重載解析和參數(shù)類(lèi)型推導(dǎo)。
重載解析會(huì)為一個(gè)給定的方法調(diào)用尋找最合適的方法聲明。由于不同的聲明具有不同的簽名,當(dāng) lambda 表達(dá)式作為方法參數(shù)時(shí),重載解析就會(huì)影響到 lambda 表達(dá)式的目標(biāo)類(lèi)型。編譯器會(huì)通過(guò)它所得之的信息來(lái)做出決定。如果 lambda 表達(dá)式具有顯式類(lèi)型(參數(shù)類(lèi)型被顯式指定),編譯器就可以直接使用 lambda 表達(dá)式的返回類(lèi)型;如果 lambda 表達(dá)式具有隱式類(lèi)型(參數(shù)類(lèi)型被推導(dǎo)而知),重載解析則會(huì)忽略 lambda 表達(dá)式函數(shù)體而只依賴(lài) lambda 表達(dá)式參數(shù)的數(shù)量。
如果在解析方法聲明時(shí)存在二義性,我們就需要利用轉(zhuǎn)型 (cast) 或顯式 lambda 表達(dá)式來(lái)提供更多的類(lèi)型信息。如果 lambda 表達(dá)式的返回類(lèi)型依賴(lài)于其參數(shù)的類(lèi)型,那么 lambda 表達(dá)式函數(shù)體有可能可以給編譯器提供額外的信息,以便其推導(dǎo)參數(shù)類(lèi)型。例如:
List<Person> ps = ...
Stream<String> names = ps.stream().map(p -> p.getName());
在上面的代碼中,ps 的類(lèi)型是 List<Person>,所以 ps.stream() 的返回類(lèi)型是 Stream<Person>。map() 方法接收一個(gè)類(lèi)型為 Function<T, R> 的函數(shù)式接口,這里 T 的類(lèi)型即是 Stream 元素的類(lèi)型,也就是 Person,而 R 的類(lèi)型未知。由于在重載解析之后 lambda 表達(dá)式的目標(biāo)類(lèi)型仍然未知,我們就需要推導(dǎo) R 的類(lèi)型:通過(guò)對(duì) lambda 表達(dá)式函數(shù)體進(jìn)行類(lèi)型檢查,我們發(fā)現(xiàn)函數(shù)體返回 String,因此 R 的類(lèi)型是 String,因而 map() 返回 Stream<String>。絕大多數(shù)情況下編譯器都能解析出正確的類(lèi)型,但如果碰到無(wú)法解析的情況,我們則需要:
- 使用顯式 lambda 表達(dá)式(為參數(shù) p 提供顯式類(lèi)型)以提供額外的類(lèi)型信息;
- 把 lambda 表達(dá)式轉(zhuǎn)型為 Function<Person, String>;
- 為泛型參數(shù) R 提供一個(gè)實(shí)際類(lèi)型。(Stream<String> names = ps.stream().<String>map(p -> p.getName()))
lambda 表達(dá)式本身也可以為它自己的函數(shù)體提供目標(biāo)類(lèi)型,也就是說(shuō) lambda 表達(dá)式可以通過(guò)外部目標(biāo)類(lèi)型推導(dǎo)出其內(nèi)部的返回類(lèi)型,這意味著我們可以方便的編寫(xiě)一個(gè)返回函數(shù)的函數(shù):
Supplier<Runnable> c = () -> () -> { System.out.println("hi"); };
類(lèi)似的,條件表達(dá)式可以把目標(biāo)類(lèi)型“分發(fā)”給其子表達(dá)式:
Callable<Integer> c = flag ? (() -> 23) : (() -> 42);
轉(zhuǎn)型表達(dá)式 (Cast expression) 可以顯式提供 lambda 表達(dá)式的類(lèi)型,這個(gè)特性在無(wú)法確認(rèn)目標(biāo)類(lèi)型時(shí)非常有用:
// 非法代碼
// Object o = () -> { System.out.println("hi"); };
// 有效代碼
Object o = (Runnable) () -> { System.out.println("hi"); };
六. 方法引用
lambda 表達(dá)式允許我們定義一個(gè)匿名方法,并允許我們以函數(shù)式接口的方式使用它。我們也希望能夠在已有的方法上實(shí)現(xiàn)同樣的特性。方法引用和 lambda 表達(dá)式擁有相同的特性,例如,它們都需要一個(gè)目標(biāo)類(lèi)型,并需要被轉(zhuǎn)化為函數(shù)式接口的實(shí)例。不過(guò)我們并不需要為方法引用提供方法體,我們可以直接通過(guò)方法名稱(chēng)引用已有方法。
以下面的代碼為例,假設(shè)我們要按照 name 或 age 為 Person 數(shù)組進(jìn)行排序:
class Person {
private final String name;
private final int age;
public int getAge() { return age; }
public String getName() {return name; }
...
}
Person[] people = ...
Comparator<Person> byName = Comparator.comparing(p -> p.getName());
Arrays.sort(people, byName);
這段代碼可以用方法引用代替 lambda 表達(dá)式:
Comparator<Person> byName = Comparator.comparing(Person::getName);
這里的 Person::getName 可以被看作為 lambda 表達(dá)式的簡(jiǎn)寫(xiě)形式。盡管方法引用不一定會(huì)把語(yǔ)法變的更緊湊,但它擁有更明確的語(yǔ)義:如果我們想要調(diào)用的方法擁有一個(gè)名字,我們就可以通過(guò)它的名字直接調(diào)用它。
因?yàn)楹瘮?shù)式接口的方法參數(shù)對(duì)應(yīng)于隱式方法調(diào)用時(shí)的參數(shù),所以被引用方法簽名可以通過(guò)放寬類(lèi)型,裝箱以及組織到參數(shù)數(shù)組中的方式對(duì)其參數(shù)進(jìn)行操作,就像在調(diào)用實(shí)際方法一樣:
// void exit(int status)
Consumer<Integer> b1 = System::exit;
// void sort(Object[] a)
Consumer<String[]> b2 = Arrays:sort;
// void main(String... args)
Consumer<String> b3 = MyProgram::main;
// void main(String... args)
Runnable r = Myprogram::mapToInt
七. 方法引用的種類(lèi)
方法引用有很多種,它們的語(yǔ)法如下:
- 靜態(tài)方法引用:ClassName::methodName
- 實(shí)例上的實(shí)例方法引用:instanceReference::methodName
- 超類(lèi)上的實(shí)例方法引用:super::methodName
- 類(lèi)型上的實(shí)例方法引用:ClassName::methodName
- 構(gòu)造方法引用:Class::new
- 數(shù)組構(gòu)造方法引用:TypeName[]::new
對(duì)于靜態(tài)方法引用,我們需要在類(lèi)名和方法名之間加入 "::" 分隔符,例如 Integer::sum。
對(duì)于具體對(duì)象上的實(shí)例方法引用,我們則需要在對(duì)象名和方法名之間加入分隔符:
Set<String> knownNames = ...
Predicate<String> isKnown = knownNames::contains;
這里的隱式 lambda 表達(dá)式會(huì)從 knownNames 中捕獲 String 對(duì)象,而它的方法體則會(huì)通過(guò) Set.contains 使用該 String 對(duì)象。有了實(shí)例方法引用,在不同函數(shù)式接口之間進(jìn)行類(lèi)型轉(zhuǎn)換就變的很方便:
Callable<Path> c = ...
Privileged<Path> a = c::call;
引用任意對(duì)象的實(shí)例方法,都需要在實(shí)例方法名稱(chēng)和其所屬類(lèi)型名稱(chēng)間加上分隔符:
Function<String, String> upperfier = String::toUpperCase;
如果類(lèi)型的實(shí)例方法是泛型的,那么我們就需要在 "::" 分隔符前提供類(lèi)型參數(shù),或者利用目標(biāo)類(lèi)型推導(dǎo)出其類(lèi)型。
需要注意的是,靜態(tài)方法引用和類(lèi)型上的實(shí)例方法引用擁有一樣的語(yǔ)法。編譯器會(huì)根據(jù)實(shí)際情況做出決定。一般我們不需要指定方法引用中的參數(shù)類(lèi)型,因?yàn)榫幾g器往往可以推導(dǎo)出結(jié)果,但如果需要我們也可以顯式在 :: 分隔符之前提供參數(shù)類(lèi)型信息。
和靜態(tài)方法引用類(lèi)似,構(gòu)造方法也可以通過(guò) new 關(guān)鍵字被直接引用:
SocketImplFactory factory = MySocketImpl::new;
如果類(lèi)型擁有多個(gè)構(gòu)造方法,那么我們就會(huì)通過(guò)目標(biāo)類(lèi)型的方法參數(shù)來(lái)選擇最佳匹配,這里的選擇過(guò)程和調(diào)用構(gòu)造方法時(shí)的選擇過(guò)程是一樣的。
如果待實(shí)例化的類(lèi)型是泛型的,那么我們可以在類(lèi)型名稱(chēng)之后提供類(lèi)型參數(shù),否則編譯器則會(huì)依照"菱形"構(gòu)造方法調(diào)用時(shí)的方式進(jìn)行推導(dǎo)。
數(shù)組的構(gòu)造方法引用的語(yǔ)法則比較特殊,為了便于理解,你可以假想存在一個(gè)接收int參數(shù)的數(shù)組構(gòu)造方法。參考下面的代碼:
IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 創(chuàng)建數(shù)組 int[10]