使用詞法作用域和閉包
很多開發(fā)人員都存在這種誤解,認(rèn)為使用lambda表達(dá)式會(huì)導(dǎo)致代碼冗余,降低代碼質(zhì)量。恰恰相反,就算代碼變得再復(fù)雜,我們也不會(huì)為了代碼的簡(jiǎn)潔性而在代碼質(zhì)量上做任何妥協(xié),下面我們就會(huì)看到。
在前面一個(gè)例子中我們已經(jīng)可以重用lambda表達(dá)式了;然而,如果再匹配另外一個(gè)字母,代碼冗余的問題很快又卷土重來了。我們先來進(jìn)一步分析下這個(gè)問題,然后再用詞法作用域和閉包來把它解決掉。
lambda表達(dá)式帶來的冗余
我們來從friends中過濾出那些以N或者B開頭的字母。繼續(xù)延用上面的那個(gè)例子,我們寫出的代碼可能會(huì)是這樣的:
final Predicate<String> startsWithN = name -> name.startsWith("N");
final Predicate<String> startsWithB = name -> name.startsWith("B");
final long countFriendsStartN =
friends.stream()
.filter(startsWithN).count();
final long countFriendsStartB =
friends.stream()
.filter(startsWithB).count();
第一個(gè)predicate判斷名字是否是以N開頭的,而第二個(gè)是判斷是否以B開頭的。我們把這兩個(gè)實(shí)例分別傳遞給兩次filter方法調(diào)用。這樣看起來很合理,但是兩個(gè)predicate產(chǎn)生了冗余,它們只是那個(gè)檢查的字母不同而已。我們來看下如何能避免這種冗余。
使用詞法作用域來避免冗余
第一個(gè)方案,我們可以把字母抽出來作為函數(shù)的參數(shù),同時(shí)把這個(gè)函數(shù)傳遞給filter方法。這是個(gè)不錯(cuò)的方法,不過filter可不是什么函數(shù)都接受的。它只接受只有一個(gè)參數(shù)的函數(shù),那個(gè)參數(shù)對(duì)應(yīng)的就是集合中的元素,返回一個(gè)boolean值,它希望傳進(jìn)來的是一個(gè)Predicate。
我們希望有一個(gè)地方能把這個(gè)字母先緩存起來,一直到參數(shù)傳遞過來(在這里就是name這個(gè)參數(shù))。下面來新建一個(gè)這樣的函數(shù)。
public static Predicate<String> checkIfStartsWith(final String letter) {
return name -> name.startsWith(letter);
}
我們定義了一個(gè)靜態(tài)函數(shù)checkIfStartsWith,它接收一個(gè)String參數(shù),并且返回一個(gè)Predicate對(duì)象,它正好可以傳遞給filter方法,以便后面進(jìn)行使用。不像前面看到的高階函數(shù)那樣是以函數(shù)作為參數(shù)的,這個(gè)方法返回的是一個(gè)函數(shù)。不過它也是一個(gè)高階函數(shù),這個(gè)我們?cè)?2頁的進(jìn)化,而非變革中已經(jīng)提到過了。
checkIfStartsWith方法返回的Predicate對(duì)象和其它lambda表達(dá)式有些不同。在 return name -> name.startsWith(letter)語句中,我們很清楚name是什么,它是傳入到lambda表達(dá)式中的參數(shù)。不過變量letter到底是什么?它是在這個(gè)匿名函數(shù)的域外邊的,Java找到了定義這個(gè)lambda表達(dá)式的域,并發(fā)現(xiàn)了這個(gè)變量letter。這個(gè)就叫做詞法作用域。詞法作用域是個(gè)很有用的東西,它使得我們可以在一個(gè)用用域中緩存一個(gè)變量,以便后面在另一個(gè)上下文中進(jìn)行使用。由于這個(gè)lambda表達(dá)式使用了它的定義域中的變量,這種情況也被稱作閉包。關(guān)于詞法作用域的訪問限制,可以看下31頁的詞法作用域有什么限制嗎?
詞法作用域有什么限制嗎?
在lambda表達(dá)式中,我們只能訪問它的定義域中的final類型或者實(shí)際上是final類型的本地變量。
lambda表達(dá)式可能馬上就會(huì)被調(diào)用,也可能延遲進(jìn)行調(diào)用,或者從不同的線程發(fā)起調(diào)用。為了避免競(jìng)爭(zhēng)沖突,我們?cè)L問的定義域中的本地變量,一旦初始化后是不允許進(jìn)行修改的。任何修改操作都會(huì)導(dǎo)致編譯異常。
標(biāo)記成final后解決了這個(gè)問題,不過Java并不強(qiáng)迫我們一定要這么標(biāo)記。事實(shí)上,Java看的是兩點(diǎn)。一個(gè)是訪問的這個(gè)變量必須要在定義它的方法中完成初始化,并且要在定義lambda表達(dá)式之前。第二,這些變量的值不能進(jìn)行修改——也就是說,它們事實(shí)上就是final類型的,盡管沒有這么標(biāo)記。
無狀態(tài)的lambda表達(dá)式是運(yùn)行時(shí)常量,而那些使用了本地變量的lambda表達(dá)則會(huì)有額外的計(jì)算開銷。
在調(diào)用filter方法的時(shí)候我們就可以用checkIfStartsWith方法返回的lambda表達(dá)式了,就像這樣:
final long countFriendsStartN =
friends.stream() .filter(checkIfStartsWith("N")).count();
final long countFriendsStartB = friends.stream()
.filter(checkIfStartsWith("B")).count();
在調(diào)用filter方法之前 ,我們先調(diào)用了checkIfStartsWith()方法,把想要的字母?jìng)鲄⑦M(jìn)去。這個(gè)調(diào)用很快就返回了一個(gè)lambda表達(dá)式,然后我們把它傳參給filter方法。
通過創(chuàng)建了一個(gè)高階函數(shù)(這里是checkIfStartsWith)并且使用了詞法作用域,我們成功的去除了代碼中的冗余。我們不用再重復(fù)的判斷name是不是以某個(gè)字母開頭了。
重構(gòu),縮小作用域
在前面的例子中我們用了一個(gè)static方法,不過我們不希望用static方法來緩存變量,這樣把我們的代碼搞亂了。最好能把這個(gè)函數(shù)的作用域縮小到使用它的地方。我們可以用一個(gè)Function接口來實(shí)現(xiàn)這個(gè)。
final Function<String, Predicate<String>> startsWithLetter = (String letter) -> {
Predicate<String> checkStarts = (String name) -> name.startsWith(letter);
return checkStarts; };
這個(gè)lambda表達(dá)式取代了原來的static方法,它可以放到函數(shù)里面,在需要用到它之前定義一下就好了。startWithLetter變量引用的是一個(gè)入?yún)⑹荢tring,出參是Predicate的Function。
和使用static方法相比,這個(gè)版本簡(jiǎn)單多了,不過我們還可以對(duì)它繼續(xù)重構(gòu)讓它更簡(jiǎn)潔點(diǎn)。從實(shí)際的角度看,這個(gè)函數(shù)和前面的static方法是一樣的;它們都接收一個(gè)String返回一個(gè)Predicate。為了不顯式的聲明一個(gè)Predicate, 我們用一個(gè)lamdba表達(dá)式整個(gè)給替換掉。
final Function<String, Predicate<String>> startsWithLetter = (String letter) -> (String name) -> name.startsWith(letter);
我們把那些亂七八糟的東西給干掉了,但是我們還可以去掉類型聲明,讓它更簡(jiǎn)潔一點(diǎn),Java編譯器會(huì)根據(jù)上下文去做類型推導(dǎo)的。我們來看下改進(jìn)后的版本。
final Function<String, Predicate<String>> startsWithLetter =
letter -> name -> name.startsWith(letter);
要適應(yīng)這種簡(jiǎn)潔的語法可得下點(diǎn)工夫。如果它亮瞎了你的眼睛的話,先看看別的地方吧。我們已經(jīng)完成了代碼的重構(gòu),現(xiàn)在可以用它來替換掉原來的checkIfStartsWith()方法了,就像這樣:
final long countFriendsStartN = friends.stream()
.filter(startsWithLetter.apply("N")).count();
final long countFriendsStartB = friends.stream()
.filter(startsWithLetter.apply("B")).count();
在這節(jié)中我們用到了高階函數(shù)。我們看到了如果把函數(shù)傳遞給另一個(gè)函數(shù),如何在函數(shù)中創(chuàng)建函數(shù),以及如何通過函數(shù)來返回一個(gè)函數(shù)。這些例子都展示了lambda表達(dá)式帶來的簡(jiǎn)潔性和可重用性。
本節(jié)中我們充分發(fā)揮了Function和Predicate的作用,不過我們來看下它們兩個(gè)到底有什么區(qū)別。Predicate接受一個(gè)類型為T的參數(shù),返回一個(gè)boolean值來代表它對(duì)應(yīng)的判斷條件的真假。當(dāng)我們需要做條件判斷的時(shí)候,我們可以使用Predicateg來完成。像filter這類對(duì)元素進(jìn)行篩選的方法都接收Predicate作為參數(shù)。而Funciton代表的是一個(gè)函數(shù),它的入?yún)⑹穷愋蜑門的變量,返回的是R類型的一個(gè)結(jié)果。它和只能返回boolean的Predicate相比要更加通用。只要是將輸入轉(zhuǎn)化成一個(gè)輸出的,我們都可以使用Function,因此map使用Function作為參數(shù)也是情理之中的事情了。
可以看到,從集合中選取元素非常簡(jiǎn)單。下面我們將介紹下如何從集合中只挑選出一個(gè)元素。