三、收集器
前面我们已经讲过了流的聚合操作,在对流进行一系列操作后,最终再聚合成我们想要的数据结构,我们可以使用JDK提供的收集器,也可以自己定义一个收集器。
1. 定义收集器
定义一个收集器只需要实现 Collector
接口,然后使用流的 collect
方法来使用,下面我们自己模拟 Collectors.joining()
收集器,首先定义收集器:
public class StringCollector implements Collector<Object, StringJoiner, String> { @Override public Supplier<StringJoiner> supplier() { return () -> new StringJoiner(",", "[", "]"); } @Override public BiConsumer<StringJoiner, Object> accumulator() { return ((stringJoiner, o) -> stringJoiner.add(o.toString())); } @Override public BinaryOperator<StringJoiner> combiner() { return StringJoiner::merge; } @Override public Function<StringJoiner, String> finisher() { return StringJoiner::toString; } @Override public Set<Characteristics> characteristics() { return Collections.emptySet(); } }
使用上面定义的收集器:
Stream<Integer> stream = Stream.of(1, 2, 3, 4); Stringstr = stream.collect(new StringCollector()); // str的值为:[1,2,3,4]
2. 自带收集器
2.1 toCollection
将流转换成集合。例如:
Stream<Integer> stream = Stream.of(1, 2, 3, 4); List<Integer> collection = stream.collect(Collectors.toCollection(() -> new ArrayList<>())); List<Integer> list = stream.collect(Collectors.toList()); Set<Integer> set = stream.collect(Collectors.toSet());
三个收集器:
toCollection
:通过函数提供集合类型,可以用于自己封装的集合类型toList
:将流转换成List集合toSet
:将流转换成Set集合
2.2 toMap
将流转成Map,例如,已知一个班级的学生信息,将学生列表转换成以姓名为Key,学生信息为Value的Map:
Map<String, Student> map = students.stream() .collect(Collectors.toMap(Student::getName, Function.identity())); 2.3 joining
2.3 joining
将流中每个元素通过指定分隔符拼接成字符串。例如,已知一个班级的学生信息,将学生的姓名拼接成用逗号隔开的字符串:
String collect = students.stream() .map(Student::getName) .collect(Collectors.joining(","));
可以使用 joining
的另一个重载方法指定结果字符串的前缀和后缀。
2.4 mapping
将流中的元素类型转成成另一种类型,与 map
方法类似。例如,获取学生的姓名列表:
List<String> collect = students.stream() .collect(Collectors.mapping(Student::getName, Collectors.toList())); //等效于 List<String> collect = students.stream() .map(Student::getName) .collect(Collectors.toList());
minBy
等效于min
maxBy
等效于max
counting
等效于count
reducing
等效于reduce
2.5 summingInt、summarizingInt、averagingInt
summingInt
、summingLong
、summingDouble
都是用来对相应类型的数据进行统计的,返回值对应 IntSummaryStatistics
、LongSummaryStatistics
、DoubleSummaryStatistics
,它们都包含统计项:计数、最小值、最大值、平均值、总和。例如,已知一个班级的学生信息,统计学生的成绩:
DoubleSummaryStatistics statistics = students.stream() .collect(Collectors.summarizingDouble(Student::getScore)); // {count=6, sum=500.000000, min=59.000000, average=83.333333, max=95.000000}
averagingInt
、averagingLong
、averagingDouble
只是用来统计平均值。
summarizingInt
、summarizingLong
、summarizingDouble
只是用来统计总和。
2.6 groupingBy、partitioningBy
groupingBy
方法可以分组收集流数据,会生成一个以分组字段为Key的Map,如果要按自己定义的类分组,该类需要重写 hashCode
方法。例如,已知一个班级的学生信息,按性别分组:
Map<Boolean, List<Student>> collect = students.stream() .collect(Collectors.groupingBy(Student::getGender));
partitioningBy
是用于分区,功能和 groupingBy
类似,返回结果类型也一样,但分区方法的返回Map的Key只能是 Boolean
类型,所以结果只能分为两组。
2.8 collectingAndThen
collectingAndThen
方法可以对流进行聚合后,再添加后置操作,该方法第一个参数是收集器,第二个参数是 Function
函数,用于后置操作。例如将流收集成List后再返回集合的大小:
Integer collect = students.stream() .collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
四、并行
数据并行化是将原有的数据集合分割成多个部分,然后给每个部分分配单独的处理单元。比如使用多核CPU的计算机,并行处理就会把数据分发到这多个CPU上单独处理。它与并发不同,并发是在单核上根据时间调度来处理的。当然并行计算并不意味着代码性能就一定能得到提升,影响并行计算的几个主要因素有:
- 数据大小,数据量必须足够大,这样并行任务的拆分和结果的合并这些额外的开销可以忽略。
- 源数据结构,通常是集合,分割相对容易。
- 装箱,处理基本类型比处理装箱类型要快。
- 核的数量,计算机只有一个内核,没必要并行。
- 单元处理开销,流中每个元素的操作时间越长,并行的性能提升越明显。
流的并行化操作非常简单,有两种方式可以生成并行流:
Stream<Student> stream = students.parallelStream(); Stream<Student> stream = students.stream().parallel();
1. fork/join
并行流的底层操作使用了 fork/join 框架,JDK自带的 ForkJoinPool
线程池就可以实现这项操作,它的原理很简单,将现有的任务以递归的形式一层一层拆分成多个子任务,然后多个子任务并行操作,执行完成后将子任务的结果再合并成最终结果。操作示意图如下: