Java Stream collect 方法进行数据统计
Java Stream collect 方法进行数据统计
在Java编程中,Stream
API 是一个强大的工具,它为处理集合数据提供了一种高效且简洁的方式。collect
方法作为 Stream
API 中的关键操作之一,在数据统计方面发挥着至关重要的作用。它允许我们将流中的元素累积到一个可变的结果容器中,例如集合、映射等,同时还可以执行各种统计操作。
1. collect 方法概述
collect
方法是 Stream
接口中的一个终端操作,这意味着在调用该方法后,流就会被消费,并且不能再被操作。它的主要作用是将流中的元素收集到一个具体的结果容器中,这个结果容器可以是 List
、Set
、Map
等集合类型,也可以是自定义的容器。
collect
方法有多个重载版本,其中最常用的有以下两种:
1.1 collect(Collector collector)
这个版本接受一个 Collector
接口的实现作为参数。Collector
是一个复杂的接口,它定义了如何将流中的元素累积到结果容器中,以及如何对累积的结果进行最终的转换和合并。Java 8 提供了许多预定义的 Collector
实现,例如 Collectors
类中定义的各种静态方法返回的 Collector
,这些预定义的 Collector
可以满足大多数常见的数据统计和收集需求。
1.2 collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)
这个版本使用三个函数式接口来定义收集过程。Supplier
用于创建结果容器,BiConsumer
用于将流中的元素累积到结果容器中,另一个 BiConsumer
用于合并多个结果容器(在并行流的情况下会用到)。这种形式更加灵活,允许我们自定义收集逻辑,而不需要实现完整的 Collector
接口。
2. 使用 Collectors 类进行基本数据统计
2.1 收集到 List
最常见的操作之一是将流中的元素收集到一个 List
中。Collectors.toList()
方法返回一个 Collector
,它可以将流中的元素收集到一个 ArrayList
中。以下是一个简单的示例:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamCollectToListExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> collectedList = numbers.stream()
.collect(Collectors.toList());
System.out.println(collectedList);
}
}
在上述代码中,我们首先创建了一个包含整数的 List
。然后,通过 stream()
方法将其转换为流,并使用 collect(Collectors.toList())
方法将流中的元素收集回一个新的 List
中。最终输出的 collectedList
与原始的 numbers
列表内容相同。
2.2 收集到 Set
如果我们希望将流中的元素收集到一个 Set
中,以去除重复元素,可以使用 Collectors.toSet()
方法。以下是示例代码:
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
public class StreamCollectToSetExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
Set<Integer> collectedSet = numbers.stream()
.collect(Collectors.toSet());
System.out.println(collectedSet);
}
}
在这个例子中,原始列表 numbers
包含重复的元素。通过 collect(Collectors.toSet())
方法,流中的元素被收集到一个 Set
中,重复元素被自动去除,最终输出的 collectedSet
中只包含不重复的元素。
2.3 收集到 Map
Collectors.toMap()
方法可以将流中的元素收集到一个 Map
中。这个方法需要两个参数,一个用于指定 Map
的键,另一个用于指定 Map
的值。以下是一个示例,将一个包含学生姓名和成绩的列表收集到一个 Map
中,以学生姓名作为键,成绩作为值:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
}
public class StreamCollectToMapExample {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("Alice", 85));
students.add(new Student("Bob", 90));
students.add(new Student("Charlie", 78));
Map<String, Integer> studentScoreMap = students.stream()
.collect(Collectors.toMap(Student::getName, Student::getScore));
System.out.println(studentScoreMap);
}
}
在上述代码中,我们定义了一个 Student
类,然后创建了一个包含多个 Student
对象的列表。通过 collect(Collectors.toMap(Student::getName, Student::getScore))
方法,将学生的姓名作为键,成绩作为值收集到一个 Map
中。
2.4 计数
Collectors.counting()
方法返回一个 Collector
,用于统计流中元素的数量。以下是一个简单的示例:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamCountingExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Long count = numbers.stream()
.collect(Collectors.counting());
System.out.println("元素数量: " + count);
}
}
在这个例子中,通过 collect(Collectors.counting())
方法统计了 numbers
列表中的元素数量,并将结果打印出来。
2.5 求和
对于数值类型的流,我们可以使用 Collectors.summingInt()
、Collectors.summingLong()
和 Collectors.summingDouble()
方法来计算流中元素的总和。以下以 Collectors.summingInt()
为例:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamSummingExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.collect(Collectors.summingInt(Integer::intValue));
System.out.println("总和: " + sum);
}
}
在上述代码中,通过 collect(Collectors.summingInt(Integer::intValue))
方法计算了 numbers
列表中所有整数的总和。
2.6 求平均值
Collectors.averagingInt()
、Collectors.averagingLong()
和 Collectors.averagingDouble()
方法可以用于计算流中数值类型元素的平均值。以下是使用 Collectors.averagingInt()
的示例:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamAveragingExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Double average = numbers.stream()
.collect(Collectors.averagingInt(Integer::intValue));
System.out.println("平均值: " + average);
}
}
这里通过 collect(Collectors.averagingInt(Integer::intValue))
方法计算了 numbers
列表中整数的平均值,并将结果打印出来。
2.7 求最大值和最小值
Collectors.maxBy()
和 Collectors.minBy()
方法可以用于找出流中的最大值和最小值。这两个方法都接受一个 Comparator
作为参数,用于定义比较规则。以下是找出整数列表中最大值的示例:
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class StreamMaxMinExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> max = numbers.stream()
.collect(Collectors.maxBy(Comparator.naturalOrder()));
max.ifPresent(System.out::println);
}
}
在上述代码中,Collectors.maxBy(Comparator.naturalOrder())
方法使用自然顺序的比较器来找出流中的最大值。由于结果可能为空(如果流为空),所以返回的是一个 Optional
对象,我们通过 ifPresent
方法来处理可能存在的值。
3. 复杂数据统计与分组
3.1 分组
Collectors.groupingBy()
方法可以根据某个属性对流中的元素进行分组。它返回一个 Collector
,将流中的元素按照指定的分类函数进行分组,结果是一个 Map
,其中键是分类函数的返回值,值是属于该组的元素列表。以下是一个将学生按成绩分组的示例:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
}
public class StreamGroupingByExample {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("Alice", 85));
students.add(new Student("Bob", 90));
students.add(new Student("Charlie", 78));
students.add(new Student("David", 85));
Map<Integer, List<Student>> scoreGroupMap = students.stream()
.collect(Collectors.groupingBy(Student::getScore));
System.out.println(scoreGroupMap);
}
}
在这个例子中,通过 collect(Collectors.groupingBy(Student::getScore))
方法,将学生按成绩分组,成绩相同的学生被分到同一组,最终结果是一个 Map
,键是成绩,值是该成绩对应的学生列表。
3.2 多级分组
Collectors.groupingBy()
方法还支持多级分组。我们可以在一级分组的基础上,再进行二级分组。以下是一个将学生先按成绩分组,再按姓名首字母分组的示例:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
}
public class StreamMultiLevelGroupingByExample {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("Alice", 85));
students.add(new Student("Bob", 90));
students.add(new Student("Charlie", 78));
students.add(new Student("David", 85));
Map<Integer, Map<Character, List<Student>>> multiLevelGroupMap = students.stream()
.collect(Collectors.groupingBy(Student::getScore,
Collectors.groupingBy(s -> s.getName().charAt(0))));
System.out.println(multiLevelGroupMap);
}
}
在上述代码中,Collectors.groupingBy(Student::getScore, Collectors.groupingBy(s -> s.getName().charAt(0)))
实现了多级分组。首先按成绩分组,然后在每个成绩组内再按姓名首字母分组,最终结果是一个嵌套的 Map
。
3.3 分区
Collectors.partitioningBy()
方法用于将流中的元素根据一个布尔条件进行分区,结果是一个 Map
,其中键是 true
或 false
,值是满足或不满足条件的元素列表。以下是一个将学生按成绩是否及格分区的示例:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
}
public class StreamPartitioningByExample {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("Alice", 85));
students.add(new Student("Bob", 90));
students.add(new Student("Charlie", 78));
students.add(new Student("David", 55));
Map<Boolean, List<Student>> partitionMap = students.stream()
.collect(Collectors.partitioningBy(s -> s.getScore() >= 60));
System.out.println("及格学生: " + partitionMap.get(true));
System.out.println("不及格学生: " + partitionMap.get(false));
}
}
在这个例子中,通过 collect(Collectors.partitioningBy(s -> s.getScore() >= 60))
方法,将学生按成绩是否及格进行分区,最终结果是一个 Map
,可以通过 true
和 false
键分别获取及格和不及格的学生列表。
3.4 对分组结果进行统计
在分组后,我们通常还需要对每个组内的数据进行进一步的统计。例如,计算每个成绩组内学生的平均成绩。我们可以使用 Collectors.collectingAndThen()
方法,它接受一个 Collector
和一个转换函数,先使用 Collector
进行收集,然后再对收集的结果应用转换函数。以下是示例代码:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
}
public class StreamGroupingAndStatisticsExample {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("Alice", 85));
students.add(new Student("Bob", 90));
students.add(new Student("Charlie", 78));
students.add(new Student("David", 85));
Map<Integer, Double> averageScoreByGroup = students.stream()
.collect(Collectors.groupingBy(Student::getScore,
Collectors.collectingAndThen(Collectors.averagingInt(Student::getScore),
Double::valueOf)));
System.out.println(averageScoreByGroup);
}
}
在上述代码中,Collectors.groupingBy(Student::getScore, Collectors.collectingAndThen(Collectors.averagingInt(Student::getScore), Double::valueOf))
先按成绩分组,然后对每个成绩组内的学生计算平均成绩,并将结果收集到一个 Map
中,键是成绩,值是该成绩组的平均成绩。
4. 使用自定义 Collector 进行数据统计
虽然 Collectors
类提供了许多预定义的 Collector
,但在某些情况下,我们可能需要自定义 Collector
来满足特定的数据统计需求。要自定义 Collector
,我们需要实现 Collector
接口,该接口包含以下几个方法:
supplier()
:返回一个Supplier
,用于创建结果容器。accumulator()
:返回一个BiConsumer
,用于将流中的元素累积到结果容器中。combiner()
:返回一个BiConsumer
,用于合并多个结果容器(在并行流的情况下会用到)。finisher()
:返回一个Function
,用于对最终的结果容器进行转换(可选,通常在不需要转换时返回Function.identity()
)。characteristics()
:返回一个Set
,包含Collector
的特征,例如Collector.Characteristics.IDENTITY_FINISH
表示finisher
方法返回Function.identity()
,Collector.Characteristics.CONCURRENT
表示Collector
支持并行收集,Collector.Characteristics.UNORDERED
表示收集过程不依赖元素的顺序。
以下是一个自定义 Collector
来计算流中整数平方和的示例:
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
public class CustomCollectorExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sumOfSquares = numbers.stream()
.collect(new SquareSumCollector());
System.out.println("平方和: " + sumOfSquares);
}
}
class SquareSumCollector implements Collector<Integer, Integer, Integer> {
@Override
public Supplier<Integer> supplier() {
return () -> 0;
}
@Override
public BiConsumer<Integer, Integer> accumulator() {
return (sum, num) -> sum += num * num;
}
@Override
public BinaryOperator<Integer> combiner() {
return Integer::sum;
}
@Override
public Function<Integer, Integer> finisher() {
return Function.identity();
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
}
}
在上述代码中,我们定义了一个 SquareSumCollector
类,实现了 Collector
接口。supplier()
方法返回一个初始值为 0 的 Supplier
,用于创建结果容器。accumulator()
方法将每个元素的平方累加到结果容器中。combiner()
方法用于合并多个结果容器,这里简单地使用 Integer::sum
进行合并。finisher()
方法返回 Function.identity()
,因为不需要对最终结果进行额外转换。characteristics()
方法指定了 Collector
的特征,这里只包含 IDENTITY_FINISH
。
5. 并行流与 collect 方法
当处理大规模数据时,使用并行流可以显著提高数据处理的效率。Stream
API 支持将流转换为并行流,通过调用 parallel()
方法即可。在并行流中使用 collect
方法时,Collector
的 combiner()
方法会被用于合并多个线程处理的结果。
以下是一个使用并行流计算整数列表平方和的示例,与前面自定义 Collector
的示例类似,但使用并行流:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ParallelStreamCollectExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sumOfSquares = numbers.parallelStream()
.collect(() -> 0,
(sum, num) -> sum += num * num,
(sum1, sum2) -> sum1 + sum2);
System.out.println("平方和: " + sumOfSquares);
}
}
在这个例子中,numbers.parallelStream()
将列表转换为并行流。collect
方法使用了三个参数的版本,() -> 0
是 Supplier
,用于创建初始结果值;(sum, num) -> sum += num * num
是 BiConsumer
,用于将元素的平方累加到结果中;(sum1, sum2) -> sum1 + sum2
是另一个 BiConsumer
,用于合并多个线程的结果。
需要注意的是,并行流并不总是能提高性能,特别是在数据量较小或者 Collector
的合并操作开销较大时。在实际应用中,需要根据具体情况进行测试和优化,以确定是否使用并行流以及如何选择合适的 Collector
。
综上所述,Java Stream
的 collect
方法是一个功能强大且灵活的数据统计工具。通过合理使用 Collectors
类提供的预定义 Collector
以及自定义 Collector
,我们可以高效地对集合数据进行各种复杂的统计和收集操作。同时,在处理大规模数据时,结合并行流可以进一步提升性能。熟练掌握 collect
方法的使用,对于编写高效、简洁的 Java 数据处理代码至关重要。