Java Map接口的默认方法,如果 Map
中包含元素,用户希望替换元素;如果 Map
中没有元素,用户希望添加元素;此外,用户还希望执行其他相关操作。使用 java.util.Map
接口新增的各种默认方法,如 computeIfAbsent
、computeIfPresent
、replace
、merge
等。
Java Map接口的默认方法 问题描述
如果 Map
中包含元素,用户希望替换元素;如果 Map
中没有元素,用户希望添加元素;此外,用户还希望执行其他相关操作。
Java Map接口的默认方法 解决方案
使用 java.util.Map
接口新增的各种默认方法,如 computeIfAbsent
、computeIfPresent
、replace
、merge
等。
Java Map接口的默认方法 具体实例
从 Java 1.2 引入集合框架(collections framework)起,Map
接口就已存在。Java 8 为 Map
接口引入了一些新的默认方法,如表 5-1 所示。
表5-1:Map
接口定义的默认方法
方法 | 描述 |
---|---|
Compute |
根据现有的键和值计算新的值 |
computeIfAbsent |
如果键存在,返回对应的值,否则通过提供的函数计算新的值并保存 |
computeIfPresent |
计算新的值以替换现有的值 |
forEach |
对 Map 进行迭代,将所有键和值传递给 Consumer |
getOrDefault |
如果键在 Map 中存在,返回对应的值,否则返回默认值 |
merge |
如果键在 Map 中不存在,返回提供的值,否则计算新的值 |
putIfAbsent |
如果键在 Map 中不存在,将其关联到给定的值 |
remove |
如果键的值与给定的值匹配,删除该键的条目 |
replace |
将现有键的值替换为新的值 |
replaceAll |
将 Map 中每个条目的值替换为对当前条目调用给定函数后的结果 |
Java 8 为已有十多年历史的 Map
接口引入了不少新方法,某些方法能为开发提供极大的便利。
1. computeIfAbsent
computeIfAbsent
方法的完整签名如下:
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
在创建方法调用结果的缓存时,computeIfAbsent
尤其有用。我们以经典的斐波那契数递归计算为例进行讨论。如例 5-8 所示,任何大于 1 的斐波那契数等于前两个斐波那契数之和 5。
例 5-8 斐波那契数递归计算
long fib(long i) {
if (i == 0) return 0;
if (i == 1) return 1;
return fib(i - 1) + fib(i - 2); ➊
}
➊ 效率极低
上述代码的问题在于需要进行大量重复的计算(如 fib(5) = fib(4) + fib(3) = fib(3) + fib(2) + fib(2) +fib(1) = ...
),导致程序效率极低。可以利用缓存解决这个问题,函数式编程将这种技术称为记忆化(memoization)。如例 5-9 所示,我们将结果修改为存储 BigInteger
实例。
例 5-9 利用缓存计算斐波那契数
private Map<Long, BigInteger> cache = new HashMap<>();
public BigInteger fib(long i) {
if (i == 0) return BigInteger.ZERO;
if (i == 1) return BigInteger.ONE;
return cache.computeIfAbsent(i, n -> fib(n - 2).add(fib(n - 1))); ➊
}
➊ 如果键的值在缓存中存在,返回对应的值,否则计算新的值并保存
本例采用缓存计算斐波那契数,其中键为提供的数字,值为相应的斐波那契数。computeIfAbsent
方法在缓存中搜索给定的数字,存在则返回对应的值,否则使用提供的 Function
计算新的值,将其保存在缓存中并返回。对单一方法而言,这已是很大的改进。
2.computeIfPresent
computeIfPresent
方法的完整签名如下:
V computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction)
仅当与某个值关联的键在 Map
中存在时,computeIfPresent
才会更新该值。假设我们需要解析一个文本,并计算文本中每个单词的出现次数。这种一致性(concordance)计算在实际中并不鲜见。如果仅对某些特定单词感兴趣,可以使用 computeIfPresent
方法进行更新,如例 5-10 所示。
例 5-10 仅更新特定单词的出现次数
public Map<String,Integer> countWords(String passage, String... strings) {
Map<String, Integer> wordCounts = new HashMap<>();
Arrays.stream(strings).forEach(s -> wordCounts.put(s, 0)); ➊
Arrays.stream(passage.split(" ")).forEach(word -> ➋
wordCounts.computeIfPresent(word, (key, val) -> val + 1));
return wordCounts;
}
❶ 将特定单词置于映射中,并将计数器设置为 0
❷ 读取文本,仅更新特定单词的出现次数
通过将特定单词置于映射中并将初始计数器设置为 0,就能让 computeIfPresent
方法只更新这些值。
如例 5-11 所示,对一段文本以及一个逗号分隔的单词列表执行上述程序,可以得到所需的结果。
例 5-11 调用
countWords
方法
String passage = "NSA agent walks into a bar. Bartender says, " +
"'Hey, I have a new joke for you.' NSA agent says, 'heard it'.";
Map<String, Integer> counts = demo.countWords(passage, "NSA", "agent", "joke");
counts.forEach((word, count) -> System.out.println(word + "=" + count));
// 输出为:NSA=2, agent=2, joke=1
可以看到,仅当所需单词是映射中的键时,程序才会更新它们的出现次数。与之前一样,采用 Map
接口定义的默认方法 forEach
打印值,该方法传入 BiConsumer
,其参数为键和值。
3.其他方法
replace
方法的用法与 put
方法类似,前提是键已经存在。如果键不存在,replace
不会执行任何操作,而 put
将添加一个空键(null key),不过这可能并非如我们所愿。
replace
方法包括两种重载形式:
V replace(K key, V value)
boolean replace(K key, V oldValue, V newValue)
对于第一种形式,如果键在映射中存在,则将其替换为对应的值;对于第二种形式,如果键的值与指定的值相等,则将其替换为新的值。
使用不存在的键调用 Map
接口的 get
方法将返回 null
,这个令人头疼的问题可以通过 getOrDefault
方法解决。该方法仅返回默认值,但不会将键添加到映射中。
getOrDefault
方法的签名如下:
V getOrDefault(Object key, V defaultValue)
如果键在映射中不存在,
getOrDefault
方法将返回默认值,但不会将这个键添加到映射中。
merge
方法非常有用,其完整签名如下:
V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction)
对于一段给定的文本,假设我们希望统计所有单词(而不仅是特定单词)的出现次数,那么通常需要考虑两种情况:如果单词已经在映射中,则更新计数器;如果单词不在映射中,则将其置于映射中并使计数器加 1。可以通过 merge
方法简化这个过程,如例 5-12 所示。
例 5-12
merge
方法的应用
public Map<String, Integer> fullWordCounts(String passage) {
Map<String, Integer> wordCounts = new HashMap<>();
String testString = passage.toLowerCase().replaceAll("\\W"," "); ➊
Arrays.stream(testString.split("\\s+")).forEach(word ->
wordCounts.merge(word, 1, Integer::sum)); ➋
return wordCounts;
}
❶ 将字符串转换为小写字母并删除标点符号
❷ 更新给定单词的计数器
merge
方法传入键和默认值。如果键在映射中不存在,则插入默认值,否则根据原有值并使用 BinaryOperator
(本例为 Integer::sum
)计算出新的值。
本范例讨论了 Map
接口新增的默认方法,希望这些方法能为程序开发带来便利。
5大部分读者想必都听过这个笑话:“据说今年的斐波那契会议将和前两年一样好。”