如何使用Java 8流API从映射列表创建映射的地图

15

背景

我有一个地图列表,看起来大致像这样:

[
  {
    "name": "A",
    "old": 0.25,
    "new": 0.3
  },
  {
    "name": "B",
    "old": 0.3,
    "new": 0.35
  },
  {
    "name": "A",
    "old": 0.75,
    "new": 0.7
  },
  {
    "name": "B",
    "old": 0.7,
    "new": 0.60
  }
]

我希望输出结果看起来像这样:

{
  "A": {
    "old": 1,
    "new": 1
  },
  "B": {
    "old": 1,
    "new": 0.95
  }
}

...其中与每个相关条目相关的oldnew的值被求和。

列表映射数据类型为List<Map<String, Object>>,因此输出应为Map<String, Map<String, Double>>

我尝试过的方法

通过绘制一些图表、阅读文档并进行试错,我得出了以下结论:

data.stream()
    .collect(
        Collectors.groupingBy(entry -> entry.get("name"),
            Collectors.summingDouble(entry ->
                Double.parseDouble(entry.get("old").toString())))
    );

生成一个类型为Map<String, Double>的对象,其中输出是

{
  "A": 1,
  "B": 1
}

对于“旧值”的求和,我无法将其转换为地图的地图。类似于这样:

对于“旧值”的求和,但我无法完全将它转换成一个映射到映射的模式。类似于这样:

data.stream()
    .collect(
        Collectors.groupingBy(entry -> entry.get("name"),
            Collectors.mapping(
                Collectors.groupingBy(entry -> entry.get("old"),
                    Collectors.summingDouble(entry ->
                        Double.parseDouble(entry.get("old").toString())
                    )
                ),
                Collectors.groupingBy(entry -> entry.get("new"),
                    Collectors.summingDouble(entry ->
                        Double.parseDouble(entry.get("new").toString())
                    )
                )
            )
        )
    );

由于Collectors.mapping()只接受一个映射函数和一个下游收集器,所以它无法运行。但我不知道如何一次映射两个值。

是否有其他函数可以创建两个不同值的映射?非常感谢任何关于更好方法的建议。


1
nameoldnew封装在一个单独的类中,可以简化您的工作,您能否提取它呢? - Nisarg Patil
4个回答

9

您可以使用流,但也可以使用MapcomputeIfAbsentmerge方法:

Map<String, Map<String, Double>> result = new LinkedHashMap<>();
data.forEach(entry -> {
    String name = (String) entry.get("name");
    Map<String, Double> map = result.computeIfAbsent(name, k -> new HashMap<>());
    map.merge("old", (Double) entry.get("old"), Double::sum);
    map.merge("new", (Double) entry.get("new"), Double::sum);
});

这样翻译会更易读。顺便说一下,我们甚至可以使用 toMap 收集器,但是如果不重构代码,可读性就不太好了。 - Ousmane D.

6

只使用Stream工具(类似于这个)就可以实现这一点:

Map<String, Map<String, Double>> collect = data.stream().collect(
    Collectors.groupingBy(m -> (String)m.get("name"),
    Collector.of(LinkedHashMap::new,
        (acc, e) -> Stream.of("old", "new").forEach(key -> acc.merge(key, (Double) e.get(key), Double::sum)),
        (m1, m2) -> {
          m2.forEach((k, v) -> m1.merge(k, v, Double::sum));
          return m1;
        })
    ));

还有一种方法是使用Java 8:

Map<String, Map<String, Double>> stats = data.stream().collect(
    Collectors.groupingBy(m -> (String) m.get("name"),
        Collectors.flatMapping(m -> m.entrySet().stream().filter(e -> !"name".equals(e.getKey())),
            Collectors.toMap(Map.Entry::getKey, e -> (Double)e.getValue(), Double::sum, LinkedHashMap::new)
        )
    ));

1
这实际上相当不错,1+1。 - Eugene

4

您的第一次尝试已经接近解决方案了,但是您需要编写一些自定义代码才能完全完成图片。

您需要实现自己的 collector,它将多个地图转换为单个双重地图。

具体如下:

       Collector.of(
            () -> new HashMap<>(),
            (Map<String, Double>target, Map<String, Object> source) -> {
                target.merge("old", (Double)source.get("old"), Double::sum);
                target.merge("new", (Double)source.get("new"), Double::sum);
            },
            (Map<String, Double> map1, Map<String, Double> map2) -> {
                map2.forEach((k, v) -> map1.merge(k, v, Double::sum));
                return map1;
            }
        ) 

这个结合了你最初的按尝试分组的方法,就解决了这个问题。
data.stream()
    .collect(
        Collectors.groupingBy(entry -> entry.get("name"),
            // Insert collector here
        )
    );

在线完整代码示例:http://tpcg.io/pJftrJ


2
您可以声明一个名为Pair的类。
public class Pair {
    private final double oldVal;
    private final double newVal;

    public Pair(double oldVal, double newVal) {
        super();
        this.oldVal = oldVal;
        this.newVal = newVal;
    }

    public double getOldVal() {
        return oldVal;
    }

    public double getNewVal() {
        return newVal;
    }

    @Override
    public String toString() {
        return "{oldVal=" + oldVal + ", newVal=" + newVal + "}";
    }

}

那么就像这样做:
Map<Object, Pair> result = sourceMaps.stream()
        .collect(Collectors.toMap(m -> m.get("name"),
                m -> new Pair((double) m.get("old"), (double) m.get("new")),
                (p1, p2) -> new Pair(p1.getOldVal() + p2.getOldVal(), p1.getNewVal() + p2.getNewVal())));

以下是输出结果:

{A={oldVal=1.0, newVal=1.0}, B={oldVal=1.0, newVal=0.95}}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接