1. 引言
本文将探讨几种在Java中计算加权平均数的方法。加权平均数是统计学中的常见概念,在数据分析和科学计算中广泛应用。我们将从基础概念出发,逐步展示不同的实现方案,包括传统循环、Stream API以及自定义Collector等高级技巧。
2. 什么是加权平均数?
普通平均数(算术平均)的计算方式是将所有数值相加后除以数量。例如,数字集合 {1, 3, 5, 7, 9} 的平均数是 (1+3+5+7+9)/5 = 5。
加权平均数则引入了权重概念,每个数值都有对应的权重值:
数值 | 权重 |
---|---|
1 | 10 |
3 | 20 |
5 | 30 |
7 | 50 |
9 | 40 |
计算方式变为:将每个数值乘以其权重后求和,再除以所有权重的总和。上例的计算过程为:
((1*10) + (3*20) + (5*30) + (7*50) + (9*40)) / (10+20+30+50+40) = 6.2
⚠️ 注意:权重越大,该数值对最终结果的影响越大。
3. 准备工作
为演示不同实现方式,我们先定义基础数据结构:
private static class Values {
int value;
int weight;
public Values(int value, int weight) {
this.value = value;
this.weight = weight;
}
}
并准备测试数据和预期结果:
private List<Values> values = Arrays.asList(
new Values(1, 10),
new Values(3, 20),
new Values(5, 30),
new Values(7, 50),
new Values(9, 40)
);
private Double expected = 6.2;
4. 双遍历计算法
最直观的实现方式是分别计算分子(数值×权重之和)和分母(权重之和):
double top = values.stream()
.mapToDouble(v -> v.value * v.weight)
.sum();
double bottom = values.stream()
.mapToDouble(v -> v.weight)
.sum();
double result = top / bottom;
✅ 优点:
- 逻辑清晰,易于理解
- 充分利用Stream API的声明式特性
❌ 缺点:
- 需要两次遍历集合
- 性能略低于单次遍历方案
优化方案:使用传统for循环在一次遍历中完成计算:
double top = 0;
double bottom = 0;
for (Values v : values) {
top += (v.value * v.weight);
bottom += v.weight;
}
⚠️ 踩坑提示:此方案使用了可变变量,在并行流中不安全。
5. 列表展开法
换个思路:将加权值展开为多个普通值。例如权重为10的数值1,展开为10个1。然后对展开后的列表计算普通平均数:
double result = values.stream()
.flatMap(v -> Collections.nCopies(v.weight, v.value).stream())
.mapToInt(v -> v)
.average()
.getAsDouble();
✅ 优点:
- 逻辑更接近数学定义
- 方便后续进行其他统计计算(如中位数)
❌ 缺点:
- 内存消耗大(特别是权重值很大时)
- 性能较差(创建大量临时对象)
6. 归约计算法
为避免多次遍历且保持不可变性,可使用Stream的reduce()
操作:
class WeightedAverage {
final double top;
final double bottom;
public WeightedAverage(double top, double bottom) {
this.top = top;
this.bottom = bottom;
}
double average() {
return top / bottom;
}
}
double result = values.stream()
.reduce(new WeightedAverage(0, 0),
(acc, next) -> new WeightedAverage(
acc.top + (next.value * next.weight),
acc.bottom + next.weight),
(left, right) -> new WeightedAverage(
left.top + right.top,
left.bottom + right.bottom))
.average();
代码解析:
- 初始值:
new WeightedAverage(0, 0)
- 累加器:合并当前值与新值
- 组合器:用于并行流合并部分结果
- 最终计算:调用
average()
方法
✅ 优点:
- 单次遍历完成计算
- 支持并行处理
- 保持不可变性
❌ 缺点:
- 代码复杂度较高
- 需要定义辅助类
7. 自定义收集器
为获得更优雅的API,我们可以实现自定义Collector
:
class RunningTotals {
double top;
double bottom;
public RunningTotals() {
this.top = 0;
this.bottom = 0;
}
}
public class WeightedAverageCollector implements Collector<Values, RunningTotals, Double> {
@Override
public Supplier<RunningTotals> supplier() {
return RunningTotals::new;
}
@Override
public BiConsumer<RunningTotals, Values> accumulator() {
return (current, next) -> {
current.top += (next.value * next.weight);
current.bottom += next.weight;
};
}
@Override
public BinaryOperator<RunningTotals> combiner() {
return (left, right) -> {
left.top += right.top;
left.bottom += right.bottom;
return left;
};
}
@Override
public Function<RunningTotals, Double> finisher() {
return rt -> rt.top / rt.bottom;
}
@Override
public Set<Characteristics> characteristics() {
return Collections.singleton(Characteristics.UNORDERED);
}
}
使用方式非常简洁:
double result = values.stream().collect(new WeightedAverageCollector());
Collector核心方法解析:
supplier()
:创建累加器容器accumulator()
:处理单个元素combiner()
:合并并行结果finisher()
:最终转换characteristics()
:声明收集器特性
✅ 优点:
- API简洁优雅
- 支持并行流
- 可复用性强
❌ 缺点:
- 实现复杂度高
- 初次开发成本大
8. 总结
本文展示了四种计算加权平均数的方案:
方案 | 适用场景 | 性能 | 复杂度 |
---|---|---|---|
双遍历计算 | 简单场景 | 中 | 低 |
列表展开 | 需要后续统计 | 差 | 中 |
归约计算 | 单次遍历需求 | 优 | 高 |
自定义收集器 | 高频复用场景 | 优 | 最高 |
选择建议:
- 一次性计算 → 双遍历法(简单直接)
- 需要并行处理 → 归约法或自定义收集器
- 多处复用 → 自定义收集器(一劳永逸)
完整代码示例可在GitHub获取。下次遇到加权计算需求,不妨试试这些方案!