Java核心技术 Java SE 8的流库2

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);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章