Administrator
发布于 2022-08-23 / 0 阅读
0
0

Java Stream 中 Collectors 的 24 个操作-202509120149

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 list =

people.stream().map(Person::getName).collect(Collectors.toList());

// Accumulate names into a

TreeSetSet set =

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> byDept =

employees.stream().collect(Collectors.groupingBy(Employee::getDepartm

ent));

// Compute sum of salaries by

departmentMap totalByDept =

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> passingFailing =

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 = Lists.newArrayList();

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 Collector> maxBy(Comparator

super T> comparator) {    return

reducing(BinaryOperator.maxBy(comparator));

}

public static Collector> minBy(Comparator

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 idList =

students.stream().map(Student::getId).collect(Collectors.toList());

// Set: [1, 2, 3]

final Set idSet =

students.stream().map(Student::getId).collect(Collectors.toSet());

//TreeSet: [1, 2, 3]

final Collection idTreeSet =

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 map11 = students.stream()

.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 map2 = students.stream()

.collect(Collectors.toMap(Student::getId, Function.identity(), (x, y) -

> x));

// {1=张三, 2=李四, 3=王五}

final Map map3 = students.stream()

.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 map5 = students.stream()

.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> map1 =

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> map12 =

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 map3 = students.stream()

.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 map2 = students.stream()

.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> map6 =

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> map7 =

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 map3 = students.stream()

.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 Collector> reducing(BinaryOper

ator op) :直接通过BinaryOperator 操作,返回值是Optional 

public static Collector reducing(T identity, BinaryOp

erator op) :预定默认值,然后通过BinaryOperator 操作

public static Collector reducing(U identity, Funct

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 numbers = IntStream.range(0,

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

  • start1) + "ms");System.out.println(sum);
  • // 使用普通流

    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 numbers = IntStream.range(0,

    1_000_000).boxed().collect(Collectors.toList());

    // 过滤偶数,然后取前 10 个

    List result = numbers.stream()

    .filter(n -> n % 2 == 0)

    .limit(10)

    .collect(Collectors.toList());

    List result = numbers.stream()

    .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 numbers = IntStream.range(0,

    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 result = names.stream()

    .filter(name -> name.length() > 3)

    .map(String::toUpperCase)

    .filter(name -> name.startsWith("A"))

    .collect(Collectors.toList());

    // 优化后的调用

    List resultOptimized =

    names.stream().map(String::toUpperCase).filter(name -> name.length() >

    3 && name.startsWith("A")).map(String::toUpperCase)

    .collect(Collectors.toList());

    List limitedNumbers = numbers.stream()

    .takeWhile(n -> n < 100)

    .collect(Collectors.toList());

    List immutableList = names.stream().filter(n -> n.length() >

    3).toList();

    Java并行流(Parallel Streams)虽然简化了多线程编程,但其底层复杂的并行机制也带来了多种

    潜在问题。以下从线程安全、顺序一致性、性能陷阱、操作限制等角度全面解析常见Bug,并结

    合实际案例和底层原理进行说明。

    一、线程安全问题

    并行流通过多线程处理数据,但开发者容易忽略共享资源的线程安全性,导致数据竞争或状态不

    一致。

    非线程安全集合操作

    在并行流中直接操作ArrayList 、HashMap 等非线程安全集合时,多个线程可能同时修改

    内部结构,导致元素丢失或出现null 值。

    可能会导致元素的覆盖和丢失

    问题根源:ArrayList 的add 方法非原子性,多个线程可能竞争同一数组索引。

    6. 调试方法debug方法

    List parallelStorage = Collections.synchronizedList(new

    ArrayList<>());IntStream.range(0, 1000).parallel() .filter(i -> i % 2

    == 0)       .forEach(parallelStorage::add);

    // 结果可能小于5050000000

    //执行5次结果:

    1277494417       1503961629        2145550532        1948586900

    1176609439


    本文整理自实践经验,如有问题欢迎交流讨论。


    评论