因爲第三章的內容比較多,並且爲了讓你們更好的瞭解Lambda表達式的使用,也寫了一些相關的實例,能夠在Github或者碼雲上拉取讀書筆記的代碼進行參考。java
當咱們第一次提到Lambda表達式時,說它能夠爲函數式接口生成一個實例。然而,Lambda表達式自己並不包含它在實現哪一個函數式接口的信息。爲了全面瞭解Lambda表達式,你應該知道Lambda的實際類型是什麼。git
Lambda的類型是從使用Lambda上下文推斷出來的。上下文(好比,接受它傳遞的方法的參數,或者接受它的值得局部變量)中Lambda表達式須要類型稱爲目標類型。github
有了目標類型的概念,同一個Lambda表達式就能夠與不一樣的函數接口關聯起來,只要它們的抽象方法可以兼容。好比,前面提到的Callable,這個接口表明着什麼也不接受且返回一個泛型T的函數。bash
同一個Lambda可用於多個不一樣的函數式接口:app
Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());;
複製代碼
是的,ToIntFunction和BiFunction都是屬於函數式接口。還有不少相似的函數式接口,有興趣的能夠去看相關的源碼。ide
到目前爲止,你應該可以很好的理解在何時以及在哪裏使用Lambda表達式了。它們能夠從賦值的上下文、方法調用(參數和返回值),以及類型轉換的上下文中得到目標類型。爲了更好的瞭解Lambda表達的時候方式,咱們來看看下面的例子,爲何不能編譯:函數
Object o = () -> {System.out.println("Tricky example");};
複製代碼
答案:很簡單,咱們都知道Object這個類並非一個函數式接口,因此它不支持這樣寫。爲了解決這個問題,咱們能夠把Object改成Runnable,Runnable是一個函數式接口,由於它只有一個抽象方法,在上一節的讀書筆記中咱們有提到過它。ui
Runnable r = () -> {System.out.println("Tricky example");};
複製代碼
你已經見過如何利用目標類型來檢查一個Lambda是否能夠用於某個特定的上下文。其實,它也能夠用來作一些略有不一樣的事情:tuiduanLambda參數的類型。this
咱們還能夠進一步的簡化代碼。Java編譯器會從上下文(目標類型)推斷出用什麼函數式接口來匹配Lambda表達式,這意味着它也能夠推斷出適合Lambda的簽名,由於函數描述符能夠經過目標類型來獲得。這樣作的好處在於,編譯器能夠了解Lambda表達式的參數類型,這樣就能夠在Lambda與法中省去標註參數類型。換句話說,Java編譯器會向下面這樣推斷Lambda的參數類型:spa
// 參數a沒有顯示的指定類型
List<Apple> greenApples = filter(apples, a -> "green".equals(a.getColor()));
複製代碼
Lambda表達式有多個參數,代碼可獨行的好處就更爲明顯。例如,你能夠在這用來建立一個Comparator對象:
// 沒有類型推斷,顯示的指定了類型
Comparator<Apple> cApple1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 有類型推斷,沒有現實的指定類型
Comparator<Apple> 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來建立一個對象。你能夠這樣作:
Supplier<Apple> c1 = Apple::new;
Apple apple = c1.get();
複製代碼
這樣作等價於
Supplier<Apple> c1 = () -> new Apple();
Apple apple = c1.get();
複製代碼
若是,你的構造函數的簽名是Apple(Integer weight),那麼可使用Function接口的簽名,能夠這樣寫:
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(120);
複製代碼
這樣作等價於
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(120);
複製代碼
若是有兩個參數Apple(weight, color),那麼咱們可使用BiFunction:
BiFunction<Integer, String, Apple> c3 = Apple::new;
Apple a3 = c3.apply(120, "red");
複製代碼
這樣作等價於
BiFunction<Integer, String, Apple> c3 =(weight, color) -> new Apple(weight, color);
Apple a3 = c3.apply(120, "red");
複製代碼
到目前爲止,咱們瞭解到了不少新內容:Lambda、函數式接口和方法引用,接下來咱們將把這一切付諸實踐。
爲了更好的熟悉Lambda和方法引用的使用,咱們繼續研究開始的那個問題,用不一樣的排序策略給一個Apple列表排序,並須要展現如何把一個圓使出報的解決方案變得更爲簡明。這會用到咱們目前瞭解到的全部概念和功能:行爲參數化、匿名類、Lambda表達式和方法引用。咱們想要實現的最終解決方案是這樣的:
apples.sort(comparing(Apple::getWeight));
複製代碼
很幸運,Java8的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());
複製代碼
它確實能實現排序,可是還須要去實現一個接口,而且排序的規則也不復雜,或許它還能夠簡化一下。
或許你已經想到了一個簡化代碼的辦法,就是使用匿名類並且每次使用只須要實例化一次就能夠了:
apples.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});
複製代碼
看上去確實簡化一些,但感受仍是有些囉嗦,咱們接着繼續簡化:
咱們可使用Lambda表達式來替代匿名類,這樣能夠提升代碼的簡潔性和開發效率:
apples.sort((o1, o2) -> o1.getWeight().compareTo(o2.getWeight()));
複製代碼
太棒了!這樣的代碼看起來很簡潔,原來四五行的代碼只須要一行就能夠搞定了!可是,咱們還可使這行代碼更加的簡潔!
使用Lambda表達式的代碼確實簡潔了很多,那你還記得咱們前面說的方法引用嗎?它是Lambda表達式的一種快捷寫法,至關因而一種語法糖,那麼咱們來試試糖的滋味如何:
apples.sort(Comparator.comparing(Apple::getWeight));
複製代碼
恭喜你,這就是你的最終解決方案!這樣的代碼比真的很簡潔,這比Java8以前的代碼好了不少。這樣的代碼比較簡短,它的意思也很明顯,而且代碼讀起來和問題描述的差很少:「對庫存進行排序,比較蘋果的重量」。
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 static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T t : list) {
if (predicate.test(t)) {
result.add(t);
}
}
return result;
}
List<Apple> apples = Arrays.asList(new Apple(150, "red"), new Apple(110, "green"), new Apple(100, "green"));
// 只要紅蘋果
Predicate<Apple> apple = a -> "red".equals(a.getColor());
// 只要紅蘋果的非
Predicate<Apple> notRedApple = apple.negate();
// 篩選
List<Apple> appleList = filter(apples, notRedApple);
// 遍歷打印
appleList.forEach(System.out::println);
複製代碼
你可能還想要把Lambda用and方法組合起來,好比一個蘋果便是紅色的又比較重:
Predicate<Apple> redAndHeavyApple = apple.and(a -> a.getWeight() >= 150);
複製代碼
你還能夠進一步組合謂詞,表達要麼是重的紅蘋果,要麼是綠蘋果:
Predicate<Apple> redAndHeavyAppleOrGreen =
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:
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
// result = 4
int result = h.apply(1);
複製代碼
你也能夠相似地使用compose方法,先把給定的函數左右compose的參數裏面給的那個函數,而後再把函數自己用於結果。好比在上一個例子用compose的化,它將意味着f(g(x)),而andThen則意味着g(f(x)):
Function<Integer, Integer> f1 = x -> x + 1;
Function<Integer, Integer> g1 = x -> x * 2;
Function<Integer, Integer> h1 = f1.compose(g1);
// result1 = 3
int result1 = h1.apply(1);
複製代碼
它們的關係以下圖所示:
compose和andThen的不一樣之處就是函數執行的順序不一樣。compose函數限制先參數,而後執行調用者,而andThen限制先調用者,而後再執行參數。
在《Java8實戰》第三章中,咱們瞭解到了不少概念關鍵的念。
第三章的內容確實不少,並且這一章的內容也很重要,若是你有興趣那麼請慢慢的看,最好本身能動手寫寫代碼不然過不了多久就會忘記了。
Github: chap3
Gitee: chap3
若是,你對Java8中的新特性很感興趣,你能夠關注個人公衆號或者當前的技術社區的帳號,利用空閒的時間看看個人文章,很是感謝!