3.8 汇总统计

问题

用户希望获取数值流中元素的数量、总和、最小值、最大值以及平均值。

方案

使用 IntStreamDoubleStreamLongStream 接口定义的 summaryStatistics 方法。

讨论

基本类型流 IntStreamDoubleStreamLongStreamStream 接口引入了用于处理基本数据类型的方法,summaryStatistics 就是其中一种方法,如例 3-42 所示。

例 3-42 summaryStatistics 方法

  1. DoubleSummaryStatistics stats = DoubleStream.generate(Math::random)
  2. .limit(1_000_000)
  3. .summaryStatistics();
  4. System.out.println(stats);
  5. System.out.println("count: " + stats.getCount());
  6. System.out.println("min : " + stats.getMin());
  7. System.out.println("max : " + stats.getMax());
  8. System.out.println("sum : " + stats.getSum());
  9. System.out.println("ave : " + stats.getAverage());

➊ 使用 toString 方法打印

3.8 汇总统计 - 图1 从 Java 7 开始,可以在数字字面量中使用下划线,如 1_000_000。

执行上述程序,输出结果类似于:

  1. DoubleSummaryStatistics{count=1000000, sum=499608.317465, min=0.000001,
  2. average=0.499608, max=0.999999}
  3. count: 1000000
  4. min : 1.3938598313334438E-6
  5. max : 0.9999988915490642
  6. sum : 499608.31746475823
  7. ave : 0.49960831746475826

DoubleSummaryStatistics 类定义的 toString 方法返回字符串的表达形式,也提供用于统计元素数量、总和、最小值、最大值以及平均值的 getter 方法(getCountgetSumgetMaxgetMin 以及 getAverage)。当 double 型元素的数量达到 100 万时,最小值趋近于 0,最大值趋近于 1,总和约为 50 万,平均值约为 0.5。

DoubleSummaryStatistics 类还定义了以下两个有趣的方法:

  1. void accept(double value)
  2. void combine(DoubleSummaryStatistics other)

accept 方法用于在汇总信息中记录另一个值,而 combine 方法将两个 DoubleSummaryStatistics 对象合二为一。两种方法在计算结果之前,向类的实例添加数据时使用。

我们以运动员薪资跟踪网站 Spotrac 为例进行讨论。本书的源代码包括一个文件,它记录了 2017 赛季美国职棒大联盟(MLB)全部 30 支球队的薪资数据,这些数据均来自 Spotrac。

例 3-43 定义了一个名为 Team 的类,包括 idname(队名)与 salary(薪资)。

例 3-43 Team 类包括 idnamesalary

  1. public class Team {
  2. private static final NumberFormat nf = NumberFormat.getCurrencyInstance();
  3.  
  4. private int id;
  5. private String name;
  6.  
  7. private double salary;
  8.  
  9. // 构造函数、getter与setter
  10.  
  11. @Override
  12. public String toString() {
  13. return "Team{" +
  14. "id=" + id +
  15. ", name='" + name + '\'' +
  16. ", salary=" + nf.format(salary) +
  17. '}';
  18. }
  19. }

解析球队工资文件,结果如下:

  1. Team{id=1, name='Los Angeles Dodgers', salary=$245,269,535.00}
  2. Team{id=2, name='Boston Red Sox', salary=$202,135,939.00}
  3. Team{id=3, name='New York Yankees', salary=$202,095,552.00}
  4. ...
  5. Team{id=28, name='San Diego Padres', salary=$73,754,027.00}
  6. Team{id=29, name='Tampa Bay Rays', salary=$73,102,766.00}
  7. Team{id=30, name='Milwaukee Brewers', salary=$62,094,433.00}

可以通过两种方式计算球队集合的汇总统计信息。第一种方式采用 collect 方法的三参数形式,如例 3-44 所示。

例 3-44 传入 Supplier、累加器与组合器的 collect 方法

  1. DoubleSummaryStatistics teamStats = teams.stream()
  2. .mapToDouble(Team::getSalary)
  3. .collect(DoubleSummaryStatistics::new,
  4. DoubleSummaryStatistics::accept,
  5. DoubleSummaryStatistics::combine);

有关这种 collect 方法的讨论请参见范例 4.9。在本例中,collect 方法通过构造函数引用来提供 DoubleSummaryStatistics 的实例,通过 accept 方法将另一个值添加到现有的 DoubleSummaryStatistics 对象,以及通过 combine 方法将两个单独的 DoubleSummaryStatistics 对象合二为一。

输出结果如下(为便于阅读,对结果做了处理):

  1. 30 teams
  2. sum = $4,232,271,100.00
  3. min = $62,094,433.00
  4. max = $245,269,535.00
  5. ave = $141,075,703.33

计算汇总信息的另一种方案请参见范例 4.6(下游收集器)。此时,汇总计算如例 3-45 所示。

例 3-45 使用 summarizingDouble 方法进行收集

  1. teamStats = teams.stream()
  2. .collect(Collectors.summarizingDouble(Team::getSalary));

其中,Collectors.summarizingDouble 方法的参数是各队的薪资。无论采用哪种方案,结果并无区别。

从本质上讲,汇总统计类是一种“糟糕”的统计方法,因为它们仅能统计数量、最大值、最小值、总和、平均值等属性。然而,如果只需要这些属性,那么 Java 标准库应能满足需要。3

3当然,读者从本范例中还应学到一点:如果有机会参加 MLB 比赛,那么不妨一试,哪怕只有很短的上场时间。赛后再继续学习 Java 吧!

另见

汇总统计是归约操作的一种特殊形式,其他归约操作的介绍请参见范例 3.3,有关下游收集器的讨论请参见范例 4.6,有关多参数 collect 方法的讨论请参见范例 4.9。