前面我們已經用過幾次collect()方法來將Stream回傳的元素拼成ArrayList了。這是一個reduce操作,它對於將一個集合轉換成另一種類型(通常是一個可變的集合)非常有用。 collect()函數,如果和Collectors工具類別裡的一些方法結合起來使用的話,能提供極大的便利性,本節我們將會介紹到。
我們還是繼續使用前面的Person列表作為例子,來看collect()方法到底有哪些能耐。假設我們要從原始清單中找出所有大於20歲的人。以下是使用了可變性和forEach()方法實作的版本:
複製代碼代碼如下:
List<Person> olderThan20 = new ArrayList<>(); people.stream()
.filter(person -> person.getAge() > 20)
.forEach(person -> olderThan20.add(person)); System.out.println("People older than 20: " + olderThan20);
我們使用filter()方法來從清單中過濾出了所有年齡大於20的人。然後,在forEach方法裡,我們將元素加入到一個在前面已經初始化好的ArrayList。我們先看下這段程式碼的輸出結果,一會兒再去重構它。
複製代碼代碼如下:
People older than 20: [Sara - 21, Jane - 21, Greg - 35]
程式輸出的結果是對的,不過還有點小問題。首先,把元素加到集合中,這種屬於低階操作――它是命令式的,而非聲明式的。如果我們想把這個迭代改造成並發的,還得去考慮線程安全的問題――可變性使得它難以並行化。幸運的是,使用collect()方法可以很容易解決掉這個問題。來看下如何實現的。
collect()方法接受一個Stream並將它們收集到一個結果容器中。要完成這個工作,它需要知道三個東西:
+如何建立結果容器(比如說,使用ArrayList::new方法) +如何把單一元素加入容器中(例如使用ArrayList::add方法) +如何把一個結果集合併到另一個中(例如使用ArrayList: :addAll方法)
對於串行操作而言,最後一條不是必需的;程式碼設計的目標是能同時支援串行和並行的。
我們把這些操作提供給collect方法,讓它來把過濾後的流給收集起來。
複製代碼代碼如下:
List<Person> olderThan20 =
people.stream()
.filter(person -> person.getAge() > 20)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
System.out.println("People older than 20: " + olderThan20);
這段程式碼的結果和前面一樣,不過這樣寫有許多好處。
首先,我們程式設計的方式更聚焦了,表述性也更強,清晰的傳達了你要把結果收集到一個ArrayList裡去的目的。 collect()的第一個參數是一工廠或生產者,後面的參數是用來收集元素的操作。
第二,由於我們沒有在程式碼中個執行明確的修改操作,可以很容易並行地執行這個迭代。我們讓底層函式庫來完成修改操作,它自己會處理好協作及線程安全的問題,儘管ArrayList本身不是線程安全的――幹的漂亮。
如果條件允許的話,collect()方法可以並行地將元素添加到不同的子列表中,然後再用一個線程安全的方式將它們合併到一個大列表裡(最後一個參數就是用來進行合併操作的) 。
我們已經看到,相對於手動把元素加入到列表中而言,使用collect()方法的好處真是太多了。下面我們來看下這個方法的一個重載的版本――它更簡單也更方便――它是使用一個Collector作為參數。這個Collector是一個包含了生產者,添加器,以及合併器在內的接口――在前面的版本中這些操作是作為獨立的參數分別傳入方法中的――使用Collector則更簡單並且可以復用。 Collectors工具類別提供了一個toList方法,可以產生一個Collector的實現,用來把元素加入到ArrayList。我們來修改下前面那段程式碼,使用一下這個collect()方法。
複製代碼代碼如下:
List<Person> olderThan20 =
people.stream()
.filter(person -> person.getAge() > 20)
.collect(Collectors.toList());
System.out.println("People older than 20: " + olderThan20);
使用了Collectors工具類別的簡潔版的collect()方法,可不只這一種用法。 Collectors工具類別中還有好幾種不同的方法可以進行不同的收集和添加的操作。比方說,除了toList()方法,還有toSet()方法,可以加入到一個Set中,toMap()方法可以用來收集到一個key-value的集合中,還有joining()方法,可以拼接成一個字串。我們也可以將mapping(),collectingAndThen(),minBy(), maxBy()和groupingBy()等方法組合起來進行使用。
我們來用下groupingBy()方法將人群依年齡分組。
複製代碼代碼如下:
Map<Integer, List<Person>> peopleByAge =
people.stream()
.collect(Collectors.groupingBy(Person::getAge));
System.out.println("Grouped by age: " + peopleByAge);
只需簡單的呼叫下collect()方法便能完成分組。 groupingBy()接受一個lambda表達式或方法引用――這種叫分類函數――它傳回需要分組的物件的某個屬性的值。根據我們這個函數傳回的值,來把呼叫上下文中的元素放進某個分組。在輸出中可以看到分組的結果:
複製代碼代碼如下:
Grouped by age: {35=[Greg - 35], 20=[John - 20], 21=[Sara - 21, Jane - 21]}
這些人已經按年齡進行了分組。
在前面這個例子中我們按人群的年齡對他們進行了分組收集。 groupingBy()方法的一個變種可以依照多個條件進行分組。簡單的groupingBy()方法使用了分類器進行元素收集。而通用的groupingBy()收集器,則可以為每一個分組指定一個收集器。也就是說,元素在收集的過程中會經過不同的分類器和集合,下面我們將會看到。
繼續使用上面這個例子,這回我們不按年齡分組了,我們只取得人的名字,按他們的年齡進行排序。
複製代碼代碼如下:
Map<Integer, List<String>> nameOfPeopleByAge =
people.stream()
.collect(
groupingBy(Person::getAge, mapping(Person::getName, toList())));
System.out.println("People grouped by age: " + nameOfPeopleByAge);
這個版本的groupingBy()接受兩個參數:第一個是年齡,這是分組的條件,第二個是收集器,它是由mapping()函數傳回的結果。這些方法都來自Collectors工具類,在這段程式碼中進行了靜態的導入。 mapping()方法接受兩個參數,一個是映射用的屬性,一個是物件要收集到的地方,比如說list或set。來看下上面這段程式碼的輸出結果:
複製代碼代碼如下:
People grouped by age: {35=[Greg], 20=[John], 21=[Sara, Jane]}
可以看到,人們的名字已經按年齡分組了。
我們再來看一個組合的操作:依照名字的首字母分組,然後選出每個分組中年紀最大的那位。
複製代碼代碼如下:
Comparator<Person> byAge = Comparator.comparing(Person::getAge);
Map<Character, Optional<Person>> oldestPersonOfEachLetter =
people.stream()
.collect(groupingBy(person -> person.getName().charAt(0),
reducing(BinaryOperator.maxBy(byAge))));
System.out.println("Oldest person of each letter:");
System.out.println(oldestPersonOfEachLetter);
我們先是按名字的首字母進行了排序。為了實現這個,我們把一個lambda表達式當作groupingBy()的第一個參數傳了進去。這個lambda表達式是用來傳回名字的首字母的,以便進行分組。第二個參數不再是mapping()了,而是執行了一個reduce操作。在每個分組內,它都使用maxBy()方法,從所有元素中遞推出最年長的那位。由於組合了許多操作,這個語法看起來有點臃腫,不過整個讀起來是這樣的:按名字首字母進行分組,然後遞推出分組中最年長的那位。來看看這段程式碼的輸出,它列出了指定字母開頭的那組名字中年紀最大的那個人。
複製代碼代碼如下:
Oldest person of each letter:
{S=Optional[Sara - 21], G=Optional[Greg - 35], J=Optional[Jane - 21]}
我們已經領教了collect()方法以及Collectors工具類別的威力。在你的IDE或JDK的官方文件裡面,再花點時間去研究下Collectors工具類別吧,熟悉下它提供的各種方法。下面我們將會用lambda表達式來完成一些過濾器的實作。