如何用Java 8 Stream计算CarData多字段平均值并生成单对象
用Java 8 Stream一次性生成多字段平均值的CarData
嘿,这个场景我太熟悉了!你不想手动拼接两个IntSummaryStatistics的结果,还想高效完成多字段的平均值统计,其实用自定义Collector就能完美解决,而且只需要遍历一次列表~
核心思路
既然要同时统计bodyWeight和shellWeight两个字段的总和与数量,最划算的方式是一次遍历完成所有统计,避免多次遍历列表(尤其是数据量大的时候)。我们可以自定义一个统计容器类,或者用数组临时存储统计数据,最后再转换成目标CarData对象。
方法一:自定义统计容器类(可读性最佳)
先写一个专门用来统计CarData字段的容器类,把总和、数量都存起来,最后再计算平均值生成CarData:
// 自定义统计容器,用来累积两个字段的总和与数据量 class CarStats { private int bodyWeightSum; private int shellWeightSum; private long count; // 空构造器,初始化默认值 public CarStats() { this(0, 0, 0); } // 带参数的构造器 public CarStats(int bodyWeightSum, int shellWeightSum, long count) { this.bodyWeightSum = bodyWeightSum; this.shellWeightSum = shellWeightSum; this.count = count; } // 累加单个CarData的数据 public CarStats accept(CarData car) { return new CarStats( this.bodyWeightSum + car.getBodyWeight(), this.shellWeightSum + car.getShellWeight(), this.count + 1 ); } // 合并两个统计结果(并行流时会用到) public CarStats combine(CarStats other) { return new CarStats( this.bodyWeightSum + other.bodyWeightSum, this.shellWeightSum + other.shellWeightSum, this.count + other.count ); } // 把统计结果转换成平均后的CarData public CarData toAverageCarData() { if (count == 0) { // 处理空列表的情况,根据需求返回null或默认CarData return null; } // 这里假设CarData有接收两个int参数的构造器,或者用Builder模式 return new CarData( (int) (bodyWeightSum / count), (int) (shellWeightSum / count) ); } }
然后用Collector.of()把这个容器和Stream结合起来:
List<CarData> cars = getCars(); CarData averageCar = cars.stream() .collect(Collector.of( CarStats::new, // 供应器:创建初始的空统计容器 CarStats::accept, // 累加器:把每个CarData加入统计 CarStats::combine, // 组合器:并行流时合并多个统计结果 CarStats::toAverageCarData // 终结器:把统计结果转成目标CarData ));
方法二:用数组临时存储统计数据(无需额外类)
如果不想写额外的容器类,也可以用一个int[]来临时存储[body总和, shell总和, 数据量],代码更简洁但可读性稍差:
List<CarData> cars = getCars(); CarData averageCar = cars.stream() .collect(Collector.of( () -> new int[]{0, 0, 0}, // 初始化数组:[bodySum, shellSum, count] (stats, car) -> { stats[0] += car.getBodyWeight(); stats[1] += car.getShellWeight(); stats[2]++; }, (leftStats, rightStats) -> { // 并行流时合并数组数据 leftStats[0] += rightStats[0]; leftStats[1] += rightStats[1]; leftStats[2] += rightStats[2]; return leftStats; }, stats -> { if (stats[2] == 0) return null; // 处理空列表 return new CarData(stats[0]/stats[2], stats[1]/stats[2]); } ));
不推荐的方式:两次遍历列表
虽然用Collectors.averagingInt可以单独统计每个字段,但需要遍历列表两次,效率不高,适合小数据量场景:
// 不推荐:两次遍历列表,数据量大时性能较差 double avgBody = cars.stream().collect(Collectors.averagingInt(CarData::getBodyWeight)); double avgShell = cars.stream().collect(Collectors.averagingInt(CarData::getShellWeight)); CarData averageCar = new CarData((int) avgBody, (int) avgShell);
注意事项
- 记得处理空列表的情况,避免出现除以0的异常;
- 如果需要保留小数(比如平均值不是整数),可以把统计容器里的总和改成
long类型,最后返回double类型的字段,或者调整CarData的字段类型。
内容的提问来源于stack exchange,提问作者Squiggs.
相关产品推荐
相关产品推荐

