使用Java 8 Stream API合并映射列表

时间:2016-06-28 09:17:42

标签: java lambda java-8

我正在尝试学习Java 8 Stream,当我尝试将一些函数转换为java8来练习时。我遇到了一个问题。

我很好奇如何将关注代码转换为java流格式。

/*
 * input example:
 * [
    {
        "k1": { "kk1": 1, "kk2": 2},
        "k2": {"kk1": 3, "kk2": 4}
    }
    {
        "k1": { "kk1": 10, "kk2": 20},
        "k2": {"kk1": 30, "kk2": 40}
    }
  ]
 * output:
 * {
        "k1": { "kk1": 11, "kk2": 22},
        "k2": {"kk1": 33, "kk2": 44}
   }
 *
 *
 */
private static Map<String, Map<String, Long>> mergeMapsValue(List<Map<String, Map<String, Long>>> valueList) {
    Set<String> keys_1 = valueList.get(0).keySet();
    Set<String> keys_2 = valueList.get(0).entrySet().iterator().next().getValue().keySet();
    Map<String, Map<String, Long>> result = new HashMap<>();
    for (String k1: keys_1) {
        result.put(k1, new HashMap<>());
        for (String k2: keys_2) {
            long total = 0;
            for (Map<String, Map<String, Long>> mmap: valueList) {
                Map<String, Long> m = mmap.get(k1);
                if (m != null && m.get(k2) != null) {
                    total += m.get(k2);
                }
            }
            result.get(k1).put(k2, total);
        }
    }
    return result;
}

3 个答案:

答案 0 :(得分:6)

这里的诀窍是正确收集内部地图。工作流程将是:

  • 将地图List<Map<String, Map<String, Long>>>的列表平面映射到地图条目流Stream<Map.Entry<String, Map<String, Long>>>
  • 按每个条目的键分组,对于映射到同一个键的值,将两个映射合并在一起。

通过合并它们来收集地图将理想地保证flatMapping收集器,遗憾的是,在Java 8中不存在it will exist in Java 9(参见JDK-8071600)。对于Java 8,可以使用StreamEx库提供的库(并在以下代码中使用MoreCollectors.flatMapping)。

private static Map<String, Map<String, Long>> mergeMapsValue(List<Map<String, Map<String, Long>>> valueList) {
    return valueList.stream()
                    .flatMap(e -> e.entrySet().stream())
                    .collect(Collectors.groupingBy(
                        Map.Entry::getKey,
                        Collectors.flatMapping(
                            e -> e.getValue().entrySet().stream(),
                            Collectors.<Map.Entry<String,Long>,String,Long>toMap(Map.Entry::getKey, Map.Entry::getValue, Long::sum)
                        )
                    ));
}

如果不使用这个方便的收集器,我们仍然可以使用等效的语义构建我们自己的收集器:

private static Map<String, Map<String, Long>> mergeMapsValue2(List<Map<String, Map<String, Long>>> valueList) {
    return valueList.stream()
                    .flatMap(e -> e.entrySet().stream())
                    .collect(Collectors.groupingBy(
                        Map.Entry::getKey,
                        Collector.of(
                            HashMap::new,
                            (r, t) -> t.getValue().forEach((k, v) -> r.merge(k, v, Long::sum)),
                            (r1, r2) -> { r2.forEach((k, v) -> r1.merge(k, v, Long::sum)); return r1; }
                        )
                    ));
}

答案 1 :(得分:4)

作为起点,转换为使用computeIfAbsentmerge可为我们提供以下内容:

private static <K1, K2> Map<K1, Map<K2, Long>> mergeMapsValue(List<Map<K1, Map<K2, Long>>> valueList) {
    final Map<K1, Map<K2, Long>> result = new HashMap<>();
    for (final Map<K1, Map<K2, Long>> map : valueList) {
        for (final Map.Entry<K1, Map<K2, Long>> sub : map.entrySet()) {
            for (final Map.Entry<K2, Long> subsub : sub.getValue().entrySet()) {
                result.computeIfAbsent(sub.getKey(), k1 -> new HashMap<>())
                        .merge(subsub.getKey(), subsub.getValue(), Long::sum);
            }
        }
    }
    return result;
}

这会从内循环中删除大部分逻辑。

以下代码为wrong,我将其留待此处参考。

转换为Stream API不会让它更整洁,但让我们试一试。

import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;

private static <K1, K2> Map<K1, Map<K2, Long>> mergeMapsValue(List<Map<K1, Map<K2, Long>>> valueList) {
    return valueList.stream()
            .flatMap(v -> v.entrySet().stream())
            .collect(groupingBy(Entry::getKey, collectingAndThen(mapping(Entry::getValue, toList()), l -> l.stream()
                    .reduce(new HashMap<>(), (l2, r2) -> {
                        r2.forEach((k, v) -> l2.merge(k, v, Long::sum);
                        return l2;
                    }))));
}

这是我设法提出的 - 它太可怕了。问题是使用foreach方法,您可以参考迭代的每个级别 - 这使得逻辑变得简单。使用功能方法,您需要单独考虑每个折叠操作。

它是如何运作的?

我们首先stream()我们List<Map<K1, Map<K2, Long>>>,给出Stream<Map<K1, Map<K2, Long>>>。接下来我们flatMap每个元素,给出一个Stream<Entry<K1, Map<K2, Long>>> - 所以我们展平了第一个维度。但我们不能进一步扁平化,因为我们需要K1价值。

然后我们在collect(groupingBy)值上使用K1给我们Map<K1, SOMETHING> - 这是什么?

好吧,首先我们使用mapping(Entry::getValue, toList())给我们Map<K1, List<Map<K2, Long>>>。然后,我们使用collectingAndThen获取List<Map<K2, Long>>并减少它。请注意,这意味着我们会生成一个中间版List,这很浪费 - 您可以使用自定义Collector解决此问题。

为此,我们使用List.stream().reduce(a, b),其中a是初始值,b是&#34; fold&#34;操作。 a设置为new HashMap<>()b设置两个值:函数的上一个应用程序的初始值或结果以及List中的当前项。因此,我们为List中的每个项目使用Map.merge来组合值。

我会说这种方法或多或少难以辨认 - 你几个小时之内就无法破译它,更不用说几天了。

答案 2 :(得分:0)

我从Tunaki那里拿了flatMap(e -> e.entrySet().stream())部分,但是为收集者使用了一个较短的变种:

Map<String, Integer> merged = maps.stream()
  .flatMap(map -> map.entrySet().stream())
  .collect(Collectors.toMap(
    Map.Entry::getKey, Map.Entry::getValue, Integer::sum));

更详细的例子:

Map<String, Integer> a = new HashMap<String, Integer>() {{
  put("a", 2);
  put("b", 5);
}};
Map<String, Integer> b = new HashMap<String, Integer>() {{
  put("a", 7);
}};
List<Map<String, Integer>> maps = Arrays.asList(a, b);

Map<String, Integer> merged = maps.stream()
  .flatMap(map -> map.entrySet().stream())
  .collect(Collectors.toMap(
    Map.Entry::getKey, Map.Entry::getValue, Integer::sum));

assert merged.get("a") == 9;
assert merged.get("b") == 5;