Lambda表達(dá)式
利用行為參數(shù)化這個(gè)概念,就可以編寫(xiě)更為靈活且可重復(fù)使用的代碼衔蹲。但同時(shí)肢娘,使用匿名類(lèi)來(lái)表示不同的行為并不令人滿(mǎn)意。Java 8引入了Lambda表達(dá)式來(lái)解決這個(gè)問(wèn)題舆驶。它使你以一種很簡(jiǎn)潔的表示一個(gè)行為或傳遞代碼橱健。
可以將Lambda表達(dá)式理解為簡(jiǎn)潔地表示可傳遞的匿名函數(shù)的一種方式:它沒(méi)有名稱(chēng),但它有參數(shù)列表沙廉、函數(shù)主體拘荡、返回類(lèi)型,可能還有一個(gè)可以?huà)伋龅漠惓A斜怼?/p>
- 匿名 - 因?yàn)樗幌衿胀ǖ姆椒ㄒ粯佑幸粋€(gè)明確的名稱(chēng)撬陵。
- 函數(shù) - 說(shuō)它是函數(shù)是因?yàn)長(zhǎng)ambda函數(shù)不像方法那樣屬于某個(gè)特定的類(lèi)珊皿,但和方法要一樣,Lambda有參數(shù)列表巨税、函數(shù)主體蟋定、返回類(lèi)型,還可能有可以?huà)伋龅漠惓A斜怼?/li>
- 傳遞 - Lambda表達(dá)式可以作為參數(shù)傳遞給方法或存儲(chǔ)在變量中草添。
- 簡(jiǎn)潔 - 無(wú)需像匿名類(lèi)那樣寫(xiě)很多模板代碼
使用Lambda的最終結(jié)果就是你的代碼變得更清晰驶兜、靈活。打比方远寸,利用Lambda表達(dá)式促王,可以更為簡(jiǎn)潔地自定義一個(gè)Comparator對(duì)象。
對(duì)比以下兩段代碼:
Comparator <Apple> byWeight = new Comparator<Apple>(){
public int compare(Apple a1,Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
};
(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
代碼看起來(lái)更清晰了≡某耄基本上只傳遞了真正需要的代碼(compare方法的主體)倡怎。
Lambda表達(dá)式由三個(gè)部分,如圖 Lambda表達(dá)式由參數(shù)、箭頭和主體組成 所示
- 參數(shù)列表 - 這里它采用了Comparator中compare方法的參數(shù)监署,兩個(gè)Apple颤专。
- 箭頭 - 箭頭
->
把參數(shù)列表與Lambda主體分隔開(kāi)。 - Lambda主體 - 比較兩個(gè)Apple的重量钠乏。表達(dá)式就是Lambda的返回值栖秕。
Lambda 的基本語(yǔ)法是:
(parameters) -> expression
或 (parameters) -> { statements; }
函數(shù)式接口
之前的Predicate<T>就是一個(gè)函數(shù)式接口,因?yàn)镻redicate僅僅定義了一個(gè)抽象方法:
public interface Predicate<T>{
boolean test(T t);
}
簡(jiǎn)而言之晓避,函數(shù)式接口就是只定義一個(gè)抽象方法的接口簇捍。Lambda表達(dá)式允許你直接以?xún)?nèi)聯(lián)的形式為函數(shù)式接口的抽象方法提供實(shí)現(xiàn),并把整個(gè)表達(dá)式作為函數(shù)式接口的實(shí)例俏拱。
函數(shù)式接口的抽象方法的簽名
把Lambda付諸實(shí)踐:環(huán)繞執(zhí)行模式
要從一個(gè)文件中讀取一行所需的模板代碼:
public static String processFile() throws IOException{
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))){
return br.readLine();
}
}
現(xiàn)在這段代碼的局限性在于只能讀文件的第一行暑塑,如果想要返回頭兩行,甚至是返回使用最頻繁的詞锅必。這時(shí)需要把processFile的行為參數(shù)化事格。
需要一個(gè)接收BufferedReader并返回String的Lambda。下面是從BufferedReader中打印兩行的寫(xiě)法:
String result = processFile(BufferedReader br) -> br.readLine() + br.readLine());
現(xiàn)在需要?jiǎng)?chuàng)建一個(gè)能匹配BufferedReader -> String搞隐,還可以?huà)伋鯥OException異常的接口:
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
@FunctionalInterface 是什么驹愚?
這個(gè)標(biāo)注用于表示該接口會(huì)設(shè)計(jì)成一個(gè)函數(shù)式接口。如果用@FunctionalInterface
定義了一個(gè)接口劣纲,而它卻不是函數(shù)式接口的話(huà)逢捺,編譯器將返回一個(gè)提示原因的錯(cuò)誤。
使用它不是必須的味廊,但是使用它是比較好的做法蒸甜。
現(xiàn)在就可以把這個(gè)接口作為新的processFile方法的參數(shù),在方法主體內(nèi)余佛,對(duì)得到BufferedReaderProcessor對(duì)象調(diào)用process方法執(zhí)行處理:
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))){
return p.process(br);
}
}
現(xiàn)在可以通過(guò)傳遞不同的Lambda重用processFile方法柠新,并以不用的方式處理文件了。
處理一行:
String oneLine =
processFile((BufferedReader br) -> br.readLine());
處理兩行:
String twoLines =
processFile((BufferedReader br) -> br.readLine() + br.readLine());
完整代碼如下:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class test{
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader("c:/tmp/data.txt"))){
return p.process(br);
}
}
public static void main(String[] args) throws IOException {
String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
System.out.println(oneLine);
System.out.println(twoLines);
}
}
現(xiàn)在已經(jīng)能成功的利用函數(shù)式接口來(lái)傳遞Lambda辉巡。
使用函數(shù)式接口
函數(shù)接口定義且只定義了一個(gè)抽象方法恨憎。函數(shù)式接口的抽象方法的簽名稱(chēng)為函數(shù)描述符。因此郊楣,為了應(yīng)用不同的Lambda表達(dá)式憔恳,需要一套能夠描述常見(jiàn)函數(shù)描述符的函數(shù)式接口。
Java 8在java.util.function包中加入了許多新的函數(shù)式接口净蚤,你可以重用它來(lái)傳遞多個(gè)不同的Lambda钥组。
Predicate
java.util.function.Predicate<T>接口定義了一個(gè)名叫test的抽象方法,它接受泛型T對(duì)象今瀑,并返回一個(gè)boolean程梦。在需要表示一個(gè)涉及類(lèi)型T的布爾表達(dá)式時(shí)点把,就可以使用這個(gè)接口。
public interface Predicate<T> {
boolean test(T t);
}
例如:
@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>定義了一個(gè)名叫accept的抽象方法屿附,它接受泛型T對(duì)象郎逃,沒(méi)有返回。
例如:
@FunctionalInterface
public interface Comsumer<T> {
void accept(T t);
}
public static <T> void forEach(List<T> list, Predicate<T> c){
for (T i: list){
c.accept(i);
}
}
forEach(
Arrays.asList(1,2,3,4,5),
(Interger i) -> System.ou.println(i)
);
Function
java.util.function.Function<T, R>定義了一個(gè)名叫apply的抽象方法挺份,它接受一個(gè)泛型T對(duì)象褒翰,并返回一個(gè)泛型R的對(duì)象。
下面將創(chuàng)建一個(gè)map方法匀泊,以將一個(gè)String列表映射到包含每個(gè)String長(zhǎng)度的Interger列表:
@FunctionalInterface
public interface Function<T, R> {
R accept(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;
}
List<Integer> l =map(Arrays.asList("lambdas","in","action"), (String s) -> s.length());
原始類(lèi)型特化
Java的類(lèi)型有兩種: 引用類(lèi)型 和 原始類(lèi)型 优训。但是泛型只能綁定到引用類(lèi)型。這是由于泛型內(nèi)部的實(shí)現(xiàn)方式造成的探赫。因此Java里有一個(gè)將原始類(lèi)型轉(zhuǎn)換為對(duì)應(yīng)的引用類(lèi)型的機(jī)制型宙。這個(gè)機(jī)制叫裝箱(boxing)。相反的操作便叫拆箱(unboxing)伦吠。裝箱和拆箱操作是可以由自動(dòng)裝箱機(jī)制來(lái)自動(dòng)完成的妆兑。但是這在性能方面要付出代價(jià),裝箱后的值需要更多的內(nèi)存并且需要額外的內(nèi)存毛仪。
Java 8為原始類(lèi)型帶來(lái)了一個(gè)專(zhuān)門(mén)的版本搁嗓,用于在輸入和輸出都是原始類(lèi)型時(shí)避免自動(dòng)裝箱的操作:
public interface IntPredicate{
boolean test(int t);
}
無(wú)裝箱:
IntPredicate evenNumbers = (int i) -> i%2 ==0;
evenNumbers.test(1000);
裝箱:
Predicate<Integer> oddNumbers = (Integer i) -> i%2 == 1;
oddNumbers.test(1000);
類(lèi)型檢查、類(lèi)型推斷以及限制
Lambda本身并不包含它在實(shí)現(xiàn)哪個(gè)函數(shù)式接口的信息箱靴。為了全面了解Lambda表達(dá)式應(yīng)該知道Lambda的實(shí)際類(lèi)型是什么腺逛。
類(lèi)型檢查
Lambda的類(lèi)型是從使用Lambda的上下文推斷出來(lái)的。上下文(例如接受它傳遞的方法的參數(shù)衡怀,或接受它的值的局部變量)中Lambda表達(dá)式需要的類(lèi)型稱(chēng)為目標(biāo)類(lèi)型棍矛。
同樣的Lambda,不同的函數(shù)式接口
有了目標(biāo)類(lèi)型的概念抛杨,同一個(gè)Lambda表達(dá)式就可以與不同的函數(shù)式接口聯(lián)系起來(lái)够委。
類(lèi)型推斷
可以進(jìn)一步簡(jiǎn)化你的代碼。編譯器會(huì)從上下文(目標(biāo)類(lèi)型)推斷出痛什么函數(shù)式接口來(lái)配合Lambda表達(dá)式怖现,這意味著它也可以推斷出適合Lambda的簽名茁帽,這樣就可以再Lambda語(yǔ)法中省去標(biāo)注參數(shù)類(lèi)型。
沒(méi)有自動(dòng)類(lèi)型推斷:
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
有自動(dòng)類(lèi)型推斷:
Comparator<Apple> c =(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
使用局部變量
之前介紹的所有Lambda表達(dá)式都只用到了其主體里面的參數(shù)屈嗤。但Lambda表達(dá)式也允許使用自由變量(不是參數(shù)潘拨,而是在外層作用域中定義的變量),就像匿名類(lèi)一樣饶号。它們被稱(chēng)作捕獲Lambda铁追。
例如:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
Lambda捕獲了portNumber變量。但關(guān)于對(duì)這些變量可以做什么有一些限制茫船。Lambda可以沒(méi)有限制的捕獲實(shí)例變量和靜態(tài)變量(也就是在其主體中引用)脂信。但是局部變量必須顯示聲明為final(或事實(shí)上是final)癣蟋。
下面的代碼無(wú)法編譯,因?yàn)閜ortNumber變量被賦值了兩次:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;
實(shí)例變量和局部變量背后的實(shí)現(xiàn)有一個(gè)關(guān)鍵的不同狰闪。實(shí)例變量都存儲(chǔ)在堆中,而局部變量則保存在棧上濒生。Java在訪(fǎng)問(wèn)自由局部變量時(shí)埋泵,實(shí)際上是在訪(fǎng)問(wèn)它的副本,而不是訪(fǎng)問(wèn)原始變量罪治。如果Lambda可以直接訪(fǎng)問(wèn)局部變量丽声,而且Lambda是在一個(gè)線(xiàn)程中使用的,則使用Lambda的線(xiàn)程觉义,可能會(huì)在分配該變量的線(xiàn)程將這個(gè)變量收回之后去訪(fǎng)問(wèn)該變量雁社,這回引發(fā)造成線(xiàn)程不安全的新的可能性。
方法引用
方法引用可以重復(fù)使用現(xiàn)有的方法定義晒骇,并像Lambda一樣傳遞它們霉撵。
下面是用方法引用寫(xiě)的一個(gè)排序的例子:
先前:
invenstory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
之后(使用方法引用和 java.util.Comparator.comparing
):
inventory.sort(comparing(Apple::getWeight));
方法引用可以被看做僅僅調(diào)用特定方法的Lambda的一種快捷寫(xiě)法。它的基本思想是洪囤,如果一個(gè)Lambda代表的只是“直接調(diào)用這個(gè)方法”徒坡,那最好還是用名稱(chēng)來(lái)調(diào)用它,而不是去描述如何調(diào)用它瘤缩。
方法引用就是讓你根據(jù)已有的方法實(shí)現(xiàn)來(lái)創(chuàng)建Lambda表達(dá)式喇完。
當(dāng)你需要使用方法引用時(shí),目標(biāo)引用放在分隔符 ::
前剥啤,方法的名稱(chēng)放在后面锦溪。例如 Apple::getWeight
就是引用了Apple類(lèi)中定義的方法getWeight。
方法引用也可以看做針對(duì)僅僅設(shè)計(jì)單一方法的Lambda的語(yǔ)法糖府怯。
構(gòu)建方法引用
方法引用主要有三類(lèi)刻诊。
- 指向靜態(tài)方法的方法引用(Integer::parseInt)
- 指向任意類(lèi)型實(shí)例方法的方法引用(String::length)
- 指向現(xiàn)有對(duì)象的實(shí)例方法的方法引用(Transaction::getValue)
第二種方法引用的思想就是你在引用一個(gè)對(duì)象的方法,而這個(gè)對(duì)象本身是Lambda的一個(gè)參數(shù)富腊。
例如 (String s) -> s.toUpperCase()
可以寫(xiě)作 String::toUpperCase
坏逢。
第三種方法引用指的是,你在Lambda中調(diào)用一個(gè)已經(jīng)存在的外部對(duì)象的方法赘被。例如是整,Lambda表達(dá)式 () -> expenssiveTransaction.getValue()
可以寫(xiě)作 expensiveTransaction::getValue
。
方法引用不需要括號(hào)民假,是因?yàn)闆](méi)有實(shí)際調(diào)用這個(gè)方法浮入。
構(gòu)造函數(shù)引用
可以利用現(xiàn)有構(gòu)造函數(shù)的名稱(chēng)和關(guān)鍵字來(lái)創(chuàng)建它的一個(gè)引用 ClassName:new
例如:
List<Integer> weight = Arrays.asList(7,3,4,10);
List<Apple> apples = map(weights, Apple::new);
public static List<Apple> map(List<Integer> List, Function<Integer, Apple> f){
List<Apple> result = new ArrayList<>();
for(Integer e: list){
result.add(f.apply(e))
}
return result;
}
Lambda和方法引用實(shí)戰(zhàn)(用不同的排序策略給一個(gè)Apple列表排序)
第1步:傳遞代碼
Java API已經(jīng)提供了一個(gè)List可用的sort方法,要如何把排序策略傳遞給sort呢羊异?sort方法的簽名樣子如下:
void sort(Comparator<? super E> c)
它需要一個(gè)Comparator對(duì)象來(lái)比較兩個(gè)Apple事秀。
第一個(gè)解決方案看上去是這樣的:
public class AppleComparator implements Comparator<Apple>{
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
第2步:使用匿名類(lèi)改進(jìn)
inventory.sort(new AppleComparator<Apple>(){
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
使用匿名類(lèi)的意義僅僅在于不用為了只實(shí)例化一次而實(shí)現(xiàn)一個(gè)Comparator彤断。
第3步:使用Lambda表達(dá)式
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
或
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
Comparator有一個(gè)叫做comparing的靜態(tài)輔助方法,它可以接受一個(gè)Function來(lái)提取Comparable鍵值易迹,并生成一個(gè)Comparator對(duì)象:
import static java.util.Comparator.comparing;
inventory.sort(comparing((a) -> a.getWeight()));
第4步:使用方法引用
inventory(comparing(Apple::getWeight));
復(fù)合Lambda表達(dá)式的有用方法
比較器復(fù)合
對(duì)于:
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
1.逆序
想要按重量遞減排序怎么辦宰衙?不需要建立一個(gè)新的Comparator的實(shí)例。接口有個(gè)默認(rèn)方法 reverse
可以使給定的比較器逆序睹欲。因此仍然可以用之前的那個(gè)比較器:
inventory.sort(comparing(Apple::getWeight).reversed());
2.比較器鏈
如果兩個(gè)蘋(píng)果一樣重怎么辦窘疮,哪個(gè)蘋(píng)果應(yīng)該排在前面闸衫?這時(shí)候可能需要再提供一個(gè)Comparator來(lái)進(jìn)一步比較弟翘。
thenComparing
就是做這個(gè)用的衅胀。它接受一個(gè)函數(shù)作為參數(shù)(與comparing方法一樣)滚躯,如果兩個(gè)對(duì)象用第一個(gè)Comparator比較之后是一樣的掸掏,就提供第二個(gè)Comparator:
inventory.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));
謂詞復(fù)合
謂詞接口包括三個(gè)方法:negate、and 和 or愿待。
- negate
可以使用negate方法來(lái)返回一個(gè)Predicate的非,比如蘋(píng)果不是紅色:
Predicate<Apple> notRedApple = redApple.negate()
- and
可以用and方法將兩個(gè)Lambda組合起來(lái):
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
- or
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150).or(a -> "green".equals(a.getcolor()));
函數(shù)復(fù)合
還可以把Function接口所代表的Lambda表達(dá)式復(fù)合起來(lái)。Function接口有兩個(gè)默認(rèn)方法:andThen和 compose砸紊。它們都會(huì)返回Function的一個(gè)實(shí)例沼溜。
- andThen 方法會(huì)返回一個(gè)函數(shù),它先對(duì)輸入應(yīng)用一個(gè)給定函數(shù),再對(duì)輸出應(yīng)用另一個(gè)函數(shù)。
比如函數(shù)f給數(shù)字加1啰扛,另一個(gè)函數(shù)給數(shù)字乘2:
Function<Integer,Integer> f = x -> x + 1;
Function<Integer,Integer> g = x -> x * 2;
Function<Integer,Integer> h = f.andThen(g);
int result = h.apply(1);
在數(shù)學(xué)上意味著g(f(x))。
- compose 方法先把給定的函數(shù)用作compose的參數(shù)里面給的那個(gè)函數(shù),然后再把函數(shù)本身用于結(jié)果续徽。
Function<Integer,Integer> f = x -> x + 1;
Function<Integer,Integer> g = x -> x * 2;
Function<Integer,Integer> h = f.compose(g);
int result = h.apply(1);
在數(shù)學(xué)上意味著f(g(x))。