Java 8学习笔记之深入解读Stream
上一篇,我们提到了解读Stream API的三个方面,并详细解读了第一个:
- 第一个是流的常规操作处理;
- 第二个就是对流的结果的处理;
- 第三个就是流的并行化处理。
接下来,我们可以对后两个进行解读。
对流的结果的处理
在之前,我们曾提到过map-reduce架构,其中的reduce操作就是把流中的数据归约成一个值,这算是对流的一个处理的方案。然而,我们有时候,不仅仅是需要对流进行归约,而是对一个流进行一个工作流的操作之后,把最后的数据再进行相应的处理之后再按照对应的规则来取出来。
因此,reduce操作就显得不够用了,因此在Java 8 中引入了Collectors类。Collectors可以看作是对归约操作的升级,它可以对流中的元素以内部迭代的方式进行归约、分组以及分区的功能。
我们先来简单看看Collectors都有哪些即用的方法:
public final class Collectors {
public static <T>
Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}
public static <T>
Collector<T, ?, Set<T>> toSet() {
return new CollectorImpl<>((Supplier<Set<T>>) HashSet::new, Set::add,
(left, right) -> { left.addAll(right); return left; },
CH_UNORDERED_ID);
}
public static Collector<CharSequence, ?, String> joining() {
return new CollectorImpl<CharSequence, StringBuilder, String>(
StringBuilder::new, StringBuilder::append,
(r1, r2) -> { r1.append(r2); return r1; },
StringBuilder::toString, CH_NOID);
}
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
return joining(delimiter, "", "");
}
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}
...
}
在这里,只列举了一部分经典的简单方法,Collectors中还有很多很多用于归约操作的方法。这个可以看一下Collectors类的源码,就能很清楚的知道了。
我们先来简单的解读一下这几个方法吧,
- toList():顾名思义,就是把流中的元素转换成一个List,在Java 8 中是把这个List的类型设置为ArrayList;
- toSet():同上,是把流中的元素转换成HashSet;
- joining():这个方法是将流中的元素拼接成String,在源码中,我们能够很清楚的看到,是先调用元素的toString()方法,然后进行拼接;
- joining(CharSequence delimiter):这个方法是重载了joining()方法,就是给我们拼接的字符串加一个分隔符。例如",";
- groupingBy(Function<? super T, ? extends K> classifier):这个方法算是Collectors类中内部迭代的一个经典方法了,这个和toList()、toSet()这些恒等方法不同的是,它会对我们想要的分组进行内部迭代。也就是说,我们只需要告诉它我们想要什么。它就可以给我们进行分组了,而不是需要我们手动实现相应的操作。
还以之前的学生列表为例,例如我们获得一个学生名字的字符串,然后每个名字用逗号隔开:
String studentNames = studentList.stream().map(Student::getName).collect(Collectors.joining(","));
我们还想知道对学生根据年级进行分组:
Map<Integer, List<Student>> studentInfo = studentList.stream().collect(Collectors.groupingBy(Student::getGrade));
另外,groupingBy还支持多级分组,例如,我们像根据学生列表统计每个年级有多少人:
Map<Integer, Long> studentCounts = studentList.stream().collect(Collectors.groupingBy(Student::getGrade, Collectors.counting()));
分区属于特殊的分组,分区的定义就是根据一个布尔表达式来进行分区的。也就是说,分区的结果Map中只有两个key:true和false。其他的操作几乎没什么区别。
流的并行化处理
流的并行化处理,在应用中的实现是非常简单的。只需要把转换成流的api从stream()换成parallelStream()即可。例如,把刚刚的统计学生列表中的每个年级多少人换成并行流:
Map<Integer, Long> parallelStudentCounts = studentList.parallelStream().collect(Collectors.groupingBy(Student::getGrade, Collectors.counting()));
是的,使用流的并行化就是这么简单。但是我们也应该要了解一下流的并行化的内部实现原理。
在并行流中,使用了默认的ForkJoinPool。在ForkJoinPool中,默认的线程数就是当前机器的CPU数,这是一个非常建议的线程池中线程数。维护线程过多的话,会造成线程等待,过少的话就无法充分利用当前机器的性能。我们知道,fork/join模型就是把数据源进行拆分,然后对拆分之后的数据块进行计算,最后把拆分的结果进行合并。具体的实现细节,以后找机会再说。
当我们了解过并行流的原理之后,我们就可以更加得心应手的正确使用并行化来处理数据了。这个正确使用指的是使用并行流可以高效的处理数据,我们清楚的知道,并行处理数据的目的就是提高串行处理数据的效率。如果使用并行处理数据的效率,比串行处理的效率差不多甚至更低的话,还不如不用。
因此,在流的并行化中,方便拆分的数据更容易并行化处理。例如数组比链表更容易拆分。当然了,我们写完自己的并行数据处理逻辑之后,很难说并行的效率高。因此,我们就可以使用一个最有力的方法----测试!

浙公网安备 33010602011771号