Java Stream 中 Collectors 的 24 个操作-202509120149
Java Stream 中 Collectors 的 24 个操作
Stream 流处理。关于流处理内容比较多,要是说一下Stream 中的Collectors 工具类
的使用。
Collectors 是java.util.stream 包下的一个工具类,其中各个方法的返回值可以作
为java.util.stream.Stream#collect 的入参,实现对队列的各种操作,包括:分组、
聚合等。官方文档给出一些例子:
Implementations of {@link Collector} that implement various useful reduction operations, suc
h as accumulating elements into collections, summarizing elements according to various crit
eria, etc.
The following are examples of using the predefined collectors to perform common mutable re
duction tasks:
// Accumulate names into a
ListList
people.stream().map(Person::getName).collect(Collectors.toList());
// Accumulate names into a
TreeSetSet
people.stream().map(Person::getName).collect(Collectors.toCollection(
TreeSet::new));•
// Convert elements to strings and concatenate them, separated by
commasString
joined =
things.stream().map(Object::toString).collect(Collectors.joining(",
"));
// Compute sum of salaries of
employeeint total =
employees.stream().collect(Collectors.summingInt(Employee::getSalary)
));
// Group employees by
departmentMap
employees.stream().collect(Collectors.groupingBy(Employee::getDepartm
ent));
// Compute sum of salaries by
departmentMap
employees.stream().collect(Collectors.groupingBy(Employee::getDepartm
ent, Collectors.summingInt(Employee::getSalary)));
// Partition students into passing and
1.1定义示例数据
先定义待操作对象,一个万能的Student 类(用到了 lombok):
然后定义一组测试数据:
1.2数据统计
这个比较简单,就是统计聚合结果的元素数量:
1.3平均值:averagingDouble、averagingInt、averagingLong
这几个方法是计算聚合元素的平均值,区别是输入参数需要是对应的类型。
failingMap
students.stream().collect(Collectors.partitioningBy(s ->
s.getGrade() >= PASS_THRESHOLD));
@Data
@AllArgsConstructorpublic
class Student {
private String id;
private String name;
private LocalDate birthday;
private int age;
private double score;}
final List
students.add(new Student("1", "张三", LocalDate.of(2009, Month.JANUARY,
1), 12, 12.123));
students.add(new Student("2", "李四", LocalDate.of(2010,
Month.FEBRUARY, 2), 11, 22.123));
students.add(new Student("3", "王五", LocalDate.of(2011, Month.MARCH,
3), 10, 32.123));
// 3
students.stream().collect(Collectors.counting())
比如,求学生的分数平均值,因为分数是double 类型,所以在不转类型的情况下,需要使
用averagingDouble :
如果考虑转换精度,也是可以实现:
如果是求学生的平均年龄,因为年龄是int 类型,就可以随意使用任何一个函数了:
注意:这三个方法的返回值都是Double 类型。
1.4和:summingDouble、summingInt、summingLong
这三个方法和上面的平均值方法类似,也是需要注意元素的类型,在需要类型转换时,需要强制
转换:1
但是对于不需要强制转换的类型,可以随意使用任何一个函数:
// 22.123
students.stream().collect(Collectors.averagingDouble(Student::getScore)
)
// 22.0
students.stream().collect(Collectors.averagingInt(s ->
(int)s.getScore()))
// 22.0
students.stream().collect(Collectors.averagingLong(s ->
(long)s.getScore()))
// 11.0
students.stream().collect(Collectors.averagingInt(Student::getAge))
// 11.0
students.stream().collect(Collectors.averagingDouble(Student::getAge))
// 11.0
students.stream().collect(Collectors.averagingLong(Student::getAge))
// 66
students.stream().collect(Collectors.summingInt(s ->
(int)s.getScore()))
// 66.369
students.stream().collect(Collectors.summingDouble(Student::getScore))
// 66
students.stream().collect(Collectors.summingLong(s ->
(long)s.getScore()))
// 33
注意:这三个方法返回值和平均值的三个方法不一样,summingDouble 返回的是Doubl
e 类型、summingInt 返回的是Integer 类型,summingLong 返回的是Long 类
型。
1.5最大值/最小值元素:maxBy、minBy
顾名思义,这两个函数就是求聚合元素中指定比较器中的最大/最小元素。比如,求年龄最大/最
小的Student 对象:
从源码可以看出来,这两个方法算是作者给的福利,用于完善数据统计的结果。内部都是封装
了reducing 方法和BinaryOperator 工具类,这些下面会讲到。
2聚合、分组
2.1聚合元素:toList、toSet、toCollection
这几个函数比较简单,是将聚合之后的元素,重新封装到队列中,然后返回。比如,得到所有S
tudent 的 ID 列表,只需要根据需要的结果类型使用不同的方法即可:
students.stream().collect(Collectors.summingInt(Student::getAge))
// 33.0
students.stream().collect(Collectors.summingDouble(Student::getAge)
// 33
students stream() collect(Collectors summingLong(Student::getAge))
//Optional[Student(id=3, name=王五, birthday=2011-03-03, age=10,
score=32.123)],注意返回类型是Optional
students.stream().collect(Collectors.minBy(Comparator.comparing(Student
::getAge))
// Optional[Student(id=1, name=张三, birthday=2009-01-01, age=12,
score=12.123)],
注意返回类型是
Optionalstudents.stream().collect(Collectors.maxBy(Comparator.comparing
(Student::getAge)))
public static
super T> comparator) { return
reducing(BinaryOperator.maxBy(comparator));
}
public static
super T> comparator) { return
reducing(BinaryOperator.minBy(comparator));
}
// List: [1, 2, 3]
注意:toList 方法返回的是List 子类,toSet 返回的是Set 子类,toCollectio
n 返回的是Collection 子类。我们都知道,Collection 的子类包括List 、Se
t 等众多子类,所以toCollection 更加灵活。
2.2聚合元素:toMap、toConcurrentMap
这两个方法的作用是将聚合元素,重新组装为Map 结构,也就是 k-v 结构。两者用法一样,区
别是toMap 返回的是Map ,toConcurrentMap 返回ConcurrentMap ,也就是说,t
oConcurrentMap 返回的是线程安全的 Map 结构。
比如,我们需要聚合Student 的 id:
但是,如果 id 有重复的,会抛出java.lang.IllegalStateException: Duplicate ke
y 异常,所以,为了保险起见,我们需要借助toMap 另一个重载方法:
可以看到,toMap 有不同的重载方法,可以实现比较复杂的逻辑。比如,我们需要得到根据 id
分组的Student 的姓名:
final List
students.stream().map(Student::getId).collect(Collectors.toList());
// Set: [1, 2, 3]
final Set
students.stream().map(Student::getId).collect(Collectors.toSet());
//TreeSet: [1, 2, 3]
final Collection
students.stream().map(Student::getId).collect(Collectors.toCollection(T
reeSet::new));
// {1=Student(id=1, name=张三, birthday=2009-01-01, age=12,
score=12.123), 2=Student(id=2, name=李四, birthday=2010-02-02, age=11,
score=22.123), 3=Student(id=3, name=王五, birthday=2011-03-03, age=10,
score=32.123)}
final Map
.collect(Collectors.toMap(Student::getId, Function.identity()));
// {1=Student(id=1, name=张三, birthday=2009-01-01, age=12,
score=12.123), 2=Student(id=2, name=李四, birthday=2010-02-02, age=11,
score=22.123), 3=Student(id=3, name=王五, birthday=2011-03-03, age=10,
score=32.123)}
final Map
.collect(Collectors.toMap(Student::getId, Function.identity(), (x, y) -
> x));
// {1=张三, 2=李四, 3=王五}
final Map
.collect(Collectors.toMap(Student::getId, Student::getName, (x, y) ->
比如,我们需要得到相同年龄得分最高的Student 对象集合:
所以,toMap 可玩性很高。
2.3分组:groupingBy、groupingByConcurrent
groupingBy 与toMap 都是将聚合元素进行分组,区别是,toMap 结果是 1:1 的 k-v 结
构,groupingBy 的结果是 1:n 的 k-v 结构。
比如,我们对Student 的年龄分组:
既然groupingBy 也是分组,是不是也能够实现与toMap 类似的功能,比如,根据 id 分组
的Student :
x));
// {10=Student(id=3, name=王五, birthday=2011-03-03, age=10,
score=32.123), 11=Student(id=2, name=李四, birthday=2010-02-02, age=11,
score=22.123), 12=Student(id=1, name=张三, birthday=2009-01-01, age=12,
score=12.123)}
final Map
.collect(Collectors.toMap(Student::getAge, Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(Student::getScore))));
// List: {10=[Student(id=3, name=王五, birthday=2011-03-03, age=10,
score=32.123)], 11=[Student(id=2, name=李四, birthday=2010-02-02,
age=11, score=22.123)], 12=[Student(id=1, name=张三, birthday=2009-01-
01, age=12, score=12.123)]}
final Map
students.stream().collect(Collectors.groupingBy(Student::getAge));//
Set: {10=[Student(id=3, name=王五, birthday=2011-03-03, age=10,
score=32.123)], 11=[Student(id=2, name=李四, birthday=2010-02-02,
age=11, score=22.123)], 12=[Student(id=1, name=张三, birthday=2009-01-
01, age=12, score=12.123)]}
final Map
students.stream().collect(Collectors.groupingBy(Student::getAge,
Collectors.toSet()));
// {1=Student(id=1, name=张三, birthday=2009-01-01, age=12,
score=12.123), 2=Student(id=2, name=李四, birthday=2010-02-02, age=11,
score=22.123), 3=Student(id=3, name=王五, birthday=2011-03-03, age=10,
score=32.123)}
final Map
.collect(Collectors.groupingBy(Student::getId,
Collectors.collectingAndThen(Collectors.toList(), list ->
list.get(0))));
为了对比,把toMap 的写法放在这:
如果想要线程安全的Map ,可以使用groupingByConcurrent 。
2.4分组:partitioningBy
partitioningBy 与groupingBy 的区别在于,partitioningBy 借助Predicat
e 断言,可以将集合元素分为true 和false 两部分。比如,按照年龄是否大于 11 分组:
##
3统计结果:summarizingDouble、summarizingInt、summarizingLong
既然是数据操作,基本上逃不出计数、求平局、求和、最大、最小这几个,所以作者也是很贴心
的实现了一组聚合的数据统计方法。
这组方法与求和、求平均的方法类似,都需要注意方法类型。比如,按照分数统计的话,需要进
行类型转换:
// {1=Student(id=1, name=张三, birthday=2009-01-01, age=12,
score=12.123), 2=Student(id=2, name=李四, birthday=2010-02-02, age=11,
score=22.123), 3=Student(id=3, name=王五, birthday=2011-03-03, age=10,
score=32.123)}
final Map
.collect(Collectors.toMap(Student::getId, Function.identity(), (x, y) -
> x));
// List: {false=[Student(id=2, name=李四, birthday=2010-02-02, age=11,
score=22.123), Student(id=3, name=王五, birthday=2011-03-03, age=10,
score=32.123)], true=[Student(id=1, name=张三, birthday=2009-01-01,
age=12, score=12.123)]}
final Map
students.stream().collect(Collectors.partitioningBy(s -> s.getAge() >
11));
// Set: {false=[Student(id=3, name=王五, birthday=2011-03-03, age=10,
score=32.123), Student(id=2, name=李四, birthday=2010-02-02, age=11,
score=22.123)], true=[Student(id=1, name=张三, birthday=2009-01-01,
age=12, score=12.123)]}
final Map
students.stream().collect(Collectors.partitioningBy(s -> s.getAge() >
11, Collectors.toSet()));
// IntSummaryStatistics{count=3, sum=66, min=12, average=22.000000,
max=32}
students.stream().collect(Collectors.summarizingInt(s -> (int)
s.getScore()))
如果是用年龄统计的话,三个方法通用:
注意:这三个方法返回值不一样,summarizingDouble 返回DoubleSummaryStatist
ics 类型,summarizingInt 返回IntSummaryStatistics 类型,summarizingLo
ng 返回LongSummaryStatistics 类型。
4链接数据:joining
这个方法对String 类型的元素进行聚合,拼接成一个字符串返回,作用与java.lang.Str
ing#join 类似,提供了 3 个不同重载方法,可以实现不同的需要。比如:
5操作链:collectingAndThen
这个方法在groupingBy 的例子中出现过,它是先对集合进行一次聚合操作,然后通过Func
tion 定义的函数,对聚合后的结果再次处理。
比如groupingBy 中的例子:
// DoubleSummaryStatistics{count=3, sum=66.369000, min=12.123000,
average=22.123000, max=32.123000}
students.stream().collect(Collectors.summarizingDouble(Student::getScor
e))
// LongSummaryStatistics{count=3, sum=66, min=12, average=22.000000,
max=32}
students.stream().collect(Collectors.summarizingLong(s -> (long)
// IntSummaryStatistics{count=3, sum=33, min=10, average=11.000000,
max=12}
students.stream().collect(Collectors.summarizingInt(Student::getAge))
// DoubleSummaryStatistics{count=3, sum=33.000000, min=10.000000,
average=11.000000, max=12.000000}
students.stream().collect(Collectors.summarizingDouble(Student::getAge)
)
// LongSummaryStatistics{count=3, sum=33, min=10, average=11.000000,
max=12}
students.stream().collect(Collectors.summarizingLong(Student::getAge))
// javagosql
Stream.of("java", "go", "sql").collect(Collectors.joining());
// java, go, sql
Stream.of("java", "go", "sql").collect(Collectors.joining(", "));
// 【java, go, sql】
Stream.of("java", "go", "sql").collect(Collectors.joining(", ", "【",
"】"));
显示将结果聚合成List 列表,然后取列表的第 0 个元素返回,通过这种方式,实现 1:1 的 ma
p 结构。
再来一个复杂一些的,找到聚合元素中年龄数据正确的Student 列表:
这个例子纯粹是为了使用collectingAndThen 的用法,其实可以简化为:
6操作后聚合:mapping
mapping 先通过Function 函数处理数据,然后通过Collector 方法聚合元素。比如,
获取获取students 的姓名列表:
这种计算与java.util.stream.Stream#map 方式类似:
从这点上看,还是通过java.util.stream.Stream#map 更清晰一些。
// {1=Student(id=1, name=张三, birthday=2009-01-01, age=12,
score=12.123), 2=Student(id=2, name=李四, birthday=2010-02-02, age=11,
score=22.123), 3=Student(id=3, name=王五, birthday=2011-03-03, age=10,
score=32.123)}
final Map
.collect(Collectors.groupingBy(Student::getId,
Collectors.collectingAndThen(Collectors.toList(), list ->
list.get(0))));
// [],结果为空,是因为例子中所有人的年龄都是对的
students.stream() .collect(
Collectors.collectingAndThen(Collectors.toList(), (list ->
list.stream()
.filter(s -> (LocalDate.now().getYear() - s.getBirthday().getYear())
!=s.getAge()).collect(Collectors.toList()))));
students.stream()
.filter(s -> (LocalDate.now().getYear() -
s.getBirthday().getYear()) != s.getAge())
.collect(Collectors.toList());
// [张三, 李四, 王五]
students.stream()
.collect(Collectors.mapping(Student::getName, Collectors.toList()));
// [张三, 李四, 王五]
students.stream()
.map(Student::getName)
.collect(Collectors.toList());
7聚合后操作:reducing
reducing 提供了 3 个重载方法:
public static
ator
public static
erator
public static
ion mapper, BinaryOperator op) :预定默认
值,通过Function 操作元素,然后通过BinaryOperator 操作
比如,计算所有students 的得分总数:
同mapping ,reducing 的操作与java.util.stream.Stream#reduce 方式类似:
在上文说到maxBy 和minBy 时,提到这两个函数就是通过reducing 实现的。
对于mapping 和reducing ,可以参考函数式编程中 map-reduce 的概念。
8: 并行流(Parallel Stream)
它可以通过 stream().parallel() 方法来让 Stream 操作并行化。然而,并行流并不总是
能带来性能提升:
// Optional[66.369],注意返回类型是Optional
students.stream()
.map(Student::getScore)
.collect(Collectors.reducing(Double::sum));
// 66.369
students.stream()
.map(Student::getScore)
.collect(Collectors.reducing(0.0, Double::sum));
// 66.369
students.stream()
.collect(Collectors.reducing(0.0, Student::getScore, Double::sum));
// Optional[66.369],注意返回类型是Optional
students.stream().map(Student::getScore).reduce(Double::sum);
// 66.369
students.stream().map(Student::getScore).reduce(0.0, Double::sum);
// 生成一个 0~999999 的数字列表
List
1000000).boxed().collect(Collectors.toList());
// 直接使用并行流
并行流的适用场景是计算量较大、数据量足够多的情况下。如果数据量较小,或者 Stream 操作
较简单,使用并行流反而会带来线程切换的开销,导致性能下降。
执
2.4 limit() 和 skip() 的误用
limit() 和 skip() 可以限制 Stream 的数据量,但要注意它们的相对位置。如果在 fil
ter() 之后使用 limit() ,可能会带来不必要的性能消耗:
这种情况下,filter() 会对 1,000,000 个元素逐个过滤,直到找到前 10 个符合条件的元
素。更高效的方式是先 limit() ,再进行其他操作:
这样,Stream 只会处理有限的元素,性能会更好。
3. Stream API 性能优化技巧
3.1 使用 toArray() 而不是 collect(Collectors.toList())
long start1 = System.currentTimeMillis();
long sum =
numbers.parallelStream().mapToInt(Integer::intValue).sum();long end1 =
System.currentTimeMillis();System.out.println("并行流执行时间:" + (end1
// 使用普通流
long start2 = System.currentTimeMillis();
long sum2 = numbers.stream().mapToInt(Integer::intValue).sum();
long end2 = System.currentTimeMillis();
System.out.println("普通流执行时间:" + (end2 - start2) +
"ms");System.out.println(sum2);
并行流执行时间:30mssum: 1783293664普通流执行时间:16mssum2: 783293664
List
1_000_000).boxed().collect(Collectors.toList());
// 过滤偶数,然后取前 10 个
List
.filter(n -> n % 2 == 0)
.limit(10)
.collect(Collectors.toList());
List
.limit(20)
// 先取出前 20 个
.filter(n -> n % 2 == 0)
// 再进行过滤
.collect(Collectors.toList());
如果我们只需要将 Stream 转换为数组,使用 toArray() 是更快的选择:
相比 collect(Collectors.toList()) ,toArray() 在实现上更直接,尤其在处理大
量数据时可以减少内存分配的开销。
collect(Collectors.toList()) :这个方法首先创建一个 ArrayList ,然后将所
有元素添加到这个列表中。在这个过程中,ArrayList 可能会经历多次扩容,每次扩容都
需要新建一个更大的数组,并将现有元素复制到新数组中。这种重复的内存分配和数组复制操
作在处理大量数据时会增加开销。
toArray() :这个方法直接生成一个数组,避免了 ArrayList 的扩容过程。
3.2 避免不必要的装箱与拆箱
在处理基本数据类型时,使用 mapToInt() 、mapToDouble() 这样的基本类型专用方
法,可以避免不必要的装箱和拆箱操作,提高性能:
果直接使用 map() 会导致频繁的装箱和拆箱,降低性能。
3.3 尽量使用 forEachOrdered()
String[] array = names.stream().toArray(String[]::new);
List
10000000).boxed().collect(Collectors.toList());long start1 =
System.currentTimeMillis();
// 使用 map 导致装箱和拆箱
int sumWithMap = numbers.stream()
.map(n -> n)
// 装箱
.reduce(0, Integer::sum); /
/ 拆箱
long end1 = System.currentTimeMillis();
System.out.println("sumWithMap: " + sumWithMap + " time: " + (end1 -
start1));
long start2 = System.currentTimeMillis();
// 使用 mapToInt 避免装箱和拆箱
int sumWithMapToInt = numbers.stream()
.mapToInt(n -> n).sum();
// 直接处理基本类型
long end2 =
System.currentTimeMillis();System.out.println("sumWithMapToInt: " +
sumWithMapToInt + " time: " + (end2 - start2));
sumWithMap:-2014260032
time: 125
sumWithMapToInt:-2014260032
time: 18
在并行流中,forEach() 的执行顺序是非确定性的,如果我们希望按原来的顺序处理数据,
使用 forEachOrdered() 可以保证顺序,但会稍微影响性能。
3.4 减少链式调用中的中间操作
每个中间操作都会产生一个新的 Stream 实例,如果链式调用过多,会增加调用栈的深度,影响
性能。尽量合并中间操作来减少链条长度:
通过合并 filter 的条件,可以减少 Stream 的中间操作,提升性能。
4. 从 Java 8 到 Java 17 的改进
Java 9 到 Java 17 中,Stream API 进行了多次优化和功能增强:
Java 9 引入了 takeWhile() 和 dropWhile() 方法,这些方法允许我们基于条件对
Stream 进行分割,性能上比过滤操作更高效。
Java 10 开始,Collectors.toUnmodifiableList() 提供了一种方法来创建不可修
改的集合,适用于需要更严格集合控制的场景。
Java 16 增加了对 Stream.toList() 的支持,方便直接将流转换为不可变的 List :
Java 17 进一步优化了 Stream 的性能,特别是在并行流的实现上,使其在多核环境下能够
更高效地利用硬件资源。
5. Java并行流常见Bug及潜在风险分析
numbers.parallelStream().forEachOrdered(System.out::println);
// 原始链式调用
List
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
// 优化后的调用
List
names.stream().map(String::toUpperCase).filter(name -> name.length() >
3 && name.startsWith("A")).map(String::toUpperCase)
.collect(Collectors.toList());
List
.takeWhile(n -> n < 100)
.collect(Collectors.toList());
List
3).toList();
Java并行流(Parallel Streams)虽然简化了多线程编程,但其底层复杂的并行机制也带来了多种
潜在问题。以下从线程安全、顺序一致性、性能陷阱、操作限制等角度全面解析常见Bug,并结
合实际案例和底层原理进行说明。
一、线程安全问题
并行流通过多线程处理数据,但开发者容易忽略共享资源的线程安全性,导致数据竞争或状态不
一致。
非线程安全集合操作
在并行流中直接操作ArrayList 、HashMap 等非线程安全集合时,多个线程可能同时修改
内部结构,导致元素丢失或出现null 值。
可能会导致元素的覆盖和丢失
问题根源:ArrayList 的add 方法非原子性,多个线程可能竞争同一数组索引。
6. 调试方法debug方法
List
ArrayList<>());IntStream.range(0, 1000).parallel() .filter(i -> i % 2
== 0) .forEach(parallelStorage::add);
// 结果可能小于5050000000
//执行5次结果:
1277494417 1503961629 2145550532 1948586900
1176609439
本文整理自实践经验,如有问题欢迎交流讨论。