Java Map接口的默认方法

Java Map接口的默认方法,如果 Map 中包含元素,用户希望替换元素;如果 Map 中没有元素,用户希望添加元素;此外,用户还希望执行其他相关操作。使用 java.util.Map 接口新增的各种默认方法,如 computeIfAbsentcomputeIfPresentreplacemerge 等。

Java Map接口的默认方法 问题描述

如果 Map 中包含元素,用户希望替换元素;如果 Map 中没有元素,用户希望添加元素;此外,用户还希望执行其他相关操作。

Java Map接口的默认方法 解决方案

使用 java.util.Map 接口新增的各种默认方法,如 computeIfAbsentcomputeIfPresentreplacemerge 等。

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大部分读者想必都听过这个笑话:“据说今年的斐波那契会议将和前两年一样好。”

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程