不可不知的 Map 技巧


如果说什么是 Java 集合中的性能天花板,那 Map 肯定是当仁不让。如果又问什么每个 Crud Boy 的终极噩梦,那空指针 (NPE) 必然将榜上有名。

HashMap 作为 JDK 1.2 版本就被引入的结果在设计之初并没有针对空数据有着特殊的限制,一不留神就可能踩坑从而引发连锁反应。

因此,随着 JDK 的不断更新迭代,在 JDK 8 中引入 compute()merge() 等一系列新方法进一步提高代码健壮性,今天就让我们来一探究竟。

1. compute()

compute() 是最基础也是最通用的方法,顾名思义即计算,可以针对指定 Key 进行重新计算赋值,作用相当于 get() + put() 结合体。

其第二个方法入参 BiFunction 输入两个值分表表示当前 KeyValue,最后返回的结果即新值。下述示例即将对应元素的值替换为新旧值字符串拼接后结果。

public void demo() {
    Map<String, String> map = new HashMap<>();
    map.put("a", "a1");

    // 计算赋值
    map.compute("a", (k, v) -> {
        String interval = "^";
        return String.format("%s%s%s", v, interval, "a2");
    });
    // {a=a1^a2}
    System.out.println(map);
}

当正像上述提到那样,其类似于 get()put() 结合体,因此若 Key 不存在或对应值为空时上述的 (k, v) 中的 v 同样是为空,若无处理将会抛出 NPE 异常。

同样有一个相对容易让人忽略的事项,当 compute() 计算返回 null 时,其并非存入一个 (key, null) 的节点,而是将对应 key 从集合中删除,在通过 containsKey() 判断时将返回 false

即与 JDK 8 之后许多新引入的特性秉持着同一理念,尽量避免 keyvalue 为空,当通过 get() 方式获取结果为空时理应代表着 key 不存在避免二义性。

public void demo() {
    Map<String, String> map = new HashMap<>();
    map.put("a", "a1");

    // 空则删除元素
    map.compute("a", (k, v) -> null);
    // 返回 false
    System.out.println(map.containsKey("a"));
}

2. computeIfAbsent()

从名字即可看出,computeIfAbsent()compute() 的特例,即当 Key 不存在时执行,若存在则不会触发。

compute() 不同的其触发时 Key 肯定是不存在的,因此第二个参数输入为 Function,即仅支持输入一个参数代表 Key

public void demo() {
    Map<String, String> map = new HashMap<>();

    // a 不存在,写入
    map.computeIfAbsent("a", k -> String.format("%s^%s", k, "v1"));
    // a 存在,不执行
    map.computeIfAbsent("a", k -> String.format("%s^%s", k, "v2"));
    // {a=a^v1}
    System.out.println(map);
}

3. computeIfPresent()

computeIfPresent() 同样为 compute() 的一种特例,作用则刚好与 computeIfAbsent() 相反,即只在 Key 存在的时候执行计算并覆盖原值,这里就不再展示示例介绍。

4. merge()

故名思意 merge() 即用于合并,即合并对应 Key 的新旧值后放回容器,旧值不存在则用新值替换,返回 null 时同样删除该元素节点。

其与 compute() 既有相当又有不同,最直观的表现即方法入参,merge() 方法接收 3 个参数:(k, v, (o,n)),分别代表着 Key,新的 Value 以及新旧值函数参数。

例如下述示例即拼接对应 key=a 的元素节点:

public void demo() {
    Map<String, String> map = new HashMap<>();
    map.put("a", "v1");

    map.merge("a", "v2", (o, n) -> {
        String interval = "^";
        return String.format("%s%s%s", o, interval, n);
    });
    // {a=v1^v2}
    System.out.println(map);
}

看到这你或许会有疑惑,merge()compute() 有和区别?能用 merge() 实现的通过 compute() 同样能够实现。

事实也的确如此,可以将 merge() 理解为 compute() 的一种特例,compute() 表示针对任意类型计算操作,而 merge() 则更倾向于针对数据的合并操作,同样其自带了部分数据预处理。

观察 HashMapmerge() 方法实现可以看到,除在元素不存在即 old = null 时直接替换,在元素存在时但旧值为空时仍会执行替换,如此一来即为我们省去空判断处理。

通过下面这个示例,就可以直观的看出二者所带来的代码差异,显然合并计算相关操作 merge() 实现简洁性更高。

public void demo1() {
    Map<String, List<String>> map = new HashMap<>();
    map.put("a", null);

    map.compute("a", (k, v) -> {
        if (v == null) {
            v = new ArrayList<>();
        }
        v.add("a1");
        return v;
    });

    System.out.println(map);
}

public void demo2() {
    Map<String, List<String>> map = new HashMap<>();
    map.put("a", null);

    List<String> v1 = new ArrayList<>(List.of("a1"));
    map.merge(key, v1, (o, n) -> {
        o.addAll(n);
        return o;
    });
    System.out.println(map);
}

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录