第二章:集合的使用
我們經常會用到各種集合,數字的,字符串的還有對象的。它們無處不在,哪怕操作集合的代碼要能稍微優化一點,都能讓代碼清晰很多。在這章中,我們探索下如何使用lambda表達式來操作集合。我們用它來遍歷集合,把集合轉化成新的集合,從集合中刪除元素,把集合進行合并。
遍歷列表
遍歷列表是最基本的一個集合操作,這么多年來,它的操作也發生了一些變化。我們使用一個遍歷名字的小例子,從最古老的版本介紹到現在最優雅的版本。
用下面的代碼我們很容易創建一個不可變的名字的列表:
final List<String> friends =
Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
System.out.println(friends.get(i));
}
下面這是最常見的一種遍歷列表并打印的方法,雖然也最一般:
for(int i = 0; i < friends.size(); i++) {
System.out.println(friends.get(i));
}
我把這種方式叫做自虐型寫法——又啰嗦又容易出錯。我們得停下來好好想想,"是i<還是i<=呢?"這只有當我們需要操作具體某個元素的時候才有意義,不過即便這樣,我們還可以使用堅持不可變原則的函數式風格來實現,這個我們很快會討論到。
Java還提供了一種相對先進的for結構。
collections/fpij/Iteration.java
for(String name : friends) {
System.out.println(name);
}
在底層,這種方式的迭代是使用Iterator接口來實現的,調用了它的hasNext和next方法。 這兩種方式都屬于外部迭代器,它們把如何做和想做什么揉到了一起。我們顯式的控制迭代,告訴它從哪開始到哪結束;第二個版本則在底層通過Iterator的方法來做這些。顯式的操作下,還可以用break和continue語句來控制迭代。 第二個版本比第一個少了點東西。如果我們不打算修改集合的某個元素的話,它的方式比第一個要好。不過這兩種方式都是命令式的,在現在的Java中應該摒棄這種方式。 改成函數式原因有這幾個:
1.for循環本身是串行的,很難進行并行化。
2.這樣的循環是非多態的;所得即所求。我們直接把集合傳給for循環,而不是在集合上調用一個方法(支持多態)來執行特定的操作。
3.從設計層面來說,這樣 寫的代碼違反了“Tell,Don't Ask”的原則 。我們請求執行一次迭代,而不是把迭代留給底層庫來執行。
是時候從老的命令式編程轉換到更優雅的內部迭代器的函數式編程了。使用內部迭代器后我們把很多具體操作都扔給了底層方法庫來執行,你可以更專注于具體的業務需求。底層的函數會負責進行迭代的。我們先用一個內部迭代器來枚舉一下名字列表。
Iterable接口在JDK8中得到加強,它有一個專門的名字叫forEach,它接收一個Comsumer類型的參數。如名字所說,Consumer的實例正是通過它的accept方法消費傳遞給它的對象的。我們用一個很熟悉的匿名內部類的語法來使用下這個forEach方法:
friends.forEach(new Consumer<String>() { public void accept(final String name) {
System.out.println(name); }
});
我們調用了friends集合上的forEach方法,給它傳遞了一個Consumer的匿名實現。這個forEach方法從對集合中的每一個元素調用傳入的Consumer的accept方法,讓它來處理這個元素。在這個示例中我們只是打印了一下它的值,也就是這個名字。 我們來看下這個版本的輸出結果,和上兩個的結果 是一樣的:
Brian
Nate
Neal
Raju
Sara
Scott
我們只改了一個地方:我們拋棄了過時的 for循環,使用了新的內部迭代器。好處是,我們不用指定如何迭代這個集合,可以更專注于如何處理每一個元素。缺點是,代碼看起來更啰嗦了——這簡直要把新的編碼風格帶來的喜悅沖的一干二凈了。所幸的是,這個很容易改掉,這正是lambda表達式和新的編譯器的威力大展身手的時候了。我們再做一點修改,把匿名內部類換成lambda表達式。
friends.forEach((final String name) -> System.out.println(name));
這樣看起來就好多了。代碼更少了,不過我們先來看下這是什么意思。這個forEach方法是一個高階函數,它接收一個lambda表達式或者代碼塊,來對列表中的元素進行操作。在每次調用的時候 ,集合中的元素會綁定到name這個變量上。底層庫托管了lambda表達式調用的活。它可以決定延遲表達式的執行,如果合適的話還可以進行并行計算。 這個版本的輸出也和前面的一樣。
Brian
Nate
Neal
Raju
Sara
Scott
內部迭代器的版本更為簡潔。而且,使用它的話我們可以更專注每個元素的處理操作,而不是怎么去遍歷——這可是聲明式的。
不過這個版本還有缺陷。一旦forEach方法開始執行了,不像別的兩個版本,我們沒法跳出這個迭代。(當然有別的方法能搞定這個)。因此,這種寫法在需要對集合里的每個元素處理的時候比較常用。后面我們會介紹到一些別的函數可以讓我們控制循環的過程。
lambda表達式的標準語法,是把參數放到()里面,提供類型信息并使用逗號分隔參數。Java編譯器為了解放我們,還能自動進行類型推導。不寫類型當然更方便了,工作少了,世界也清靜了。下面是上一個版本去掉了參數類型之后的:
friends.forEach((name) -> System.out.println(name));
在這個例子里,Java編譯器通過上下文分析,知道name的類型是String。它查看被調用方法forEach的簽名,然后分析參數里的這個函數式接口。接著它會分析這個接口里的抽象方法,查看參數的個數及類型。即便這個lambda表達式接收多個參數,我們也一樣能進行類型推導,不過這樣的話所有參數都不能帶參數類型;在lambda表達式中,參數類型要么全不寫,要寫的話就得全寫。
Java編譯器對單個參數的lambda表達式會進行特殊處理:如果你想進行類型推導的話,參數兩邊的括號可以省略掉。
friends.forEach(name -> System.out.println(name));
這里有一點小警告:進行類型推導的參數不是final類型的。在前面顯式聲明類型例子中,我們同時也把參數標記為final的。這樣能防止你在lambda表達式中修改參數的值。通常來說,修改參數的值是個壞習慣,這樣容易引起BUG,因此標記成final是個好習慣。不幸的是,如果我們想使用類型推導的話,我們就得自己遵守規則不要修改參數,因為編譯器可不再為我們保駕護航了。
走到這步可費了老勁了,現在代碼量確實少了一點。不過這還不算最簡。我們來體驗下最后這個極簡版的。
friends.forEach(System.out::println);
在上面的代碼中我們用到了一個方法引用。我們用方法名就可以直接替換整個的代碼了。在下節中我們會深入探討下這個,不過現在我們先來回憶下Antoine de Saint-Exupéry的一句名言:完美不是無法再增添加什么,而是無法再去掉什么。
lambda表達式讓我們能夠簡潔明了的進行集合的遍歷。下一節我們會講到它如何使我們在進行刪除操作和集合轉化的時候,也能夠寫出如此簡潔的代碼。