尋找元素
現在我們對這個設計優雅的轉換集合的方法已經不陌生了,但它對尋找元素卻也無能為力。不過filter方法卻是為此而生的。
我們現在要從一個名字清單中,取出那些以N開頭的名字。當然可能一個也沒有,結果可能是個空集合。我們先用老方法實現一把。
複製代碼代碼如下:
final List<String> startsWithN = new ArrayList<String>();
for(String name : friends) {
if(name.startsWith("N")) {
startsWithN.add(name);
}
}
這麼簡單的事件,寫了這麼多程式碼,也夠嗦的了。我們先創建了一個變量,然後把它初始為一個空集合。然後遍歷原來的集合,找出那些以指定字母開頭的名字。如果找到,就插入集合裡。
我們用filter方法來重構上面這段程式碼,看看它的威力到底如何。
複製代碼代碼如下:
final List<String> startsWithN =
friends.stream()
.filter(name -> name.startsWith("N"))
.collect(Collectors.toList());
filter方法接收一個傳回布林值的lambda表達式。如果表達式結果為true,運行上下文中的那個元素就會被加到結果集中;如果不是,就跳過它。最後回傳的是一個Steam,它裡面只包含那些表達式回傳true的元素。最後我們用一個collect方法把這個集合轉換成一個清單――在後面52頁的使用collect方法和Collecters類別中,我們會對這個方法進去更深入的探討。
讓我們來列印一下這個結果集中的元素:
複製代碼代碼如下:
System.out.println(String.format("Found %d names", startsWithN.size()));
從輸出結果很明顯能看出來,這個方法把集合中匹配的元素全都找出來了。
複製代碼代碼如下:
Found 2 names
filter方法和map方法一樣,也回傳了一個迭代器,不過它們也就這點相同而已了。 map傳回的集合和輸入集合大小是一樣的,而filter回傳的可不好說。它傳回的集合的大小區間,從0一直到輸入集的元素個數。和map不一樣的是,filter回傳的是輸入集的子集。
到目前為止,lambda表達式帶來的程式碼簡潔性讓我們很滿意,不過如果不注意的話,程式碼冗餘的問題就開始慢慢滋長了。下面我們來討論下這個問題。
lambda表達式的重用
lambda表達式看起來很簡潔,實際上一不小心很容易就出現程式碼冗餘了。冗餘會導致程式碼品質低下,難以維護;如果我們想做一個改動,得把好幾處相關的程式碼都一起改掉才行。
避免冗餘還可以幫忙我們提升效能。相關的程式碼都集中在一個地方,這樣我們分析它的效能表現,然後優化這一處的程式碼,很容易就能提升程式碼的效能。
現在我們來看看為什麼使用lambda表達式容易導致程式碼冗餘,同時考慮如何去避免它。
複製代碼代碼如下:
final List<String> friends =
Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
final List<String> editors =
Arrays.asList("Brian", "Jackie", "John", "Mike");
final List<String> comrades =
Arrays.asList("Kate", "Ken", "Nick", "Paula", "Zach");
We want to filter out names that start with a certain letter.
我們希望過濾一下某個字母開頭的名字。先用filter方法簡單實作一下。
複製代碼代碼如下:
final long countFriendsStartN =
friends.stream()
.filter(name -> name.startsWith("N")).count();
final long countEditorsStartN =
editors.stream()
.filter(name -> name.startsWith("N")).count();
final long countComradesStartN =
comrades.stream()
.filter(name -> name.startsWith("N")).count();
lambda表達式讓程式碼看起來很簡潔,不過它不知不覺的帶來了程式碼的冗餘。在上面這個例子中,如果想改一下lambda表達式,我們得改不止一處地方――這可不行。幸運的是,我們可以把lambda表達式賦值給變量,然後對它們進行重用,就像使用物件一樣。
filter方法,lambda表達式的接收方,接收的是一個java.util.function.Predicate函數式介面的參考。在這裡,Java編譯器又派上用場了,它用指定的lambda表達式產生了Predicate的test方法的一個實作。現在我們可以更明確的讓Java編譯器去產生這個方法,而不是在參數定義的地方再產生。在上面例子中,我們可以明確的把lambda表達式儲存到一個Predicate類型的引用裡面,然後再把這個引用傳遞給filter方法;這樣做很容易就避免了程式碼冗餘。
我們來重構前面這段程式碼,讓它符合DRY的原則吧。 (Don't Repeat Yoursef――DRY――原則,可參考The Pragmatic Programmer: From Journeyman to Master[HT00],一書)。
複製代碼代碼如下:
final Predicate<String> startsWithN = name -> name.startsWith("N");
final long countFriendsStartN =
friends.stream()
.filter(startsWithN)
.count();
final long countEditorsStartN =
editors.stream()
.filter(startsWithN)
.count();
final long countComradesStartN =
comrades.stream()
.filter(startsWithN)
.count();
現在不用再重複寫那個lambda表達式了,我們只寫了一次,並把它儲存到了一個叫startsWithN的Predicate類型的引用裡面。這後面的三個filter呼叫裡,Java編譯器看到在Predicate偽裝下的lambda表達式,笑而不語,默默接收了。
這個新引入的變數為我們消除了程式碼冗餘。不過不幸的是,後面我們就會看到,敵人很快又回來報仇雪恨了,我們再看看有什麼更厲害的武器能替我們消滅它們。