Java中將多個Map扁平化為單個Map

banq發表於2024-03-27

自 Java 8 推出以來,處理資料流已成為 Java 開發中的一項常見任務。通常,這些流包含複雜的結構(例如對映),這在進一步處理它們時可能會帶來挑戰。

在本教程中,我們將探討如何將地Map對映流展平為單個Map對映。

在深入研究解決方案之前,讓我們先澄清一下“展平Map流”的含義。本質上,我們希望將對映流轉換為單個Map對映,其中包含流中每個Map對映的所有鍵值對。

像往常一樣,一個例子可以幫助我們快速理解問題。假設我們有三個儲存玩家姓名和分數之間關聯的Map對映:

Map<String, Integer> playerMap1 = new HashMap<String, Integer>() {{
    put("Kai", 92);
    put("Liam", 100);
}};
Map<String, Integer> playerMap2 = new HashMap<String, Integer>() {{
    put("Eric", 42);
    put("Kevin", 77);
}};
Map<String, Integer> playerMap3 = new HashMap<String, Integer>() {{
    put("Saajan", 35);
}};
我們的輸入是包含這些對映的流。為簡單起見,我們將在本教程中使用Stream.of(playerMap1, playerMap2 , …)構建輸入流。然而,值得注意的是,流不一定具有定義的遇到順序。

現在,我們的目標是將包含上述三個Map對映的流合併為一個名稱-分數Map對映:

Map<String, Integer> expectedMap = new HashMap<String, Integer>() {{
    put("Saajan", 35);
    put("Liam", 100);
    put("Kai", 92);
    put("Eric", 42);
    put("Kevin", 77);
}};
值得一提的是,由於我們使用的是HashMap物件,因此無法保證最終結果中的條目順序。

此外,流中的Map對映可能包含重複的鍵和空值。稍後,我們將擴充套件示例以涵蓋本教程中的這些場景。

接下來,讓我們深入研究程式碼。

使用flatMap()和Collectors.toMap()
合併Map的一種方法是使用flatMap()方法和toMap ()收集器:

Map<String, Integer> mergedMap = Stream.of(playerMap1, playerMap2, playerMap3)
  .flatMap(map -> map.entrySet()
    .stream())
  .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
assertEquals(expectedMap, mergedMap);

  • 在上面的程式碼中,flatMap ()方法將流中的每個對映展平為其條目流。
  • 然後,我們使用toMap()收集器將流的元素收集到單個對映中。

toMap ()收集器需要兩個函式作為引數:
  1. 一個用於提取鍵 ( Map.Entry::getKey ),
  2. 另一個用於提取值 ( Map.Entry::getValue )。

這裡,我們使用方法引用來表示這兩個函式。這些函式應用於流中的每個條目以構造結果對映。

處理重複鍵
我們學習瞭如何使用toMap()收集器將HashMap流合併到一個對映中。然而,如果Map對映流包含重複的鍵,這種方法將會失敗。例如,如果我們將具有重複鍵“Kai”的新對映新增到流中,則會丟擲IllegalStateException:

Map<String, Integer> playerMap4 = new HashMap<String, Integer>() {{
    put(<font>"Kai", 76);
}};
assertThrows(IllegalStateException.class, () -> Stream.of(playerMap1, playerMap2, playerMap3, playerMap4)
  .flatMap(map -> map.entrySet()
    .stream())
  .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)),
"Duplicate key Kai (attempted merging values 92 and 76)");

為了解決重複鍵的問題,我們可以將合併函式作為第三個引數傳遞給toMap()方法來處理與重複鍵關聯的值。

對於重複鍵場景,我們可能有不同的合併要求。在我們的示例中,一旦出現重複名稱,我們希望選擇較高的分數。因此,我們的目標是得到這Map:

Map<String, Integer> expectedMap = new HashMap<String, Integer>() {{
    put(<font>"Saajan", 35);
    put(
"Liam", 100);
    put(
"Kai", 92); // <- max of 92 and 76<i>
    put(
"Eric", 42);
    put(
"Kevin", 77);
}};

接下來我們看看如何實現:

