系列中的上一篇
当前教程
使用 Lambda 表达式处理 Map 值

系列中的上一篇: 管理 Map 的内容

系列中的下一篇: 使用 SortedMap 和 NavigableMap 保持键排序

使用 Lambda 表达式处理 Map 值

 

使用 Map 的内容

The Map 接口有一个 forEach() 方法,其工作方式与 forEach() 方法在 Iterable 接口上的工作方式相同。区别在于此 forEach() 方法接受一个 BiConsumer 作为参数,而不是一个简单的 Consumer.

让我们创建一个简单的 map 并打印出其内容。

Map<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");

map.forEach((key, value) -> System.out.println(key + " :: " + value));

此代码产生以下结果

1 :: one
2 :: two
3 :: three

 

替换值

The Map 接口为您提供了三种方法来用另一个值替换与键绑定的值。

第一个是 replace(key, value),它会盲目地用新值替换现有值。这相当于 put-if-present 操作。此方法返回从您的 map 中删除的值。

如果您需要更精细的控制,那么您可以使用此方法的重载,它将现有值作为参数:replace(key, existingValue, newValue)。在这种情况下,只有当现有值与新值匹配时才会替换现有值。此方法在替换发生时返回 true

The Map 接口还有一个方法可以使用 BiFunction 替换 map 中的所有值。此 BiFunction 是一个重新映射函数,它将键和值作为参数,并返回一个新值,该新值将替换现有值。对该方法的调用会在您的 map 的所有键/值对上内部迭代。

以下示例展示了如何使用此 replaceAll() 方法

Map<Integer, String> map = new HashMap<>();

map.put(1, "one");
map.put(2, "two");
map.put(3, "three");

map.replaceAll((key, value) -> value.toUpperCase());
map.forEach((key, value) -> System.out.println(key + " :: " + value));

运行此代码会产生以下结果

1 :: ONE
2 :: TWO
3 :: THREE

 

计算值

The Map 接口为您提供了第三种模式,以三种方法的形式将键值对添加到 map 或修改 map 的现有值:compute()computeIfPresent()computeIfAbsent().

这三种方法接受以下参数

compute() 的情况下,重新映射双函数使用两个参数调用。第一个是键,第二个是现有值(如果有),或者 null(如果没有)。您的重新映射双函数可以使用空值调用。

对于 computeIfPresent(),如果该键绑定到一个值并且该值不为空,则会调用重新映射函数。如果该键绑定到一个空值,则不会调用重新映射函数。您的重新映射函数不能使用空值调用。

对于 computeIfAbsent(),因为没有值绑定到该键,所以重新映射函数实际上是一个简单的 Function,它将键作为参数。如果该键不存在于 map 中或绑定到一个空值,则会调用此函数。

在所有情况下,如果您的双函数(或函数)返回一个空值,则该键将从 map 中删除:不会为该键创建映射。使用这三种方法之一,无法将具有空值的键/值对放入 map 中。

在所有情况下,返回的值是绑定到 map 中该键的新值,或者如果重新映射函数返回空值,则为 null。值得指出的是,这种语义不同于 put() 方法。The put() 方法返回前一个值,而 compute() 方法返回新值。

一个非常有趣的 computeIfAbsent() 方法的用例是创建以列表作为值的 map。假设您有以下字符串列表:[one two three four five six seven]。您需要创建一个 map,其中键是该列表中单词的长度,值是这些单词的列表。您需要创建以下 map

3 :: [one, two, six]
4 :: [four, five]
5 :: [three, seven]

如果没有 compute() 方法,您可能会这样写

List<String> strings = List.of("one", "two", "three", "four", "five", "six", "seven");
Map<Integer, List<String>> map = new HashMap<>();
for (String word: strings) {
    int length = word.length();
    if (!map.containsKey(length)) {
        map.put(length, new ArrayList<>());
    }
    map.get(length).add(word);
}

map.forEach((key, value) -> System.out.println(key + " :: " + value));

运行此代码会产生预期结果

3 :: [one, two, six]
4 :: [four, five]
5 :: [three, seven]

顺便说一下,您可以使用 putIfAbsent() 来简化此 for 循环

for (String word: strings) {
    int length = word.length();
    map.putIfAbsent(length, new ArrayList<>());
    map.get(length).add(word);
}

但是使用 computeIfAbsent() 可以使此代码变得更好

for (String word: strings) {
    int length = word.length();
    map.computeIfAbsent(length, key -> new ArrayList<>())
       .add(word);
}

此代码是如何工作的?

  • 如果该键不在 map 中,则会调用映射函数,该函数会创建一个空列表。此列表由 computeIfAbsent() 方法返回。这是代码向其中添加 word 的空列表。
  • 如果该键在 map 中,则不会调用映射函数,并且会返回绑定到该键的当前值。这是您需要向其中添加 word 的部分填充的列表。

此代码比 putIfAbsent() 代码效率更高,主要是因为在这种情况下,空列表仅在需要时创建。The putIfAbsent() 调用需要一个现有的空列表,该列表仅在该键不在 map 中时使用。在您需要按需创建添加到 map 的对象的情况下,应优先使用 computeIfAbsent() 而不是 putIfAbsent().

 

合并值

如果您的地图的值是其他值的聚合,则 computeIfAbsent() 模式非常有效。但是,支持此聚合的结构存在限制:它必须是可变的。对于 ArrayList 来说就是这样,这也是您编写的代码所做的:它将您的值添加到 ArrayList 中。

假设您需要创建一个单词的串联,而不是创建单词列表。 String 类在这里被视为其他字符串的聚合,但它不是可变容器:您不能使用 computeIfAbsent() 模式来做到这一点。

这就是 merge() 模式发挥作用的地方。 merge() 方法接受三个参数

  • 一个键
  • 一个值,您需要将其绑定到该键
  • 一个重新映射 BiFunction

如果键不在映射中或绑定到空值,则该值将绑定到该键。在这种情况下不会调用重新映射函数。

相反,如果键已经绑定到非空值,则重新映射函数将使用现有值和作为参数传递的新值进行调用。如果此重新映射函数返回 null,则该键将从映射中删除。否则,它产生的值将绑定到该键。

您可以在以下示例中看到此 merge() 模式

List<String> strings = List.of("one", "two", "three", "four", "five", "six", "seven");
Map<Integer, String> map = new HashMap<>();
for (String word: strings) {
    int length = word.length();
    map.merge(length, word, 
              (existingValue, newWord) -> existingValue + ", " + newWord);
}

map.forEach((key, value) -> System.out.println(key + " :: " + value));

在这种情况下,如果 length 键不在映射中,则 merge() 调用只是添加它并将其绑定到 word。另一方面,如果 length 键已在映射中,则双函数将使用现有值和 word 进行调用。然后,双函数的结果将替换当前值。

运行此代码会产生以下结果

3 :: one, two, six
4 :: four, five
5 :: three, seven

在这两种模式中,computeIfAbsent()merge(),您可能想知道为什么创建的 lambda 接受一个始终在该 lambda 上下文中可用的参数,并且可以从该上下文中捕获。答案是:出于性能原因,您应该优先使用非捕获 lambda 而不是捕获 lambda。


上次更新: 2021 年 9 月 14 日


系列中的上一篇
当前教程
使用 Lambda 表达式处理 Map 值

系列中的上一篇: 管理 Map 的内容

系列中的下一篇: 使用 SortedMap 和 NavigableMap 保持键排序