第二章:集合的使用
我們常會用到各種集合,數字的,字串的還有物件的。它們無所不在,即使操作集合的程式碼要能稍微優化一點,都能讓程式碼清晰很多。在這章中,我們探討如何使用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表達式讓我們能夠簡潔明了的進行集合的遍歷。下一節我們將講到它如何使我們在進行刪除操作和集合轉換的時候,也能夠寫出如此簡潔的程式碼。