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);
}
}