在 Java 中尋找具有重複值的映射鍵
1. 概述
Map
是一種用於鍵值儲存和檢索的高效結構。
在本教程中,我們將探索在Set
中尋找具有重複值的映射鍵的不同方法。換句話說,我們的目標是將Map<K, V>
轉換為Map<V, Set<K>>
。
2. 準備輸入範例
讓我們考慮一個場景,我們擁有一個儲存開發人員與其主要工作的作業系統之間的關聯的映射:
Map<String, String> INPUT_MAP = Map.of(
"Kai", "Linux",
"Eric", "MacOS",
"Kevin", "Windows",
"Liam", "MacOS",
"David", "Linux",
"Saajan", "Windows",
"Loredana", "MacOS"
);
現在,我們的目標是找到具有重複值的鍵,將使用相同作業系統的不同開發人員分組到一個Set
中:
Map<String, Set<String>> EXPECTED = Map.of(
"Linux", Set.of("Kai", "David"),
"Windows", Set.of("Saajan", "Kevin"),
"MacOS", Set.of("Eric", "Liam", "Loredana")
);
雖然我們主要處理非空鍵和值,但值得注意的是, HashMap
允許同時null
值和一個null
鍵。因此,讓我們基於INPUT_MAP
建立另一個輸入來覆蓋涉及null
鍵和空值的場景:
Map<String, String> INPUT_MAP_WITH_NULLS = new HashMap<String, String>(INPUT_MAP) {{
put("Tom", null);
put("Jerry", null);
put(null, null);
}};
結果,我們期望得到如下圖:
Map<String, Set<String>> EXPECTED_WITH_NULLS = new HashMap<String, Set<String>>(EXPECTED) {{
put(null, new HashSet<String>() {{
add("Tom");
add("Jerry");
add(null);
}});
}};
我們將在本教程中討論各種方法。接下來,讓我們深入研究程式碼。
3. Java 8之前
在解決問題的第一個想法中,我們初始化一個空的HashMap<V, Set<K>>
並在經典的 for-each 循環中使用它來填充它:
static <K, V> Map<V, Set<K>> transformMap(Map<K, V> input) {
Map<V, Set<K>> resultMap = new HashMap<>();
for (Map.Entry<K, V> entry : input.entrySet()) {
if (!resultMap.containsKey(entry.getValue())) {
resultMap.put(entry.getValue(), new HashSet<>());
}
resultMap.get(entry.getValue())
.add(entry.getKey());
}
return resultMap;
}
如程式碼所示, transformMap()
是一個通用方法。在循環中,我們使用每個條目的值作為結果映射的鍵,並將與相同鍵關聯的值收集到Set
中。
無論輸入映射是否包含null
鍵或值, transformMap()
都會執行此操作:
Map<String, Set<String>> result = transformMap(INPUT_MAP);
assertEquals(EXPECTED, result);
Map<String, Set<String>> result2 = transformMap(INPUT_MAP_WITH_NULLS);
assertEquals(EXPECTED_WITH_NULLS, result2);
4.Java 8
Java 8 帶來了大量新功能,使用 Stream API 等工具簡化了集合作業。此外,它還透過諸如computeIfAbsent()
之類的附加功能豐富了Map
介面。
在本節中,我們將使用 Java 8 中引入的功能來解決該問題。
4.1.將 Stream 與groupingBy()
一起使用
Stream API 隨附的groupingBy()
收集器允許我們根據分類器函數對流元素進行分組,並將它們儲存為Map
中對應鍵下的清單。
因此,我們可以建構一個Entry<K, V>
流,並使用groupingBy()
將流收集到Map<V, List<K>>
中。這與我們的要求非常吻合。剩下的唯一步驟是將List<K>
轉換為Set<K>
。
接下來,讓我們實作這個想法,看看如何將List<K>
轉換為Set<K>
:
Map<String, Set<String>> result = INPUT_MAP.entrySet()
.stream()
.collect(groupingBy(Map.Entry::getValue, mapping(Map.Entry::getKey, toSet())));
assertEquals(EXPECTED, result);
正如我們在上面的程式碼中所看到的,我們使用mapping()
收集器將**分組結果( List
)對應到不同的類型( Set
)。**
與我們的transformMap()
解決方案相比, groupingBy()
方法更容易閱讀和理解。此外,作為一種功能性解決方案,這種方法使我們能夠流暢地應用進一步的操作。
然而, groupingBy()
解決方案在轉換我們的INPUT_MAP_WITH_NULLS
輸入時會引發NullPointerException
:
assertThrows(NullPointerException.class, () -> INPUT_MAP_WITH_NULLS.entrySet()
.stream()
.collect(groupingBy(Map.Entry::getValue, mapping(Map.Entry::getKey, toSet()))));
這是因為**groupingBy()
無法處理null
鍵:**
public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,...){
...
BiConsumer<Map<K, A>, T> accumulator = (m, t) -> {
K key = Objects.requireNonNull(classifier.apply(t), "element cannot be mapped to a null key");
...
}
接下來,讓我們看看是否可以為非空和可為空的鍵或值建立解決方案。
4.2.使用forEach()
和computeIfAbsent()
Java 8 的forEach()
提供了一個簡潔的方法來迭代集合。如果鍵尚未與值關聯或對應到null
, computeIfAbsent()
會計算指定鍵的新值。
接下來,我們結合這兩種方法來解決我們的問題:
Map<String, Set<String>> result = new HashMap<>();
INPUT_MAP.forEach((key, value) -> result.computeIfAbsent(value, k -> new HashSet<>())
.add(key));
assertEquals(EXPECTED, result);
此解決方案遵循與**transformMap() .**
相同的邏輯。然而,借助 Java 8 的強大功能,實現更加優雅和緊湊。
此外, forEach()
+ computeIfAbsent()
解決方案適用於包含null
鍵或值的對應:
Map<String, Set<String>> result2 = new HashMap<>();
INPUT_MAP_WITH_NULLS.forEach((key, value) -> result2.computeIfAbsent(value, k -> new HashSet<>())
.add(key));
assertEquals(EXPECTED_WITH_NULLS, result2);
5.利用Guava Multimap
Guava 是一個廣泛使用的庫,它提供了**Multimap
接口,可以將單個鍵與多個值關聯起來。**
現在,讓我們開發基於Multimaps
解決方案來解決我們的問題。
5.1.使用 Stream API 和Multimap
收集器
我們已經了解了 Java Stream API 如何幫助我們打造緊湊、簡潔且功能齊全的實作。 Guava 提供了Multimap
收集器,例如Multimaps.toMultimap()
,它允許Multimap
**方便地**將流中的元素收集到 Multimap 中。
接下來,我們來看看如何結合流和Multimaps.toMultimap()
來解決問題:
Map<String, Set<String>> result = INPUT_MAP.entrySet()
.stream()
.collect(collectingAndThen(Multimaps.toMultimap(Map.Entry::getValue, Map.Entry::getKey, HashMultimap::create), Multimaps::asMap));
assertEquals(EXPECTED, result);
toMultimap()
方法採用三個參數:
- 取得密鑰的
Function
- 取得值的
Function
- Multimap
Supplier
– 提供Multimap
物件。在這個例子中,我們使用HashMultimap
來取得SetMultimap.
因此, toMultimap()
將Map.Entry<K, V>
物件收集到SetMultimap<V, K>
中。然後,我們使用collectingAndThen()
來執行Multimaps.asMap()
,將收集到的SetMultimap<V, K>
轉換為Map<V, Set<K>>
。
此方法適用於持有空鍵或值的對應:
Map<String, Set<String>> result2 = INPUT_MAP_WITH_NULLS.entrySet()
.stream()
.collect(collectingAndThen(Multimaps.toMultimap(Map.Entry::getValue, Map.Entry::getKey, HashMultimap::create), Multimaps::asMap));
assertEquals(EXPECTED_WITH_NULLS, result2);
5.2.使用invertFrom()
和forMap()
或者,我們可以使用Multimaps.invertFrom()
和Multimaps.forMap()
來實現我們的目標:
SetMultimap<String, String> multiMap = Multimaps.invertFrom(Multimaps.forMap(INPUT_MAP), HashMultimap.create());
Map<String, Set<String>> result = Multimaps.asMap(multiMap);
assertEquals(EXPECTED, result);
SetMultimap<String, String> multiMapWithNulls = Multimaps.invertFrom(Multimaps.forMap(INPUT_MAP_WITH_NULLS), HashMultimap.create());
Map<String, Set<String>> result2 = Multimaps.asMap(multiMapWithNulls);
assertEquals(EXPECTED_WITH_NULLS, result2);
forMap()
方法傳回Multimap<K, V>
**Map<K, V>** .
隨後,顧名思義, invertFrom()
將Multimap<K, V>
視圖轉換為**Multimap<V, K>**
並將其傳送到作為第二個參數提供的目標Multimap
。最後,我們再次使用Multimaps.asMap()
將Multimap<V, K>
轉換為Map<V, Set<K>>
。
此外,正如我們所看到的,這種方法適用於具有空鍵或值的映射。
六,結論
在本文中,我們探索了在Set
中尋找具有重複值的映射鍵的各種方法,包括經典的基於循環的解決方案、基於 Java 8 的解決方案以及使用 Guava 的方法。
與往常一樣,範例的完整原始程式碼可在 GitHub 上取得。