Java Stream API:高效处理学生成绩数据并按平均分排序教程

Java Stream API:高效处理学生成绩数据并按平均分排序教程

本教程详细介绍了如何使用Java Stream API高效处理学生成绩数据。内容涵盖从数据收集、利用Collectors.tomap将学生多门成绩转换为平均分、到使用流操作进行过滤、以及最终通过Map.Entry.comparingByValue进行降序排序并格式化输出。通过优化计算逻辑和利用Stream API的强大功能,避免了重复计算,提升了代码的简洁性和执行效率。

1. 问题背景与数据处理需求

在实际应用中,我们经常需要处理结构化数据,例如学生成绩。本教程的目标是构建一个程序,能够:

  1. 读取指定数量的学生姓名及其对应的多门成绩。
  2. 记录每位学生的全部成绩。
  3. 计算每位学生的平均分。
  4. 筛选出平均分高于或等于4.50的学生。
  5. 将筛选后的学生按照平均分降序排列
  6. 以特定格式(”{姓名} -> {平均分}”,平均分保留两位小数)输出结果。

2. 初始数据收集与存储

首先,我们需要一个数据结构来存储学生的姓名和他们的多门成绩。HashMap是一个理想的选择,其中键为学生姓名(String),值为一个包含该学生所有成绩的列表(List)。

import java.util.*; import java.util.stream.Collectors;  public class StudentGradesProcessor {     public static void main(String[] args) {         Scanner scanner = new Scanner(System.in);          int n = Integer.parseInt(scanner.nextLine()); // 读取学生数量          Map<String, List<double>> studentRecords = new HashMap<>();          // 循环读取学生姓名和成绩         while (n > 0) {             String name = scanner.nextLine();             double grade = Double.parseDouble(scanner.nextLine());             // 如果学生不存在,则添加新条目;否则,将成绩添加到现有列表中             studentRecords.putIfAbsent(name, new ArrayList<>());             studentRecords.get(name).add(grade);             n--;         }          // 后续处理将在下方详细介绍     } }

在上述代码中,我们使用putIfAbsent方法确保每个学生姓名只创建一次列表,然后通过get(name).add(grade)将成绩添加到对应的列表中。

3. Stream API优化:计算平均分、过滤与排序

原始的处理方式可能会在过滤和排序阶段多次计算学生的平均分,这不仅效率低下,而且在进行浮点数比较时,直接将double类型转换为int进行排序(如(int) (average2 – average1))可能会导致精度丢失,从而产生错误的排序结果。

为了解决这些问题,我们可以采用以下优化策略:

立即学习Java免费学习笔记(深入)”;

  1. 一次性计算平均分并存储: 将Map>转换为Map,其中键是学生姓名,值是其计算好的平均分。
  2. 利用Map.Entry的比较器: Java 8的Stream API提供了强大的Comparator辅助方法,特别是针对Map.Entry的排序,可以避免手动编写复杂的比较逻辑。

3.1 转换数据结构:计算平均分

我们可以使用Collectors.toMap将原始的studentRecords映射转换为一个新的Map,其中存储的是学生的姓名及其平均分。

        Map<String, Double> recordsWithAverage = studentRecords.entrySet()             .stream()             // 将每个Map.Entry<String, List<Double>>转换为Map.Entry<String, Double>             // 键保持不变,值为对应List<Double>的平均值             .collect(Collectors.toMap(                 Map.Entry::getKey, // 新Map的键是原始Map的键(学生姓名)                 e -> e.getValue().stream().mapToDouble(Double::doubleValue).average().orElse(0.0) // 新Map的值是平均分             ));

这里,e -> e.getValue().stream().mapToDouble(Double::doubleValue).average().orElse(0.0)负责计算每个学生所有成绩的平均值。mapToDouble(Double::doubleValue)将Stream转换为DoubleStream,以便进行平均值计算。orElse(0.0)用于处理学生没有成绩的情况(尽管在此问题中不会发生,但作为健壮性考虑是好的实践)。

3.2 过滤与排序

现在我们有了recordsWithAverage,其中每个学生都对应一个已经计算好的平均分。接下来的过滤和排序操作将变得非常简洁和高效。

        recordsWithAverage.entrySet()             .stream()             // 过滤:只保留平均分大于或等于4.50的学生             .Filter(e -> e.getValue() >= 4.50)             // 排序:按平均分降序排列             // Map.Entry.comparingByValue() 返回一个Comparator,用于比较Map.Entry的值             // Comparator.reverseOrder() 将比较器反转,实现降序排列             .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))             // 遍历并打印结果             .foreach(pair -> {                 System.out.printf("%s -> %.2f%n", pair.getKey(), pair.getValue());             });

这里的关键是Map.Entry.comparingByValue(Comparator.reverseOrder())。它直接作用于Map.Entry对象,根据其值进行比较,并且Comparator.reverseOrder()确保了降序排列。这样,我们无需手动编写复杂的Comparator逻辑,代码更加简洁易懂。

4. 完整代码示例

将上述所有部分整合起来,完整的Java程序如下:

import java.util.*; import java.util.stream.Collectors;  public class StudentGradesProcessor {     public static void main(String[] args) {         Scanner scanner = new Scanner(System.in);          int n = Integer.parseInt(scanner.nextLine());          // 步骤1: 收集原始学生姓名和成绩数据         Map<String, List<Double>> studentRecords = new HashMap<>();         while (n > 0) {             String name = scanner.nextLine();             double grade = Double.parseDouble(scanner.nextLine());             studentRecords.putIfAbsent(name, new ArrayList<>());             studentRecords.get(name).add(grade);             n--;         }          // 步骤2: 计算每个学生的平均分,并存储到新的Map中         Map<String, Double> recordsWithAverage = studentRecords.entrySet()             .stream()             .collect(Collectors.toMap(                 Map.Entry::getKey,                 e -> e.getValue().stream().mapToDouble(Double::doubleValue).average().orElse(0.0)             ));          // 步骤3: 过滤、排序并打印结果         recordsWithAverage.entrySet()             .stream()             .filter(e -> e.getValue() >= 4.50) // 过滤:平均分大于等于4.50             .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) // 排序:按平均分降序             .forEach(pair -> {                 System.out.printf("%s -> %.2f%n", pair.getKey(), pair.getValue()); // 格式化输出             });          scanner.close(); // 关闭Scanner     } }

5. 注意事项与总结

  • 避免重复计算: 将平均分计算为独立的一步,避免在过滤和排序过程中反复计算,显著提高了效率。
  • 使用Double.compare()或内置比较器: 对于浮点数比较,应使用Double.compare()或像Map.Entry.comparingByValue()这样的内置比较器,而不是将差值强制转换为int,以避免精度问题和不正确的排序结果。
  • Stream API的链式操作: Stream API允许将多个操作(如filter、sorted、forEach)链式调用,使代码更具可读性和表达力。
  • Collectors.toMap的强大功能: Collectors类提供了多种强大的收集器,能够将流中的元素转换成不同的数据结构,是Stream API中不可或缺的一部分。
  • 资源管理: 养成关闭Scanner等资源的好习惯,避免资源泄露。

通过本教程,您应该已经掌握了如何使用Java Stream API高效地处理学生成绩数据,包括数据的收集、转换、过滤和排序,并了解了如何优化代码以提高性能和准确性。

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享