1. 概述
在Java中,哈希映射(Map) 是一种常用的数据结构,用于存储键值对。自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()
的方法签名,它接受一个mappingFunction
函数来计算新值:
default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) { ... }
因此,让我们创建一个Magic
类提供一些函数:
private Magic magic = new Magic();
Magic
类很简单,只提供两个方法:nullFunc()
始终返回 null
,而strFunc()
返回一个非空字符串。
公平地说,在我们的测试中,putIfAbsent()
和computeIfAbsent()
都将使用magic
类中的新值。
4. 当键不存在时的返回值
两个方法的共同点是“如果不存在”。我们已经讨论了“不存在”的定义。第一个区别是两种方法在“不存在”情况下返回值的不同。
首先,看看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
允许 null
值。那么,让我们尝试将 null
值插入到MY_MAP
中,看看这两个方法的行为如何。
首先,看看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()
:
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. “计算”是惰性的,“插入”是立即的
我们已经讨论了两种方法之间的两个差异。接下来,让我们看看当由方法或函数提供“值”部分时,两种方法是否表现一致。
如往常一样,我们先来看看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(/mockito-spy)来验证strFunc()
方法是否被调用。测试结果显示,无论键是否存在,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()
可以在键不存在且需要计算值时使用。
7. 总结
本文通过示例讨论了putIfAbsent()
和computeIfAbsent()
之间的差异。了解这个区别对于在代码中做出正确的选择至关重要。
如往常一样,示例的完整源代码可在GitHub上找到:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-collections-maps-7。