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();

代码解析

  1. 初始值:new WeightedAverage(0, 0)
  2. 累加器:合并当前值与新值
  3. 组合器:用于并行流合并部分结果
  4. 最终计算:调用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获取。下次遇到加权计算需求,不妨试试这些方案!