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返回 nullcomputeIfAbsent()会拒绝将键值对添加到映射中。

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