Java Map 中 putIfAbsent() 和computeIfAbsent() 的差異
1. 概述
Map
是一種常用的包含鍵值關聯的資料結構。 Java 提供了多種操作映射條目的方法。從Java 8開始,一些新成員加入了Map
家族。
putIfAbsent()
和computeIfAbsent()
是其中兩個。我們經常使用這兩種方法來新增條目。雖然它們乍看之下似乎很相似,但它們具有不同的行為和用例。
在本教程中,我們將討論這兩種方法之間的差異。
2. 簡介
在深入討論差異之前,讓我們先建立一些共同點。
putIfAbsent()
和computeIfAbsent()
都是Java中Map
介面提供的方法,它們有一個共同的目標:如果鍵不存在,則向映射添加鍵值對。當我們想要防止覆蓋現有條目時,此行為特別有用。
值得注意的是, 「缺席」包括兩種情況:
- 地圖中不存在該鑰匙
- 鍵存在,但關聯值為
null
但是,這兩種方法的行為並不相同。
在本教程中,我們不會一般性地討論如何使用這兩種方法。相反,我們只會關注他們的差異,我們會從三個角度來看他們的差異。
此外,我們將使用單元測試斷言來演示差異。接下來,讓我們快速設定測試範例。
3. 準備工作
由於我們將呼叫putIfAbsent()
和computeIfAbsent()
將條目插入映射中,因此我們首先為所有測試建立一個HashMap
實例:
private static final Map<String, String> MY_MAP = new HashMap<>();
@BeforeEach
void resetTheMap() {
MY_MAP.clear();
MY_MAP.put("Key A", "value A");
MY_MAP.put("Key B", "value B");
MY_MAP.put("Key C", "value C");
MY_MAP.put("Key Null", null);
}
正如我們所看到的,我們也使用@BeforeEach
註解建立了resetTheMap()
方法。此方法可確保MY_MAP
包含每個測試的相同鍵值對。
如果我們查看computeIfAbsent()'s
簽名,我們會看到該方法接受mappingFunction
函數來計算新值:
default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) { ... }
因此,我們創建Magic
類別來提供一些功能:
private Magic magic = new Magic();
Magic
類別非常簡單。它只提供兩種方法: nullFunc()
總是傳回null,
strFunc()
傳回非空字串。
class Magic {
public String nullFunc() {
return null;
}
public String strFunc(String input) {
return input + ": A nice string";
}
}
公平地說,在我們的測試中, putIfAbsent()
和computeIfAbsent()
中使用的所有新映射值都來自magic
的方法。
4. key不存在時的回傳值
這兩種方法的共同點是「 IfAbsent”
。我們已經討論過「缺席」的定義。第一個差異是「不存在」情況下兩個方法的回傳值。
我們先來看看putIfAbsent()
方法:
// absent: putting new key -> null
String putResult = MY_MAP.putIfAbsent("new key1", magic.nullFunc());
assertNull(putResult);
// absent: putting new key -> not-null
putResult = MY_MAP.putIfAbsent("new key2", magic.strFunc("new key2"));
assertNull(putResult);
// absent: existing key -> null (original)
putResult = MY_MAP.putIfAbsent("Key Null", magic.strFunc("Key Null"));
assertNull(putResult);
從上面的測試可以看出,**當不存在時, putIfAbsent()
方法總是傳回null,
**無論新值是否為null
。
接下來,讓我們使用computeIfAbsent()
執行相同的測試,看看它回傳什麼:
// absent: computing new key -> null
String computeResult = MY_MAP.computeIfAbsent("new key1", k -> magic.nullFunc());
assertNull(computeResult);
// absent: computing new key -> not-null
computeResult = MY_MAP.computeIfAbsent("new key2", k -> magic.strFunc(k));
assertEquals("new key2: A nice string", computeResult);
// absent: existing key -> null (original)
computeResult = MY_MAP.computeIfAbsent("Key Null", k -> magic.strFunc(k));
assertEquals("Key Null: A nice string", computeResult);
如測試所示,當不存在時, computeIfAbsent()
方法會傳回mappingFunction
的回傳值。
5. 當新值為null
時
我們知道HashMap
允許空值。接下來,我們試著在MY_MAP
插入一個null
值,看看這兩個方法的行為如何。
首先,讓我們來看看putIfAbsent()
:
assertEquals(4, MY_MAP.size()); // initial: 4 entries
MY_MAP.putIfAbsent("new key", magic.nullFunc());
assertEquals(5, MY_MAP.size());
assertTrue(MY_MAP.containsKey("new key")); // new entry has been added to the map
assertNull(MY_MAP.get("new key"));
因此,當目標映射中不存在該鍵時, putIfAbsent()
總是會在映射中新增一個新的鍵值對,即使新值為null.
現在, computeIfAbsent()'s
了:
assertEquals(4, MY_MAP.size()); // initial: 4 entries
MY_MAP.computeIfAbsent("new key", k -> magic.nullFunc());
assertEquals(4, MY_MAP.size());
assertFalse(MY_MAP.containsKey("new key")); // <- no new entry added to the map
正如我們所看到的,如果mappingFunction
傳回null, computeIfAbsent()
拒絕將鍵值對加入到映射中。
6.「 compute
」是Lazy,「 put
」是Eager
我們已經討論了這兩種方法之間的兩個區別。接下來,讓我們看看當「 value
」部分由方法或函數提供時,這兩個方法的行為是否相同。
像往常一樣,我們先來看看putIfAbsent()
方法:
Magic spyMagic = spy(magic);
// key existent
MY_MAP.putIfAbsent("Key A", spyMagic.strFunc("Key A"));
verify(spyMagic, times(1)).strFunc(anyString());
// key absent
MY_MAP.putIfAbsent("new key", spyMagic.strFunc("new key"));
verify(spyMagic, times(2)).strFunc(anyString());
正如我們在上面的測試中所看到的,我們使用 Mockito spy
magic
物件來驗證strFunc()
方法是否被呼叫。測試發現,無論key是否存在, strFunc()
方法都會被呼叫。
接下來,我們來看看computeIfAbsent()
方法如何處理mappingFunction:
Magic spyMagic = spy(magic);
// key existent
MY_MAP.computeIfAbsent("Key A", k -> spyMagic.strFunc(k));
verify(spyMagic, never()).strFunc(anyString()); // the function wasn't called
// key absent
MY_MAP.computeIfAbsent("new key", k -> spyMagic.strFunc(k));
verify(spyMagic, times(1)).strFunc(anyString());
如測試所示, computeIfAbsent()
作用正如其名稱所暗示的那樣,僅在「不存在」的情況下計算mappingFunction
。
另一個日常用例是當我們使用Map<String, List<String>>
之類的東西時:
-
putIfAbsent(aKey, new ArrayList()) –
無論“aKey”
是否不存在,都會建立一個新的ArrayList
對象 -
computeIfAbsent(aKey, k -> new ArrayList()) –
僅當“aKey”
不存在時才會建立新的ArrayList
實例
因此,如果鍵不存在,我們應該在直接添加鍵值對時使用putIfAbsent()
,而不進行任何計算。另一方面,當我們需要計算值並在鍵不存在時添加鍵值對時,可以使用computeIfAbsent()
。
七、結論
在本文中,我們透過範例討論了putIfAbsent()
和computeIfAbsent()
之間的差異。了解這種差異對於在我們的程式碼中做出正確的選擇至關重要。
與往常一樣,範例的完整原始程式碼可在 GitHub 上取得。