簡體   English   中英

使用Java 8 Stream API合並地圖列表

[英]Merge list of maps using Java 8 Stream API

我正在嘗試學習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;
}

這里的技巧是正確收集內部地圖。 工作流程為:

  • 將地圖List<Map<String, Map<String, Long>>>的列表平面映射到地圖項Stream<Map.Entry<String, Map<String, Long>>>
  • 按每個條目的鍵分組,對於映射到相同鍵的值,將兩個映射合並在一起。

通過合並來收集地圖將理想地保證一個flatMapping收集器,盡管Java 9中將存在它 ,但不幸的是Java 8中不存在該flatMapping器(請參閱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; }
                        )
                    ));
}

首先,轉換為使用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;
}

這從內部循環中消除了很多邏輯。


下面的代碼是錯誤的 ,這里留給我參考。

轉換為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值。

因此,我們然后在K1值上使用collect(groupingBy) ,為我們提供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是“ fold”操作。 a設置為new HashMap<>()b a兩個值:函數的上一個應用程序的初始值或結果以及List的當前項。 因此,對於List每個項目,請使用Map.merge組合值。

我會說這種方法或多或少難以辨認-您將無法在幾個小時的時間內解密,更不用說幾天了。

我從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;

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM