2016-06-28 121 views
1

我正在嘗試學習Java 8 Stream以及何時嘗試將某些函數轉換爲java8來練習。我遇到了一個問題。使用Java 8 Stream API合併地圖列表

我很好奇,如何將後續代碼轉換爲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; 
} 
+0

所以所有的地圖都是一樣的 - 即在兩個層次都有相同的密鑰? –

+4

這不是一個代碼翻譯服務。你需要告訴我們你已經試過的東西,這樣我們才能告訴你你做錯了什麼。 – explv

+2

您應該首先重新考慮您的原始方法,即在外部循環中循環的內容以及內部循環中的內容。 – Holger

回答

5

這裏的訣竅是收集正確的內部地圖。工作流程將爲:

  • 將地圖列表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

我偷了你的'龍::總和' - 我一直忘記存在。我認爲你的'Stream'方法總體上更好,因爲它不會產生中間'List'結果 - 雖然你和我的都是完全難以辨認的。我會主張使用Java 8'Map'方法的'foreach'方法... –

+1

@BoristheSpider是的,使用好的舊for循環可能通常會更好。 – Tunaki

+0

對不起,我不明白你的方法,所以我在函數中添加printf。它似乎是lambda(r1,r2) - > {r2.forEach((k,v) - > r1.merge(k,v,Long :: sum));返回r1; }根本不要執行。你能解釋Collector.of的工作原理嗎?我無法通過搜索獲得有關它的很多信息。 – yunfan

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是「摺疊」操作。 a設置爲new HashMap<>()b取兩個值:函數的上一次應用的初始值或結果以及List中的當前項目。因此,我們對List中的每個項目使用Map.merge來組合這些值。

我會說這種方法或多或少難以辨認 - 你幾個小時內就無法破譯它,更別說幾天了。

+2

當你修改函數的參數時不要使用'reduce'。 – Holger

+0

@Holger。我不明白你的意思。看起來他的代碼不會修改函數的參數。 – yunfan

+1

@yunfan:傳遞給'reduce'的函數通過對'r2'的每個映射調用'merge'來修改映射'l2'。 – Holger

0

我把flatMap(e -> e.entrySet().stream())部分來自Tunaki,但用於收集較短的變體:

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;