Map<String, Integer> mergedMap = Stream.of(playerMap1, playerMap2, playerMap3, playerMap4)
  .flatMap(map -> map.entrySet()
    .stream())
  .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Integer::max));
 
assertEquals(expectedMap, mergedMap);

如程式碼中所示,我們使用方法引用Integer::max作為toMap()中的合併函式。這確保了當出現重複鍵時,最終對映中的結果值將是與這些鍵關聯的兩個值中較大的一個。

處理空值
我們已經看到Collectors.toMap()可以方便地將條目收集到單個對映中。但是,Collectors.toMap ()方法無法將null處理為 map 的值。如果任何對映條目的值為null,我們的解決方案將引發NullPointerException 。

讓我們新增一個新Map來驗證:

Map<String, Integer> playerMap5 = new HashMap<String, Integer>() {{
    put(<font>"Kai", null);
    put(
"Jerry", null);
}};
assertThrows(NullPointerException.class, () -> Stream.of(playerMap1, playerMap2, playerMap3, playerMap4, playerMap5)
  .flatMap(map -> map.entrySet()
    .stream())
  .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Integer::max)));

現在,輸入流中的對映包含重複的鍵和空值。這一次,我們仍然希望重複的玩家名字能夠獲得更高的分數。此外,我們將null視為最低分數。 然後,我們的預期結果如下:

Map<String, Integer> expectedMap = new HashMap<String, Integer>() {{
    put(<font>"Saajan", 35);
    put(
"Liam", 100);
    put(
"Kai", 92); // <- max of 92, 76, and null<i>
    put(
"Eric", 42);
    put(
"Kevin", 77);
    put(
"Jerry", null);
}};

由於Integer.max()無法處理null值,因此我們建立一個 null 安全方法來從兩個可為 null 的Integer物件中獲取較大的值:

private Integer maxInteger(Integer int1, Integer int2) {
    if (int1 == null) {
        return int2;
    }
    if (int2 == null) {
        return int1;
    }
    return max(int1, int2);
}

接下來我們來解決這個問題。

使用flatMap()和forEach()
解決這個問題的一個簡單方法是首先初始化一個空對映,然後在forEach()中將put()所需的鍵值對放入其中:

Map<String, Integer> mergedMap = new HashMap<>();
Stream.of(playerMap1, playerMap2, playerMap3, playerMap4, playerMap5)
  .flatMap(map -> map.entrySet()
    .stream())
  .forEach(entry -> {
      String k = entry.getKey();
      Integer v = entry.getValue();
      if (mergedMap.containsKey(k)) {
          mergedMap.put(k, maxInteger(mergedMap.get(k), v));
      } else {
          mergedMap.put(k, v);
      }
    });
assertEquals(expectedMap, mergedMap);

使用groupingBy()、  mapping()和reducing()
flatMap () + forEach()解決方案很簡單。然而,它不是一種函式式方法,需要我們編寫一些樣板合併邏輯。

或者,我們可以結合groupingBy()、mapping()和ducing() 收集器來從功能上解決這個問題:

Map<String, Integer> mergedMap = Stream.of(playerMap1, playerMap2, playerMap3, playerMap4, playerMap5)
  .flatMap(map -> map.entrySet()
    .stream())
  .collect(groupingBy(Map.Entry::getKey, mapping(Map.Entry::getValue, reducing(null, this::maxInteger))));
 
assertEquals(expectedMap, mergedMap);

如上面的程式碼所示,我們在collect()方法中組合了三個收集器。接下來,讓我們快速瞭解一下他們是如何協同工作的:
  • groupingBy(Map.Entry::getKey, mapping(…)) –按鍵對對映條目進行分組以獲取鍵 -> 條目結構,這些條目將轉到對映()
  • Map(Map.Entry :: getValue,reducing(…) -使用Map.Entry :: getValue將每個 Entry對映到Integer並將Integer值移交給另一個下游收集器reducing()的下游收集器
  • reduce(null, this::maxInteger) – 下游收集器透過執行maxInteger函式來應用減少重複鍵的整數值的邏輯,該函式返回兩個整數值中的最大值

相關文章