摘要:上下文比如,接受它傳遞的方法的參數,或者接受它的值得局部變量中表達式需要類型稱為目標類型。但局部變量必須顯示的聲明,或實際上就算。換句話說,表達式只能捕獲指派給它們的局部變量一次。注捕獲實例變量可以被看作捕獲最終局部變量。
由于第三章的內容比較多,而且為了讓大家更好的了解Lambda表達式的使用,也寫了一些相關的實例,可以在Github或者碼云上拉取讀書筆記的代碼進行參考。類型檢查、類型推斷以及限制
當我們第一次提到Lambda表達式時,說它可以為函數式接口生成一個實例。然而,Lambda表達式本身并不包含它在實現哪個函數式接口的信息。為了全面了解Lambda表達式,你應該知道Lambda的實際類型是什么。
類型檢查Lambda的類型是從使用Lambda上下文推斷出來的。上下文(比如,接受它傳遞的方法的參數,或者接受它的值得局部變量)中Lambda表達式需要類型稱為目標類型。
同樣的Lambda,不同的函數式接口有了目標類型的概念,同一個Lambda表達式就可以與不同的函數接口關聯起來,只要它們的抽象方法能夠兼容。比如,前面提到的Callable,這個接口代表著什么也不接受且返回一個泛型T的函數。
同一個Lambda可用于多個不同的函數式接口:
Comparatorc1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); ToIntBiFunction c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); BiFunction c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());;
是的,ToIntFunction和BiFunction都是屬于函數式接口。還有很多類似的函數式接口,有興趣的可以去看相關的源碼。
到目前為止,你應該能夠很好的理解在什么時候以及在哪里使用Lambda表達式了。它們可以從賦值的上下文、方法調用(參數和返回值),以及類型轉換的上下文中獲得目標類型。為了更好的了解Lambda表達的時候方式,我們來看看下面的例子,為什么不能編譯:
Object o = () -> {System.out.println("Tricky example");};
答案:很簡單,我們都知道Object這個類并不是一個函數式接口,所以它不支持這樣寫。為了解決這個問題,我們可以把Object改為Runnable,Runnable是一個函數式接口,因為它只有一個抽象方法,在上一節的讀書筆記中我們有提到過它。
Runnable r = () -> {System.out.println("Tricky example");};
你已經見過如何利用目標類型來檢查一個Lambda是否可以用于某個特定的上下文。其實,它也可以用來做一些略有不同的事情:tuiduanLambda參數的類型。
類型推斷我們還可以進一步的簡化代碼。Java編譯器會從上下文(目標類型)推斷出用什么函數式接口來匹配Lambda表達式,這意味著它也可以推斷出適合Lambda的簽名,因為函數描述符可以通過目標類型來得到。這樣做的好處在于,編譯器可以了解Lambda表達式的參數類型,這樣就可以在Lambda與法中省去標注參數類型。換句話說,Java編譯器會向下面這樣推斷Lambda的參數類型:
// 參數a沒有顯示的指定類型 ListgreenApples = filter(apples, a -> "green".equals(a.getColor()));
Lambda表達式有多個參數,代碼可獨行的好處就更為明顯。例如,你可以在這用來創建一個Comparator對象:
// 沒有類型推斷,顯示的指定了類型 ComparatorcApple1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); // 有類型推斷,沒有現實的指定類型 Comparator cApple2 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
有時候,指定類型的情況下代碼更易讀,有時候去掉它們也更易讀。并沒有說哪個就一定比哪個好,需要根據自身情況來選擇。
使用局部變量我們迄今為止所介紹的所有Lambda表達式都只用到了其主體里的參數。但Lambda表達式也允許用外部變量,就像匿名類一樣。他們被稱作捕獲Lambda。例如:下面的Lambda捕獲了portNumber變量:
int portNumber = 6666; Runnable r3 = () -> System.out.println(portNumber);
盡管如此,還有一點點小麻煩:關于能對這些變量做什么有一些限制。Lambda可以沒有限制地捕獲(也就是在主體中引用)實例變量和靜態變量。但局部變量必須顯示的聲明final,或實際上就算final。換句話說,Lambda表達式只能捕獲指派給它們的局部變量一次。(注:捕獲實例變量可以被看作捕獲最終局部變量this)。例如,下面的代碼無法編譯。
int portNumber = 6666; Runnable r3 = () -> System.out.println(portNumber); portNumber = 7777;
portNumber是一個final變量,盡管我們沒有顯示的去指定它。但是,在代碼編譯的時候,編譯器會自動給這個變量加了一個final,起碼我看反編譯后的代碼是有一個final的。
對于局部變量的限制
你可能會有一個疑問,為什么局部變量會有這些限制。第一個,實例變量和局部變量背后的實現有一個關鍵不同。實例變量都存儲在堆中,而局部變量則保存在棧上。如果Lambda可以直接訪問局部變量,而且Lambda是在一個線程中使用,則使用Lambda的線程,可能會在分配該變量的線程將這個變量回收之后,去訪問該變量。因此,Java在訪問自由局部變量是,實際上是在訪問它的副本,而不是訪問原始變量。如果局部變量僅僅復制一次那就沒什么區別了,因此就有了這個限制。
現在,我們來了解你會在Java8代碼中看到的另一個功能:方法引用。可以把它們視為某些Lambda的快捷方式。
方法引用方法引用讓你可以重復使用現有的方法,并像Lambda一樣傳遞它們。在一些情況下,比起用Lambda表達式還要易讀,感覺也更自然。下面就是我們借助Java8 API,用法引用寫的一個排序例子:
// 之前 apples.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())); // 之后,方法引用 apples.sort(Comparator.comparing(Apple::getWeight));
酷,使用::的代碼看起來更加簡潔。在此之前,我們也有使用到過,它的確看起來很簡潔。
管中窺豹方法引用可以被看作僅僅調用特定方法的Lambda的一種快捷寫法。它的基本思想是,如果一個Lambda代表的只是:“直接調用這個方法”,那最好還是用名稱來調用它,而不是去描述如何調用它。事實上,方法引用就是讓你根據已有的方法實現來創建Lambda表達式。但是,顯示地指明方法的名稱,你的代碼可讀性會更好。它是如何工作的?當你需要使用方法引用是,目標引用放在分隔符::前,方法的名稱放在后面。 例如,Apple::getWeight就是引用了Apple類中定義的getWeight方法。請記住,不需要括號,因為你沒有實際調用這個方法。方法引用就是用Lambda表達式(Apple a) -> a.getWeight()的快捷寫法。
我們接著來看看關于Lambda與方法引用等效的一些例子:
Lambda:(Apple a) -> a.getWeight() 方法引用:Apple::getWeight Lambda:() -> Thread.currentThread().dumpStack() 方法引用:Thread.currentThread()::dumpStack Lambda:(str, i) -> str.substring(i) 方法引用:String::substring Lambda:(String s) -> System.out.println(s) 方法引用:System.out::println
你可以把方法引用看作是Java8中個一個語法糖,因為它簡化了一部分代碼。
構造函數引用對于一個現有的構造函數,你可以利用它的名稱和關鍵字new來創建它的一個引用:ClassName::new。如果,一個構造函數沒有參數,那么可以使用Supplier來創建一個對象。你可以這樣做:
Supplierc1 = Apple::new; Apple apple = c1.get();
這樣做等價于
Supplierc1 = () -> new Apple(); Apple apple = c1.get();
如果,你的構造函數的簽名是Apple(Integer weight),那么可以使用Function接口的簽名,可以這樣寫:
Functionc2 = Apple::new; Apple a2 = c2.apply(120);
這樣做等價于
Functionc2 = (weight) -> new Apple(weight); Apple a2 = c2.apply(120);
如果有兩個參數Apple(weight, color),那么我們可以使用BiFunction:
BiFunctionc3 = Apple::new; Apple a3 = c3.apply(120, "red");
這樣做等價于
BiFunctionc3 =(weight, color) -> new Apple(weight, color); Apple a3 = c3.apply(120, "red");
到目前為止,我們了解到了很多新內容:Lambda、函數式接口和方法引用,接下來我們將把這一切付諸實踐。
Lambda和方法引用實戰為了更好的熟悉Lambda和方法引用的使用,我們繼續研究開始的那個問題,用不同的排序策略給一個Apple列表排序,并需要展示如何把一個圓使出報的解決方案變得更為簡明。這會用到我們目前了解到的所有概念和功能:行為參數化、匿名類、Lambda表達式和方法引用。我們想要實現的最終解決方案是這樣的:
apples.sort(comparing(Apple::getWeight));第1步:代碼傳遞
很幸運,Java8的Api已經提供了一個List可用的sort方法,我們可以不用自己再去實現它。那么最困難部分已經搞定了!但是,如果把排序策略傳遞給sort方法呢?你看,sort方法簽名是這樣的:
void sort(Comparator super E> c)
它需要一個Comparator對象來比較兩個Apple!這就是在Java中傳遞策略的方式:它們必須包裹在一個對象利。我們說sort的行為被參數化了了:傳遞給他的排序策略不同,其行為也會不同。
可能,你的第一個解決方案是這樣的:
public class AppleComparator implements Comparator{ @Override public int compare(Apple o1, Apple o2) { return o1.getWeight().compareTo(o2.getWeight()); } } apples.sort(new AppleComparator());
它確實能實現排序,但是還需要去實現一個接口,并且排序的規則也不復雜,或許它還可以簡化一下。
第2步:使用匿名類或許你已經想到了一個簡化代碼的辦法,就是使用匿名類而且每次使用只需要實例化一次就可以了:
apples.sort(new Comparator() { @Override public int compare(Apple o1, Apple o2) { return o1.getWeight().compareTo(o2.getWeight()); } });
看上去確實簡化一些,但感覺還是有些啰嗦,我們接著繼續簡化:
第3步:使用Lambda表達式我們可以使用Lambda表達式來替代匿名類,這樣可以提高代碼的簡潔性和開發效率:
apples.sort((o1, o2) -> o1.getWeight().compareTo(o2.getWeight()));
太棒了!這樣的代碼看起來很簡潔,原來四五行的代碼只需要一行就可以搞定了!但是,我們還可以使這行代碼更加的簡潔!
第4步:使用方法引用使用Lambda表達式的代碼確實簡潔了不少,那你還記得我們前面說的方法引用嗎?它是Lambda表達式的一種快捷寫法,相當于是一種語法糖,那么我們來試試糖的滋味如何:
apples.sort(Comparator.comparing(Apple::getWeight));
恭喜你,這就是你的最終解決方案!這樣的代碼比真的很簡潔,這比Java8之前的代碼好了很多。這樣的代碼比較簡短,它的意思也很明顯,并且代碼讀起來和問題描述的差不多:“對庫存進行排序,比較蘋果的重量”。
復合(組合)Lambda表達式的有用方法Java8的好幾個函數式接口都有為方便而設計的的方法。具體而言,許多函數式接口,比如用于傳遞Lambda表達式的Comparator、Function和Predicate都提供了允許你進行復合的方法。這是什么意思呢?在實踐中,這意味著你可以把多個簡單的Lambda復合成復雜的表達式。比如,你可以讓兩個謂詞之間做一個or操作,組合成一個更大的謂詞。而且,你還可以讓一個函數的結果成為另一個函數的輸入。你可能會想,函數式接口中怎么可能有更多的方法?(畢竟,這違背了函數式接口的定義,只能有一個抽象方法)還記得我們上一節筆記中提到默認方法嗎?它們不是抽象方法。關于默認方法,我們以后在進行詳細的了解吧。
比較復合器還記剛剛我們對蘋果的排序嗎?它只是一個從小到大的一個排序,現在我們需要讓它進行逆序。看看剛剛方法引用的代碼,你會發現它貌似無法進行逆序啊!不過不用擔心,我們可以讓它進行逆序,而且很簡單。
1.逆序
想要實現逆序其實很簡單,需要使用一個reversed()方法就可以完成我們想要的逆序排序:
apples.sort(Comparator.comparing(Apple::getWeight).reversed());
按重量遞減排序,就這樣完成了。這個方法很有用,而且用起來很簡單。
2.比較器鏈
上面的代碼很簡單,但是你仔細想想,如果存在兩個一樣重的蘋果誰前誰后呢?你可能需要再提供一個Comparator來進一步定義這個比較。比如,再按重量比較了兩個蘋果之后,你可能還想要按原產國進行排序。thenComparing方法就是做這個用的。它接受一個函數作為參數(就像comparing方法一樣),如果兩個對象用第一個Comparator比較之后還是一樣,就提供第二個Comparator。我們又可以優雅的解決這個問題了:
apples.sort(Comparator.comparing(Apple::getWeight).reversed() .thenComparing(Apple::getCountry));復合謂詞
謂詞接口包括了三個方法: negate、and和or,讓你可以重用已有的Predicate來創建更復雜的謂詞。比如,negate方法返回一個Predicate的非,比如蘋果不是紅的:
private staticList filter(List list, Predicate predicate) { List result = new ArrayList<>(); for (T t : list) { if (predicate.test(t)) { result.add(t); } } return result; } List apples = Arrays.asList(new Apple(150, "red"), new Apple(110, "green"), new Apple(100, "green")); // 只要紅蘋果 Predicate apple = a -> "red".equals(a.getColor()); // 只要紅蘋果的非 Predicate notRedApple = apple.negate(); // 篩選 List appleList = filter(apples, notRedApple); // 遍歷打印 appleList.forEach(System.out::println);
你可能還想要把Lambda用and方法組合起來,比如一個蘋果即是紅色的又比較重:
PredicateredAndHeavyApple = apple.and(a -> a.getWeight() >= 150);
你還可以進一步組合謂詞,表達要么是重的紅蘋果,要么是綠蘋果:
PredicateredAndHeavyAppleOrGreen = apple.and(a -> a.getWeight() >= 150) .or(a -> "green".equals(a.getColor()));
這一點為什么很好呢?從簡單的Lambda表達式出發,你可以構建更復雜的表達式,但讀起來仍然和問題陳述的差不多!請注意,and和or方法是按照表達式鏈中的位置,從左向右確定優先級的。因此,a.or(b).and(c)可以看作(a || b) && c。
函數復合最后,你還可以把Function接口所代表的Lambda表達式復合起來。Function接口為此匹配了andThen和compose兩個默認方法,它們都會返回Function的一個實例。
andThen方法會返回一個函數,它先對輸入應用一個給定函數,再對輸出應用另一個函數。假設,有一個函數f給數字加1(x -> x + 1),另外一個函數g給數字乘2,你可以將它們組合成一個函數h:
Functionf = x -> x + 1; Function g = x -> x * 2; Function h = f.andThen(g); // result = 4 int result = h.apply(1);
你也可以類似地使用compose方法,先把給定的函數左右compose的參數里面給的那個函數,然后再把函數本身用于結果。比如在上一個例子用compose的化,它將意味著f(g(x)),而andThen則意味著g(f(x)):
Functionf1 = x -> x + 1; Function g1 = x -> x * 2; Function h1 = f1.compose(g1); // result1 = 3 int result1 = h1.apply(1);
它們的關系如下圖所示:
compose和andThen的不同之處就是函數執行的順序不同。compose函數限制先參數,然后執行調用者,而andThen限制先調用者,然后再執行參數。
總結在《Java8實戰》第三章中,我們了解到了很多概念關鍵的念。
Lambda表達式可以理解為一種匿名函數:它沒有名稱,但有參數列表、函數主體、返回類型,可能還有一個可拋出的異常列表。
Lambda表達式讓我們可以簡潔的傳遞代碼。
函數式接口就是僅僅只有一個抽象方法的接口。
只有在接受函數式接口的地方才可以使用Lambda表達式。
Lambda表達式允許你直接內聯,為函數式接口的抽象方法提供實現,并且將整個表達式作為函數式接口的一個實例。
Java8自帶一些常用的函數式接口,在java.util.function包里,包括了Predicate
為了避免裝箱操作,等于Predicate
Lambda表達式所需要代表的類型稱為目標類型。
方法引用可以讓我們重復使用現有的方法實現并且直接傳遞它們。
Comparator、Predicate和Function等函數式接口都有幾個可以用來結合Lambda表達式的默認方法。
第三章的內容確實很多,而且這一章的內容也很重要,如果你有興趣那么請慢慢的看,最好自己能動手寫寫代碼否則過不了多久就會忘記了。
第三章筆記中的代碼:Github: chap3
Gitee: chap3
如果,你對Java8中的新特性很感興趣,你可以關注我的公眾號或者當前的技術社區的賬號,利用空閑的時間看看我的文章,非常感謝!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76774.html
摘要:之前,使用匿名類給蘋果排序的代碼是的,這段代碼看上去并不是那么的清晰明了,使用表達式改進后或者是不得不承認,代碼看起來跟清晰了。這是由泛型接口內部實現方式造成的。 # Lambda表達式在《Java8實戰》中第三章主要講的是Lambda表達式,在上一章節的筆記中我們利用了行為參數化來因對不斷變化的需求,最后我們也使用到了Lambda,通過表達式為我們簡化了很多代碼從而極大地提高了我們的...
摘要:實戰讀書筆記第一章從方法傳遞到接著上次的,繼續來了解一下,如果繼續簡化代碼。去掉并且生成的數字是萬,所消耗的時間循序流并行流至于為什么有時候并行流效率比循序流還低,這個以后的文章會解釋。 《Java8實戰》-讀書筆記第一章(02) 從方法傳遞到Lambda 接著上次的Predicate,繼續來了解一下,如果繼續簡化代碼。 把方法作為值來傳遞雖然很有用,但是要是有很多類似與isHeavy...
摘要:第三個問題查找所有來自于劍橋的交易員,并按姓名排序。第六個問題打印生活在劍橋的交易員的所有交易額。第八個問題找到交易額最小的交易。 付諸實戰 在本節中,我們會將迄今學到的關于流的知識付諸實踐。我們來看一個不同的領域:執行交易的交易員。你的經理讓你為八個查詢找到答案。 找出2011年發生的所有交易,并按交易額排序(從低到高)。 交易員都在哪些不同的城市工作過? 查找所有來自于劍橋的交易...
摘要:依舊使用剛剛對蘋果排序的代碼。現在,要做的是篩選出所有的綠蘋果,也許你會這一個這樣的方法在之前,基本上都是這樣寫的,看起來也沒什么毛病。但是,現在又要篩選一下重量超過克的蘋果。 《Java8實戰》-讀書筆記第一章(01) 最近一直想寫點什么東西,卻不知該怎么寫,所以就寫寫關于看《Java8實戰》的筆記吧。 第一章內容較多,因此打算分幾篇文章來寫。 為什么要關心Java8 自1996年J...
摘要:但是到了第二天,他突然告訴你其實我還想找出所有重量超過克的蘋果。現在,農民要求需要篩選紅蘋果。那么,我們就可以根據條件創建一個類并且實現通過謂詞篩選紅蘋果并且是重蘋果酷,現在方法的行為已經取決于通過對象來實現了。 通過行為參數化傳遞代碼 行為參數化 在《Java8實戰》第二章主要介紹的是通過行為參數化傳遞代碼,那么就來了解一下什么是行為參數化吧。 在軟件工程中,一個從所周知的問題就是,...
閱讀 1772·2023-04-25 21:50
閱讀 2419·2019-08-30 15:53
閱讀 767·2019-08-30 13:19
閱讀 2742·2019-08-28 17:58
閱讀 2464·2019-08-23 16:21
閱讀 2700·2019-08-23 14:08
閱讀 1374·2019-08-23 11:32
閱讀 1439·2019-08-22 16:09