使用 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()
和computeIfPresent()
的情况下 - 一个
BiFunction
充当重新映射函数,或在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 日