MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java Stream collect 方法进行数据统计

2022-02-027.8k 阅读

Java Stream collect 方法进行数据统计

在Java编程中,Stream API 是一个强大的工具,它为处理集合数据提供了一种高效且简洁的方式。collect 方法作为 Stream API 中的关键操作之一,在数据统计方面发挥着至关重要的作用。它允许我们将流中的元素累积到一个可变的结果容器中,例如集合、映射等,同时还可以执行各种统计操作。

1. collect 方法概述

collect 方法是 Stream 接口中的一个终端操作,这意味着在调用该方法后,流就会被消费,并且不能再被操作。它的主要作用是将流中的元素收集到一个具体的结果容器中,这个结果容器可以是 ListSetMap 等集合类型,也可以是自定义的容器。

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,其中键是 truefalse,值是满足或不满足条件的元素列表。以下是一个将学生按成绩是否及格分区的示例:

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,可以通过 truefalse 键分别获取及格和不及格的学生列表。

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 方法时,Collectorcombiner() 方法会被用于合并多个线程处理的结果。

以下是一个使用并行流计算整数列表平方和的示例,与前面自定义 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 方法使用了三个参数的版本,() -> 0Supplier,用于创建初始结果值;(sum, num) -> sum += num * numBiConsumer,用于将元素的平方累加到结果中;(sum1, sum2) -> sum1 + sum2 是另一个 BiConsumer,用于合并多个线程的结果。

需要注意的是,并行流并不总是能提高性能,特别是在数据量较小或者 Collector 的合并操作开销较大时。在实际应用中,需要根据具体情况进行测试和优化,以确定是否使用并行流以及如何选择合适的 Collector

综上所述,Java Streamcollect 方法是一个功能强大且灵活的数据统计工具。通过合理使用 Collectors 类提供的预定义 Collector 以及自定义 Collector,我们可以高效地对集合数据进行各种复杂的统计和收集操作。同时,在处理大规模数据时,结合并行流可以进一步提升性能。熟练掌握 collect 方法的使用,对于编写高效、简洁的 Java 数据处理代码至关重要。