9.收集到映射表中
Collectors.toMap方法有兩個函數引元,用來產生映射表的鍵和值:
Map<Integer, String> idToName = people.collect(Collectors.toMap(Person::getId, Person::getName));
通常情況下,值應該是實際的元素,因此第二個函數可以使用Function.identity()
多個元素具有相同的鍵,收集器會拋出一個IllegalStateException對象。可以通過提供第三個函數引元來覆蓋這種行爲。該函數會針對給定的已有值和新值來解決衝突並確定鍵對應的值。這個函數應該返回已有值、新值或它們的組合。
獲取所有Locale的名字爲鍵和其本地化名字爲值:
Stream<Locale> locales = Stream.of(Locale.getAvailableLocales());
Map<String, String> languageNames = locales.collect(
Collectors.toMap(
Locale::getDisplayLanguage,
l -> l.getDisplayLanguage(l),
(existingValue, newValue) -> existingValue));
不必擔心同一種語言是否可能出現兩次,只記錄第一項。
如果需要了解給定所有國家的語言,就需要一個Map< String, Set< String > >:
Map<String, Set<String>> countryLanguageSets = locales.collect(
Collectors.toMap(
Locale::getDisplayCountry,
l -> Collections.singleton(l.getDisplayLanguage()),
(a, b) -> {
Set<String> union = new HashSet<>(a);
union.addAll(b);
return union;
}
));
想要得到TreeMap,可以將構造器做爲第4個引元來提供,必須提供一種合併函數:
Map<Integer, Person> idToPerson = people.collect(
Collectors.toMap(
Person::getId,
Function.identity(),
(existingValue, newValue) -> { throw new IllegalStateException(); },
TreeMap::new));
對於每個toMap方法,都有一個等價的可以產生併發散列表的toConcurrentMap方法。單個併發映射表可以用於併發集合處理。當使用並行流時,共享的映射表比合並映射表要高效。元素不再是按照流中的順序收集。
public class CollectingIntoMaps {
public static class Person{
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
@Override
public String toString() {
return getClass().getName() + "[id=" + id + ",name=" + name + "]";
}
}
public static Stream<Person> people() throws IOException {
return Stream.of(new Person(1001, "Peter"), new Person(1002, "Paul"),
new Person(1003, "Mary"));
}
public static void main(String[] args) throws IOException {
Map<Integer, String> idToName = people().collect(
Collectors.toMap(Person::getId, Person::getName));
System.out.println("idToName: " + idToName);
Map<Integer, Person> idToPerson = people().collect(
Collectors.toMap(Person::getId, Function.identity()));
System.out.println("idToPerson: " + idToPerson.getClass().getName() + idToPerson);
idToPerson = people().collect(
Collectors.toMap(Person::getId, Function.identity(),
(existingValue, newValue) -> { throw new IllegalStateException(); }, TreeMap::new));
System.out.println("idToPerson: " + idToPerson.getClass().getName() + idToPerson);
Stream<Locale> locales = Stream.of(Locale.getAvailableLocales());
Map<String, String> languageNames = locales.collect(
Collectors.toMap(Locale::getDisplayLanguage,
l -> l.getDisplayLanguage(l),
(existingValue, newValue) -> existingValue));
System.out.println("languageNames: " + languageNames);
locales = Stream.of(Locale.getAvailableLocales());
Map<String, Set<String>> countryLanguageSets = locales.collect(
Collectors.toMap(
Locale::getDisplayCountry,
l -> Collections.singleton(l.getDisplayLanguage()),
(a, b) -> {
Set<String> union = new HashSet<>(a);
union.addAll(b);
return union;
}
));
System.out.println("countryLanguageSets: " + countryLanguageSets);
}
}
10.羣組和分區
groupingBy將具有相同特性的值羣聚成組。
Map<String, List<Locale>> countryToLocales = locales.collect(
Collectors.groupingBy(Locale::getCountry));
List<Locale> swissLocales = countryToLocales.get("CN");
// [zh_CN]
每個Locale都有一個語言代碼(en)和一個國家代碼(US)。Locale en_US描述美國英語,而en_IE是愛爾蘭英語,某些國家有過個Locale。
當分類函數是斷言函數時,流的元素可以分區爲兩個列表:該函數返回true的元素和其他元素(partitioningBy比groupingBy更高效)。
Map<Boolean, List<Locale>> englishAndOtherLocales = locales.collect(
Collectors.partitioningBy(l -> l.getLanguage().equals("en")));
List<Locale> englishLocales = englishAndOtherLocales.get(true);
使用groupingByConcurrent方法,就會使用並行流時獲得一個被並行組裝的並行映射表。
11.下游收集器
groupingBy方法會產生一個映射表,它的每個值都是一個列表,如果想處理這些列表,就需要提供一個下游收集器。
靜態導入java.util.stream.Collectors.*會使表達式更容易閱讀。
import static java.util.stream.Collectors.*;
Map<String, Set<Locale>> countryToLocales = locales.collect(
groupingBy(Locale::getCountry, toSet()));
Java提供了多種可以將羣組元素約簡爲數字的收集器:
1.counting會產生收集到的元素的個數:
Map<String, Long> countryToLocaleCounts = locales.collect(
groupingBy(Locale::getCountry, counting()));
2.summing(Int|Long|Double)會接收一個函數引元,將該函數應用到下游元素中,併產生它們的和:
Map<String, Integer> stateToCityPopulation = cities.collect(
groupingBy(City::getState, summarizingInt(City::getPopulation)));
3.maxBy和minBy會接收一個比較器,併產生下游元素中的最大值和最小值:
Map<String, Optional<City>> stateToLargestCity = cities.collect(
groupingBy(City::getState, maxBy(Comparator.comparing(City::getPopulation))));
mapping方法會產生函數應用到下游結果上的收集器,並將函數值傳遞給另一個收集器:
Map<String, Optional<String>> stateToLongstCityName = cities.collect(
groupingBy(City::getState, mapping(City::getName, maxBy(Comparator.comparing(String::length)))));
將城市羣組在一起,在每個州內部,生成各個城市的名字,並按照最大長度約簡。
9節中把語言收集到一個集中,使用mapping會有更加的解決方案:
Map<String, Set<String>> contryToLanguages = locales.collect(
groupingBy(Locale::getDisplayCountry,
mapping(Locale::getDisplayLanguage, toSet())));
9節中使用的是toMap而不是groupingBy,上述方式中,無需操心如何將各個集組合起來。
可以從每個組的彙總對象中獲取到這些函數值的總和、個數、平均值、最小值和最大值:
Map<String, IntSummaryStatistics> stateToCityPopulationSummary = cities.collect(
groupingBy(City::getState, summarizingInt(City::getPopulation)));
public class DownstreamCollectors {
public static class City {
private String name;
private String state;
private int population;
public City(String name, String state, int population) {
this.name = name;
this.state = state;
this.population = population;
}
public String getName() {
return name;
}
public String getState() {
return state;
}
public int getPopulation() {
return population;
}
}
public static Stream<City> readCities(String filename) throws IOException {
return Files.lines(Paths.get(filename)).map(l -> l.split(", ")).
map(a -> new City(a[0], a[1], Integer.parseInt(a[2])));
}
public static void main(String[] args) throws IOException {
Stream<Locale> locales = Stream.of(Locale.getAvailableLocales());
Map<String, Set<Locale>> countryToLocaleSet = locales.collect(groupingBy(
Locale::getCountry, toSet()));
System.out.println("countryToLocaleSet: " + countryToLocaleSet);
locales = Stream.of(Locale.getAvailableLocales());
Map<String, Long> countryToLocaleCounts = locales.collect(groupingBy(
Locale::getCountry, counting()));
System.out.println("countryToLocaleCounts: " + countryToLocaleCounts);
Stream<City> cities = readCities("cities.txt");
Map<String, Integer> stateToCityPopulation = cities.collect(
groupingBy(City::getState, summingInt(City::getPopulation)));
System.out.println("stateToCityPopulation: " + stateToCityPopulation);
cities = readCities("cities.txt");
Map<String, Optional<String>> stateToLongestCityName = cities.collect(groupingBy(
City::getState, mapping(City::getName, maxBy(Comparator.comparing(String::length)))));
System.out.println("stateToLongestCityName: " + stateToLongestCityName);
locales = Stream.of(Locale.getAvailableLocales());
Map<String, Set<String>> countryToLanguages = locales.collect(groupingBy(
Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));
System.out.println("countryToLanguages: " + countryToLanguages);
cities = readCities("cities.txt");
Map<String, IntSummaryStatistics> stateToCityPopulationSummary = cities.collect(
groupingBy(City::getState, summarizingInt(City::getPopulation)));
System.out.println(stateToCityPopulationSummary.get("上海市"));
cities = readCities("cities.txt");
Map<String, String> stateToCityNames = cities.collect(
groupingBy(City::getState, reducing("", City::getName, (s, t) -> s.length() == 0 ? t : s + ", " + t)));
System.out.println("stateToCityNames: " + stateToCityNames);
cities = readCities("cities.txt");
stateToCityNames = cities.collect(groupingBy(City::getState, mapping(City::getName, joining(", "))));
System.out.println("stateToCityNames: " + stateToCityNames);
}
}
12.約簡操作
reduce方法是一種用於從流中計算某個值的通用機制。最簡單的形式將接受一個二元函數,並從前兩個元素開始持續應用它。如果該函數是求和函數:
List<Integer> values = ...;
Optional<Integer> sum = values.stream().reduce((x,y) -> x + y);
reduce會計算v0 + v1 + v2 +…,如果流爲空,方法會返回一個Optional。上面情況可以寫成reduce(Integer::sum)
通常v0 op v1 op v2 op…,調用函數op(vi, vi+1)寫作vi op vi+1。這項操作是可結合的:即組合元素時使用順序不應該成爲問題。(x op y) op z等於x op (y op z),這使得在使用並行流時,可以執行高效約簡。
求和,乘積,字符串連接,取最大值,最小值,求集的並與交等,都是可結合操作。減法不是一個可結合操作,(6-3)-2 ≠ 6 - (3 - 2)。
通常幺元值e使得e op x = x,可以使用這個元素做爲計算的起點:
Integer sum = values.stream().reduce(0, (x, y) -> x + y);
// 0 + v0 + v1 + v2 + ...
如果流爲空,則會返回幺元值。
如果有一個對象流,且想要對某些屬性求和,需要(T,T)->T這樣的函數,即引元和結果的類型相同的函數。
但如果類型不同,例如流的元素具有String類型,而累積結果是整數,首先需要一個累積器(total, word) -> total + word.length(),這個函數會被反覆調用產生累積的總和。但是當計算被並行化時,會有更多個這種類型的計算,需要提供第二個函數來將結果合併:
int result = words.reduce(0,
(total, word) -> total + word.length(),
(total1, total2) -> total1 + total2);
在實踐中reduce會顯得並不夠通用,通常映射爲數字流並使用其他方法來計算總和、最大值和最小值(words.mapToInt(String::length).sum(),因爲不涉及裝箱操作,所以更簡單也更高效)。
13.基本類型流
將整數收集到Stream< Integer >中,將每個整數都包裝到包裝器對象中是很低效的。流庫中具有專門的類型IntStream、LongStream和DoubleStream,用來直接存儲基本類型值,無需使用包裝器。short、char、byte和boolean,可以使用IntStream,對於float可以使用DoubleStream。
調用IntStream.of和Arrays.stream方法創建IntStream:
IntStream stream = IntStream.of(1, 1, 2, 3, 5);
stream = Arrays.stream(values, 2, 4); //values是一個數組
基本類型流還可以使用靜態的generate和iterate方法,此外,IntStream和LongStream有靜態方法range和rangeClosed,可以生成步長爲1的整數範圍:
IntStream zeroToNinetyNine = IntStream.range(0 ,100);
IntStream zeroToHundred = IntStream.rangeClosed(0 ,100);
CharSequence接口擁有codePoints和chars方法,可以生成有字符的Unicode碼或有UTF-16編碼機制的碼元構成的IntStream:
String sentence = "\uD835\uDD46 is the set of octonions.";
IntStream codes = sentence.codePoints();
對象流可以用mapToInt、mapToLong和mapToDouble將其轉換爲基本類型流:
Stream<String> words = ...;
IntStream lengths = words.mapToInt(String::length);
基本類型流轉換爲對象流,使用boxed方法:
Stream<Integer> integers = IntStream.range(0, 100).boxed();
基本類型流的方法與對象流的方法的主要差異:
1.toArray方法會返回基本類型數組。
2.返回結果OptionalInt|Long|Double,要用getAsInt|Long|double方法,而不是get方法
3.具有返回總和、平均值、最大值和最小值的sum、average、max和min方法,對象流沒有
4.summaryStatistics方法會產生一個類型爲Int|Long|DoubleSummaryStatistics的對象,他們可以同時報告流的總和、平均值、最大值和最小值。
Random類具有ints、longs和doubles方法,可以返回隨機數構成的基本類型流。
public class PrimitiveTypeStreams {
public static void show(String title, IntStream stream) {
final int SIZE = 10;
int[] firstElements = stream.limit(SIZE + 1).toArray();
System.out.print(title + ": ");
for (int i = 0; i < firstElements.length; i++) {
if (i > 0) {
System.out.print(", ");
}
if (i < SIZE) {
System.out.print(firstElements[i]);
} else {
System.out.print("...");
}
}
System.out.println();
}
public static void main(String[] args) throws IOException {
IntStream is1 = IntStream.generate(() -> (int)(Math.random() * 100));
show("is1", is1);
IntStream is2 = IntStream.range(5, 10);
show("is2", is2);
IntStream is3 = IntStream.rangeClosed(5, 10);
show("is3", is3);
Path path = Paths.get("alice30.txt");
String contents = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
Stream<String> words = Stream.of(contents.split("\\PL+"));
IntStream is4 = words.mapToInt(String::length);
show("is4", is4);
String sentence = "\uD835\uDD46 is the set of octonions.";
System.out.println(sentence);
IntStream codes = sentence.codePoints();
System.out.println(codes.mapToObj(c -> String.format("%X ", c)).collect(
Collectors.joining()));
Stream<Integer> integers = IntStream.range(0, 100).boxed();
IntStream is5 = integers.mapToInt(Integer::intValue);
show("is5", is5);
}
}
14.並行流
流使得並行處理塊操作變得很容易,但必須有一個並行流。可以用Collection.parallelStream()方法從任何集合中獲取一個並行流:
Stream<String> parallelWords = words.parallelStream();
而parallel方法可以將任意的順序流轉換爲並行流:
Stream<String> parallelWords = Stream.of(strings).parallel();
只要在終結方法執行時,流處於並行模式,那麼所有的中間流操作都將被並行化。
當流操作並行運行時,其目標是要讓其返回結果與順序執行時返回的結果相同。重要的是,這些操作可以以任意順序執行。
對字符串流的所有短單詞計數:
String contents = new String(Files.readAllBytes(Paths.get("alice30.txt")), StandardCharsets.UTF_8);
List<String> wordList = Arrays.asList(contents.split("\\PL+"));
int[] shortWords = new int[12];
wordList.parallelStream().forEach(
s -> { if (s.length() < 12) { shortWords[s.length()]++; }});
System.out.println(Arrays.toString(shortWords));
這是一種非常糟糕的代碼。傳遞給forEach的函數會在多個併發線程中運行,每個都會更新共享的數組。這是一種經典的競爭情況。如果多次運行這個程序,每次運行都會產生不同的計數值,而且每個都是錯的。
需要確保傳遞給並行流操作的任何函數都可以安全地並行執行,最佳方式是遠離易變狀態。如果用長度將字符串羣組,然後在分別計數,就可以安全地並行化計算:
Map<Integer, Long> shortWordCounts = wordList.parallelStream().
filter(s -> s.length() < 10).collect(Collectors.groupingBy(String::length, Collectors.counting()));
默認情況下,有序集合(數組和列表或Stream.sorted)產生的流都是有序的。因此結果是完全可以預知的,運行相同操作兩次,結果是完全相同的結果。
排序並不排斥高效地並行處理。當計算stream.map()時,流可以被劃分爲n的部分,會並行處理。然後按照順序重新組裝起來。
當放棄排序需求時,可以被更有效地並行化。通過在流上調用unordered方法,就可以明確表示對排序不感興趣。在有序的流中,distinct會保留所有相同元素的第一個,這對並行化是一種阻礙,因爲處理每個部分的線程在其之前的所有部分都被處理完之前,並不知道應該丟棄哪些元素。
還可以通過放棄排序要求來提高limit方法的速度:
Stream<String> sample = words.parallelStream().unordered().limit(n);
合併映射表的代價很高昂,所以Collectors.groupByConcurrent方法使用了共享的併發映射表。爲了從並行化中獲益,映射表中值的順序不會與流中的順序相同。
Map<Integer, List<String>> result = words.parallelStream().collect(
Collectors.groupingByConcurrent(String::length));
如果使用獨立於排序的下游收集器:
Map<Integer, Long> wordCounts = words.parallelStream().collect(
Collectors.groupingByConcurrent(String::length, Collectors.counting()));
不要修改在執行某項流操作後會將元素返回到流中的集合(即使這樣修改是線程安全的)。流並不會收集它們的數據,數據總是在單獨的集合中。如果修改了這樣的集合,那麼流操作的結果就是未定義的(順序流和並行流都採用這種方式)。
因爲中間的流操作是惰性的,所以直到執行終結操作時纔對集合進行修改仍舊是可行的。儘管不推薦,但仍可以工作:
List<String> wordList = ...;
Stream<String> words = wordList.stream();
wordList.add("END");
long n = words.distinct().count();
但是下面是錯誤的:
Stream<String> words = wordList.stream();
words.forEach(s -> if(s.length() < 12) wordList.remove(s));
爲了讓並行流正常工作,需要滿足大量條件:
1.數據應該在內存中
2.流應該可以被高效地分成若干個子部分,由數組和平衡二叉樹支撐的流
3.流操作的工作量應該具有較大的規模,不要將所有流都轉化爲並行流,只有對已經位於內存中的數據執行大量計算操作時,才應該使用並行流
4.流操作不應該被阻塞
public class ParallelStreams {
public static void main(String[] args) throws IOException {
String contents = new String(Files.readAllBytes(Paths.get("alice30.txt")), StandardCharsets.UTF_8);
List<String> wordList = Arrays.asList(contents.split("\\PL+"));
// 代碼很糟糕
int[] shortWords = new int[10];
wordList.parallelStream().forEach(s -> {
if (s.length() < 10) {
shortWords[s.length()]++;
}
});
System.out.println(Arrays.toString(shortWords));
//再試一次結果可能會不同(也可能是錯誤的)
Arrays.fill(shortWords, 0);
wordList.parallelStream().forEach(s ->{
if (s.length() < 10) {
shortWords[s.length()]++;
}
});
System.out.println(Arrays.toString(shortWords));
// 補救措施:分組計數
Map<Integer, Long> shortWordCounts = wordList.parallelStream()
.filter(s -> s.length() < 10)
.collect(groupingBy(String::length, counting()));
System.out.println(shortWordCounts);
Map<Integer, List<String>> result = wordList.parallelStream().collect(
groupingByConcurrent(String::length));
System.out.println(result.get(14));
result = wordList.parallelStream().collect(
groupingByConcurrent(String::length));
System.out.println(result.get(14));
Map<Integer, Long> wordCounts = wordList.parallelStream().collect(
groupingByConcurrent(String::length, counting()));
System.out.println(wordCounts);
}
}