使用Stream API计算含0分任务的平均分问题排查
问题排查:包含未完成0分任务的平均分计算错误
问题背景
我需要实现两个方法来计算包含学生未尝试完成的0分任务在内的平均分,但遇到0分场景时计算结果不符合预期。具体需求:
- totalScores:返回学生到其总平均分的映射;
- averageTotalScore:返回所有学生的平均结果,需考虑课程的全部任务而非仅学生完成的任务。
举例说明:若一名学生有4个实验任务,其中3个完成并获得分数,1个未尝试(应按0分计算),但当前totalScores方法计算平均分时未计入该0分任务,导致实际结果与预期不符(如Eva Bee的实际结果为77.00,预期为57.75)。
现有代码
CourseResult类
public class CourseResult { private final Person person; private final Map<String, Integer> taskResults; public CourseResult(final Person person, final Map<String, Integer> taskResults) { this.person = person; this.taskResults = taskResults; } public Person getPerson() { return person; } public Map<String, Integer> getTaskResults() { return taskResults; } }
有问题的totalScores方法
public static Map<Person, Double> totalScores(Stream<CourseResult> results) { Map<Person, List<Integer>> scores = results .flatMap(cr -> cr.getTaskResults().entrySet().stream() .filter(e -> e.getValue() >= 0) .map(e -> new AbstractMap.SimpleEntry<>(cr.getPerson(), e.getValue()))) .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(e -> Optional.ofNullable(e.getValue()).orElse(0), Collectors.toList()))); Map<Person, Double> averageScores = new HashMap<>(); scores.forEach((person, taskResults) -> { int totalScore = taskResults.stream() .mapToInt(Integer::intValue) .sum(); double tasksPerPerson = scores.get(person).stream() .mapToInt(Integer::intValue) .filter(score -> score >= 0) .count(); double averageScore = totalScore / tasksPerPerson; averageScores.put(person, averageScore); }); return averageScores; }
问题根源
- 丢失未尝试任务:当前逻辑仅处理学生已提交的任务(
taskResults中存在的条目),完全没考虑课程中存在但学生未尝试的任务——核心问题是你没有传入课程总任务列表,无法知道学生有多少未完成的任务需要按0分计算。 - 不必要的过滤:
- flatMap阶段的
.filter(e -> e.getValue() >= 0)会过滤掉值为负数的分数,若未尝试任务用null表示,这里会直接抛出NPE; - 计算任务数量时的
.filter(score -> score >= 0)会把0分任务排除,导致分母偏小,平均分偏高。
- flatMap阶段的
- 冗余的Optional处理:
Optional.ofNullable(e.getValue()).orElse(0)在已经过滤e.getValue() >=0的情况下毫无意义,非null的正数不会触发orElse分支。
修复方案
要解决问题,必须先获取课程的所有任务列表,才能统计每个学生的总任务数(已完成+未完成)。以下是修复后的代码:
修复后的完整实现
import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; public class CourseScoreCalculator { // 修改方法签名,传入课程的所有任务集合,确保能统计未完成任务 public static Map<Person, Double> totalScores(Stream<CourseResult> results, Set<String> allCourseTasks) { return results.collect(Collectors.toMap( CourseResult::getPerson, courseResult -> calculateStudentAverage(courseResult, allCourseTasks) )); } // 单独提取学生平均分计算逻辑,提高可读性 private static double calculateStudentAverage(CourseResult courseResult, Set<String> allCourseTasks) { Map<String, Integer> studentTaskScores = courseResult.getTaskResults(); int totalScore = 0; int totalTaskCount = allCourseTasks.size(); // 遍历所有课程任务,未完成的自动按0分累加 for (String taskName : allCourseTasks) { totalScore += studentTaskScores.getOrDefault(taskName, 0); } // 避免除以0的情况(如果课程没有任务) return totalTaskCount == 0 ? 0.0 : (double) totalScore / totalTaskCount; } // 实现averageTotalScore方法:计算所有学生平均分的平均值 public static double averageTotalScore(Stream<CourseResult> results, Set<String> allCourseTasks) { Map<Person, Double> studentAverages = totalScores(results, allCourseTasks); return studentAverages.values().stream() .mapToDouble(Double::doubleValue) .average() // 如果没有学生,返回0.0 .orElse(0.0); } }
修复说明
- 新增课程任务参数:通过传入
allCourseTasks集合,明确课程的总任务数,确保未尝试的任务被计入; - 按任务列表计算分数:遍历所有课程任务,用
getOrDefault自动将未完成任务的分数设为0; - 移除冗余过滤:不再过滤任何分数,确保0分任务被正确统计;
- 异常防护:处理了课程无任务、无学生的边界情况;
- 逻辑拆分:将学生平均分计算拆分为私有方法,代码更清晰易维护。
内容的提问来源于stack exchange,提问作者Spiritus
相关产品推荐
相关产品推荐

