在 Java 中將映射流扁平化為單一映射
1. 概述
自 Java 8 推出以來,處理資料流已成為 Java 開發中常見的任務。通常,這些流包含複雜的結構(例如映射),這在進一步處理它們時可能會帶來挑戰。
在本教程中,我們將探討如何將地圖串流展平為單一地圖。
2.問題介紹
在深入研究解決方案之前,讓我們先澄清一下「展平地圖流」的含義。本質上,我們希望將映射流轉換為單一映射,其中包含流中每個映射的所有鍵值對。
像往常一樣,一個例子可以幫助我們快速理解問題。假設我們有三個儲存玩家姓名和分數之間關聯的對應:
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<String, Integer> expectedMap = new HashMap<String, Integer>() {{
put("Saajan", 35);
put("Liam", 100);
put("Kai", 92);
put("Eric", 42);
put("Kevin", 77);
}};
值得一提的是,由於我們使用的是HashMap
對象,因此無法保證最終結果中的條目順序。
此外,流中的映射可能包含重複的鍵和null
值。稍後,我們將擴展範例以涵蓋本教程中的這些場景。
接下來,讓我們深入研究程式碼。
3. 使用flatMap()
和Collectors.toMap()
合併地圖的一種方法是使用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()
收集器需要兩個函數作為參數:一個用於提取鍵 ( Map.Entry::getKey
),另一個用於提取值 ( Map.Entry::getValue
)。這裡,我們使用方法參考來表示這兩個函數。這些函數應用於流中的每個條目以建構結果映射。
4. 處理重複密鑰
我們學習如何使用toMap()
收集器將HashMaps
流合併到一個映射中。然而,如果映射流包含重複的鍵,這種方法將會失敗。例如,如果我們將具有重複鍵“Kai”
的新映射新增至流中,則會拋出IllegalStateException
:
Map<String, Integer> playerMap4 = new HashMap<String, Integer>() {{
put("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<String, Integer> expectedMap = new HashMap<String, Integer>() {{
put("Saajan", 35);
put("Liam", 100);
put("Kai", 92); // <- max of 92 and 76
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()** .
這確保了當出現重複鍵時,最終映射中的結果值將是與這些鍵關聯的兩個值中較大的一個。
5. 處理null
值
我們已經看到Collectors.toMap()
可以方便地將條目收集到單一映射中。但是, Collectors.toMap()
方法無法將null
處理為 map 的值。如果任何映射條目的值為 null,我們的解決方案將引發NullPointerException
null.
讓我們新增一個地圖來驗證:
Map<String, Integer> playerMap5 = new HashMap<String, Integer>() {{
put("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
值。這一次,我們仍然希望重複的玩家名字能夠獲得更高的分數。此外,我們將null
視為最低分數。然後,我們的預期結果如下:
Map<String, Integer> expectedMap = new HashMap<String, Integer>() {{
put("Saajan", 35);
put("Liam", 100);
put("Kai", 92); // <- max of 92, 76, and null
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);
}
接下來我們來解決這個問題。
5.1.使用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);
5.2.使用groupingBy()
、 mapping()
和reducing()
flatMap() + forEach()
解決方案很簡單。然而,它不是一種函數式方法,需要我們寫一些樣板合併邏輯。
或者,我們可以結合groupingBy()
、 [mapping()](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Collectors.html#mapping(java.util.function.Function,java.util.stream.Collector))
和reducing()
收集器來從功能上解決這個問題:
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(…)) –
按鍵將映射條目分組以取得key -> Entries
結構,這些Entries
將轉到mapping()
-
mapping(Map.Entry::getValue, reducing(…))
-使用Map.Entry::getValue
將每個Entry
映射到Integer
**並將Integer
值移交給另一個下游收集器reducing()
下游**收集器 -
reducing(null, this::maxInteger)
– 下游收集器透過執行maxInteger
函數來應用減少重複鍵的Integer
數值的邏輯,該函數傳回兩個整數值中的最大值
六,結論
在本文中,我們深入研究了在 Java 中合併映射流,並提出了幾種處理各種場景的方法,包括合併包含重複鍵的映射和優雅地處理null
值。
與往常一樣,本文中的範例可以 在 GitHub 上找到。