如何在 Java 中增加映射值
一、簡介
在本教程中,我們將了解幾種增加與Map.
Maps
介面是 Java 中 Collections 框架的一部分,表示鍵值對的集合。一些常見的Map
實作包括HashMap
、 TreeMap
和LinkedHashMap
。
2. 問題陳述
讓我們來看一個範例,其中我們有一個句子作為String
輸入,並將句子中出現的每個字元的頻率儲存在Map
中。這是問題和輸出的範例:
sample sentence:
"the quick brown fox jumps over the lazy dog"
character frequency:
t: 2 times
h: 2 times
e: 3 times
q: 1 time
u: 2 times
...... and so on
該解決方案將涉及將字符頻率儲存在Map
中,其中鍵是字符,值是字符在給定String
中出現的總次數。由於String
中可能存在重複的字符,因此我們需要更新與鍵關聯的值,或多次增加鍵的當前值。
3. 解決方案
3.1.使用containsKey()
解決我們問題的最簡單的解決方案是遍歷String
,並且在每一步中,我們使用containsKey()
方法來檢查映射中的字元是否存在。如果鍵存在,我們將值加 1,或在映射中放置一個新條目,以鍵為字符,值為 1:
public Map<Character, Integer> charFrequencyUsingContainsKey(String sentence) {
Map<Character, Integer> charMap = new HashMap<>();
for (int c = 0; c < sentence.length(); c++) {
int count = 0;
if (charMap.containsKey(sentence.charAt(c))) {
count = charMap.get(sentence.charAt(c));
}
charMap.put(sentence.charAt(c), count + 1);
}
return charMap;
}
3.2.使用getOrDefault()
Java 8 在Map
中引入了getOrDefault()
方法,作為擷取與Map
中某個鍵關聯的值的簡單方法,或在該鍵不存在時擷取預設預設值:
V getOrDefault(Object key, V defaultValue);
我們將使用此方法來取得與String
(我們的鍵)目前字元關聯的值,並將該值加 1。這是containsKey()
的一個更簡單、更簡潔的替代方案:
public Map<Character, Integer> charFrequencyUsingGetOrDefault(String sentence) {
Map<Character, Integer> charMap = new HashMap<>();
for (int c = 0; c < sentence.length(); c++) {
charMap.put(sentence.charAt(c),
charMap.getOrDefault(sentence.charAt(c), 0) + 1);
}
return charMap;
}
3.3.使用merge()
Java 8 提供了merge()
方法,作為一種以Map
中更新的值覆寫與特定鍵關聯的值的方法。此方法接受一個鍵、一個值和一個重新映射函數,該函數用於計算將取代Map
中現有值的新更新值:
default V merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)
重新映射函數是BiFunction
,這表示它遵循 Java 8 的函數式程式設計範例。我們將所需的函數作為參數內聯傳送到merge()
方法以及參數,然後它執行所需的函數。
該方法期望我們定義一個預設值,該值將與重新映射函數的結果合併。因此,我們可以將重新映射函數編寫為預設值(即 1)與鍵存在的當前值的總和:
(a, b) -> a + b
我們也可以使用方法參考重寫相同的內容:
public Map<Character, Integer> charFrequencyUsingMerge(String sentence) {
Map<Character, Integer> charMap = new HashMap<>();
for (int c = 0; c < sentence.length(); c++) {
charMap.merge(sentence.charAt(c), 1, Integer::sum);
}
return charMap;
}
3.4.使用compute()
compute()
方法與merge()
方法的不同之處在於, compute()
方法對遺失的鍵沒有影響,不會拋出例外。該方法不接受任何值,並且重新映射函數應該決定新值:
V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
我們需要處理鍵缺失且值為 null 的情況,並將該值設為預設值 1。
public Map<Character, Integer> charFrequencyUsingCompute(String sentence) {
Map<Character, Integer> charMap = new HashMap<>();
for (int c = 0; c < sentence.length(); c++) {
charMap.compute(sentence.charAt(c), (key, value) -> (value == null) ? 1 : value + 1);
}
return charMap;
}
3.5.使用AtomicInteger
的incrementAndGet()
我們可以使用AtomicInteger
類型來儲存字元的頻率,而不是常規的Integer
包裝類別。這透過向程式碼引入原子性而使我們受益,並且我們可以使用AtomicInteger
類別的incrementAndGet()
方法。此方法對值執行原子遞增操作,相當於++i
操作。
此外,我們應該使用Map
的putIfAbsent()
方法來確保金鑰存在:
public Map<Character, AtomicInteger> charFrequencyWithGetAndIncrement(String sentence) {
Map<Character, AtomicInteger> charMap = new HashMap<>();
for (int c = 0; c < sentence.length(); c++) {
charMap.putIfAbsent(sentence.charAt(c), new AtomicInteger(0));
charMap.get(sentence.charAt(c)).incrementAndGet();
}
return charMap;
}
請注意,此方法的傳回類型使用AtomicInteger
。
此外,我們可以透過使用computeIfAbsent()
傳遞一個重新映射函數來以稍微更緊湊的方式重寫相同的程式碼:
public Map<Character, AtomicInteger> charFrequencyWithGetAndIncrementComputeIfAbsent(String sentence) {
Map<Character, AtomicInteger> charMap = new HashMap<>();
for (int c = 0; c < sentence.length(); c++) {
charMap.computeIfAbsent(sentence.charAt(c), k-> new AtomicInteger(0)).incrementAndGet();
}
return charMap;
}
3.6.使用番石榴
我們也可以使用Guava函式庫來解決這個問題。要使用 Guava,我們首先應該新增 Maven 庫作為依賴項:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
Guava 提供了一個AtomicLongMap
類,它有一個內建的getAndIncrement()
方法,可以在我們的用例中幫助我們:
public Map<Character, Long> charFrequencyUsingAtomicMap(String sentence) {
AtomicLongMap<Character> map = AtomicLongMap.create();
for (int c = 0; c < sentence.length(); c++) {
map.getAndIncrement(sentence.charAt(c));
}
return map.asMap();
}
4. 對方法進行基準測試
JMH 是我們可以用來對上述不同方法進行基準測試的工具。首先,我們包含相關的依賴項:
<span class="hljs-tag"><<span class="hljs-name">dependency</span>></span> <span class="hljs-tag"><<span class="hljs-name">groupId</span>></span>org.openjdk.jmh<span class="hljs-tag"></<span class="hljs-name">groupId</span>></span> <span class="hljs-tag"><<span class="hljs-name">artifactId</span>></span>jmh-core<span class="hljs-tag"></<span class="hljs-name">artifactId</span>></span> <span class="hljs-tag"><<span class="hljs-name">version</span>></span>1.37<span class="hljs-tag"></<span class="hljs-name">version</span>></span> <span class="hljs-tag"></<span class="hljs-name">dependency</span>></span> <span class="hljs-tag"><<span class="hljs-name">dependency</span>></span> <span class="hljs-tag"><<span class="hljs-name">groupId</span>></span>org.openjdk.jmh<span class="hljs-tag"></<span class="hljs-name">groupId</span>></span> <span class="hljs-tag"><<span class="hljs-name">artifactId</span>></span>jmh-generator-annprocess<span class="hljs-tag"></<span class="hljs-name">artifactId</span>></span> <span class="hljs-tag"><<span class="hljs-name">version</span>></span>1.37<span class="hljs-tag"></<span class="hljs-name">version</span>></span> <span class="hljs-tag"></<span class="hljs-name">dependency></span></span>
最新版本的JMH Core和JMH Annotation Processor可以在 Maven Central 中找到。
我們將每個方法包裝在Benchmark
帶註解的方法中,並傳遞一些附加參數:
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(value = 1, warmups = 1)
public void benchContainsKeyMap() {
IncrementMapValueWays im = new IncrementMapValueWays();
im.charFrequencyUsingContainsKey(getString());
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(value = 1, warmups = 1)
public void benchMarkComputeMethod() {
IncrementMapValueWays im = new IncrementMapValueWays();
im.charFrequencyUsingCompute(getString());
}
// and so on
在執行基準測試後,我們發現compute()
和merge()
方法的分數優於其他方法:
Benchmark Mode Cnt Score Error Units
BenchmarkMapMethodsJMH.benchContainsKeyMap avgt 5 50697.511 ± 25054.056 ns/op
BenchmarkMapMethodsJMH.benchMarkComputeMethod avgt 5 45124.359 ± 377.541 ns/op
BenchmarkMapMethodsJMH.benchMarkGuavaMap avgt 5 121372.968 ± 853.782 ns/op
BenchmarkMapMethodsJMH.benchMarkMergeMethod avgt 5 46185.990 ± 5446.775 ns/op
我們還應該注意到,這些結果略有不同,並且當密鑰的總體分佈不那麼大時可能不明顯。由於我們在範例中僅考慮英文字符和一些特殊字符,因此鍵的範圍上限為數百個。對於鍵數量可能很大的其他場景,效能將是一個大問題。
5. 多線程注意事項
我們上面討論的方法沒有任何多執行緒考慮。如果多個執行緒正在讀取輸入String
並更新共用Map
,我們肯定會容易受到增量操作中並發修改的影響。這將導致我們的最終結果計數不一致。
解決這個問題的方法之一是使用ConcurrentHashMap,
常規HashMap
的線程安全替代方案。 ConcurrentHashMap
提供對並發操作的支持,且不需要外部同步。
Map<Character, Integer> charMap = new ConcurrentHashMap<>();
在我們的解決方案中,我們還應該考慮我們正在執行的字元計數增量操作應該是原子的。為了確保這一點,我們應該使用原子方法,例如compute()
和merge().
讓我們寫一個測試案例來驗證我們的斷言。我們將使用並發哈希圖的共享實例來建立兩個執行緒。每個執行緒取得String
輸入的一部分並執行相同的操作:
@Test
public void givenString_whenUsingConcurrentMapCompute_thenReturnFreqMap() throws InterruptedException {
Map<Character, Integer> charMap = new ConcurrentHashMap<>();
Thread thread1 = new Thread(() -> {
IncrementMapValueWays ic = new IncrementMapValueWays();
ic.charFrequencyWithConcurrentMap("the quick brown", charMap);
});
Thread thread2 = new Thread(() -> {
IncrementMapValueWays ic = new IncrementMapValueWays();
ic.charFrequencyWithConcurrentMap(" fox jumps over the lazy dog", charMap);
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
Map<Character, Integer> expectedMap = getExpectedMap();
Assert.assertEquals(expectedMap, charMap);
}
六,結論
在本文中,我們研究了增加Map
條目值的不同方法。我們對這些方法的執行速度進行了基準測試,並研究如何編寫線程安全的解決方案。
與往常一樣,本文中使用的程式碼片段是可用的 在 GitHub 上。與往常一樣,本文中使用的程式碼片段是可用的 在 GitHub 上。