Java8计算地图中对象列表的平均值

时间:2017-03-02 13:35:38

标签: java-8 java-stream

初始数据:

public class Stats {
    int passesNumber;
    int tacklesNumber;

    public Stats(int passesNumber, int tacklesNumber) {
        this.passesNumber = passesNumber;
        this.tacklesNumber = tacklesNumber;
    }

    public int getPassesNumber() {
        return passesNumber;
    }

    public void setPassesNumber(int passesNumber) {
        this.passesNumber = passesNumber;
    }

    public int getTacklesNumber() {
        return tacklesNumber;
    }

    public void setTacklesNumber(int tacklesNumber) {
        this.tacklesNumber = tacklesNumber;
    }
} 

Map<String, List<Stats>> statsByPosition = new HashMap<>();
statsByPosition.put("Defender", Arrays.asList(new Stats(10, 50), new Stats(15, 60), new Stats(12, 100)));
statsByPosition.put("Attacker", Arrays.asList(new Stats(80, 5), new Stats(90, 10)));

我需要按位置计算统计数据的平均值。因此结果应该是具有相同键的映射,但是值应该聚合到单个Stats对象(List应该简化为单个Stats对象)

{
  "Defender" => Stats((10 + 15 + 12) / 3, (50 + 60 + 100) / 3),
  "Attacker" => Stats((80 + 90) / 2, (5 + 10) / 2)
} 

4 个答案:

答案 0 :(得分:3)

我不认为Java8中有任何新功能可以帮助解决这个问题,至少效率不高。

如果仔细查看所有新API,那么您将看到它们中的大多数旨在提供更强大的原语来处理单个值及其序列 - 即double,{{序列1}},int

例如,为了计算? extends Object上的序列平均值,JDK引入了一个新类 - double,它做了一件显而易见的事情 - 收集了DoubleSummaryStatistics值的任意序列的摘要。 我实际上建议你自己采取类似的方法:制作你自己的double类,看起来像这样:

StatsSummary

现在,上面的实现实际上允许您在使用// assuming this is what your Stats class look like: class Stats { public final double a ,b; //the two stats public Stats(double a, double b) { this.a = a; this.b = b; } } // summary will go along the lines of: class StatsSummary implements Consumer<Stats> { DoubleSummaryStatistics a, b; // summary of stats collected so far StatsSummary() { a = new DoubleSummaryStatistics(); b = new DoubleSummaryStatistics(); } // this is how we collect it: @Override public void accept(Stats stat) { a.accept(stat.a); b.accept(stat.b); } public void combine(StatsSummary other) { a.combine(other.a); b.combine(other.b); } // now for actual methods that return stuff. I will implement only average and min // but rest of them are not hard public Stats average() { return new Stats(a.getAverage(), b.getAverage()); } public Stats min() { return new Stats(a.getMin(), b.getMin()); } } 等时表达您的正确意图:通过构建一个严格的API并使用JDK中可用的类作为构建块,您可以减少总体错误。

但是,如果你只想在某个地方计算一次平均值并且不需要其他任何东西,那么编写这个类有点矫枉过正,这是一个快速而又肮脏的解决方案:

Stream

答案 1 :(得分:1)

Java 8可能有一个更清晰的解决方案,但这种方法运行良好且不太复杂:

Map<String, Stats> newMap = new HashMap<>();

statsByPosition.forEach((key, statsList) -> {
    newMap.put(key, new Stats(
         (int) statsList.stream().mapToInt(Stats::getPassesNumber).average().orElse(0),
         (int) statsList.stream().mapToInt(Stats::getTacklesNumber).average().orElse(0))
    );
});

功能forEach方法可让您迭代给定地图的每个key value对。

您只需在地图中添加一个新条目即可获得平均值。在那里,您可以在给定地图中使用key。新值是一个新的Stats,其中构造函数的参数是直接计算的。

只需获取旧地图的值(statsList函数中的forEach,将给定统计数据的值与Integer的{​​{1}}值相对应,使用mapToInt函数。

此函数返回average,与OptionalDouble几乎相同。防止任何事情无效,您使用其Optional<Double>方法并传递默认值(如orElse())。由于平均值为0,因此您必须将值转换为double

如前所述,使用int可能会使用更短的版本。

答案 2 :(得分:0)

您也可以使用自定义收集器。我们将以下方法添加到Stats类:

 public Stats() {

 }

 public void accumulate(Stats stats) {
     passesNumber += stats.passesNumber;
     tacklesNumber += stats.tacklesNumber;
 }

 public Stats combine(Stats acc) {
     passesNumber += acc.passesNumber;
     tacklesNumber += acc.tacklesNumber;
     return this;
 }


 @Override
 public String toString() {
     return "Stats{" +
             "passesNumber=" + passesNumber +
             ", tacklesNumber=" + tacklesNumber +
             '}';
 }

现在我们可以在Stats方法中使用collect

System.out.println(statsByPosition.entrySet().stream().collect(
        Collectors.toMap(
            entity -> entity.getKey(),
            entity -> {
                Stats entryStats = entity.getValue().stream().collect(
                        Collector.of(Stats::new, Stats::accumulate, Stats::combine)
                ); // get stats for each map key. 

                // get average
                entryStats.setPassesNumber(entryStats.getPassesNumber() / entity.getValue().size());
                // get average
                entryStats.setTacklesNumber(entryStats.getTacklesNumber() / entity.getValue().size());

            return entryStats;
        }
))); // {Attacker=Stats{passesNumber=85, tacklesNumber=7}, Defender=Stats{passesNumber=12, tacklesNumber=70}}

答案 3 :(得分:0)

我不想发布此内容,但这对评论来说太大了。 如果 java-9可用且 StreamEx ,您可以这样做:

    public static Map<String, Stats> third(Map<String, List<Stats>> statsByPosition) {

    return statsByPosition.entrySet().stream()
            .collect(Collectors.groupingBy(e -> e.getKey(),
                    Collectors.flatMapping(e -> e.getValue().stream(),
                            MoreCollectors.pairing(
                                    Collectors.averagingDouble(Stats::getPassesNumber),
                                    Collectors.averagingDouble(Stats::getTacklesNumber),
                                    (a, b) -> new Stats(a, b)))));
}