浅谈 Guava 中的 ImmutableMap.of 方法的坑

作者:明明如月学长, CSDN 博客专家,大厂高级 Java 工程师,《性能优化方法论》作者、《解锁大厂思维:剖析《阿里巴巴Java开发手册》》、《再学经典:《EffectiveJava》独家解析》专栏作者。

热门文章推荐

  • (1)《为什么很多人工作 3 年 却只有 1 年经验?》
  • (2)《从失望到精通:AI 大模型的掌握与运用技巧》
  • (3)《AI 时代,程序员的出路在何方?》
  • (4)《如何写出高质量的文章:从战略到战术》
  • (5)《我的技术学习方法论》
  • (6)《我的性能方法论》
  • (7)《AI 时代的学习方式: 和文档对话》

一、背景

Guava 的 ImmutableMap类提供了 of方法,可以很方便地构造不可变 Map。

 ImmutableMap<Object, Object> build = ImmutableMap.of("a",1,"b",2);

然而,实际工作开发中很多人会从开始认为非常方便,后面到发现很多大家都会遇到相似的“问题”。
比如 ImmutableMap类的 of 存在很多重载的方法,但是最多只有五个键值对。
有无参的方法:

  /**
   * Returns the empty map. This map behaves and performs comparably to {@link
   * Collections#emptyMap}, and is preferable mainly for consistency and maintainability of your
   * code.
   *
   * <p><b>Performance note:</b> the instance returned is a singleton.
   */
  @SuppressWarnings("unchecked")
  public static <K, V> ImmutableMap<K, V> of() {
    return (ImmutableMap<K, V>) RegularImmutableMap.EMPTY;
  }

有支持一个键值对的方法:

  /**
   * Returns an immutable map containing a single entry. This map behaves and performs comparably to
   * {@link Collections#singletonMap} but will not accept a null key or value. It is preferable
   * mainly for consistency and maintainability of your code.
   */
  public static <K, V> ImmutableMap<K, V> of(K k1, V v1) {
    return ImmutableBiMap.of(k1, v1);
  }

到支持五个键值对的方法:

  /**
   * Returns an immutable map containing the given entries, in order.
   *
   * @throws IllegalArgumentException if duplicate keys are provided
   */
  public static <K, V> ImmutableMap<K, V> of(
      K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5) {
    return RegularImmutableMap.fromEntries(
        entryOf(k1, v1), entryOf(k2, v2), entryOf(k3, v3), entryOf(k4, v4), entryOf(k5, v5));
  }

很多人会遇到的坑:

  • 超过五个键值对怎么办?
  • key 和 value “居然”都不能为 null?
  • 同一个 key 重复 put 报错

二、场景还原

2.1 超过 5 个键值对问题

虽然 of 方法很好用,但是经常会遇到超过 5 个键值对的情况,就非常不方便。

解法1:升级版本

在 guava 31.0 版本以后,已经拓展到了 10 个键值对!

  /**
   * Returns an immutable map containing the given entries, in order.
   *
   * @throws IllegalArgumentException if duplicate keys are provided
   * @since 31.0
   */
  public static <K, V> ImmutableMap<K, V> of(
      K k1,
      V v1,
      K k2,
      V v2,
      K k3,
      V v3,
      K k4,
      V v4,
      K k5,
      V v5,
      K k6,
      V v6,
      K k7,
      V v7,
      K k8,
      V v8,
      K k9,
      V v9,
      K k10,
      V v10) {
    return RegularImmutableMap.fromEntries(
        entryOf(k1, v1),
        entryOf(k2, v2),
        entryOf(k3, v3),
        entryOf(k4, v4),
        entryOf(k5, v5),
        entryOf(k6, v6),
        entryOf(k7, v7),
        entryOf(k8, v8),
        entryOf(k9, v9),
        entryOf(k10, v10));
  }

解法2:使用 builder 方法

com.google.common.collect.ImmutableMap#builder 方法可以通过构造器的方式不断 put 键值对,最后 build即可,也非常方便。

      ImmutableMap<Object, Object> build = ImmutableMap.builder()
                .put("a", 1)
                .put("b", 2)
                .put("c", 3)
                .put("d",4)
                .put("e",5)
                .put("f",6)
                .build();

也可以参考 2.2 中的解法。

2.2 键值都不允许为 null

复现

很多人看到名字就知道不可“修改” 但不太清楚它的键值都不允许为 null。

key 为空的情况:
image.png

value 为空的情况:
image.png

真正开发时不会那么简单,有时候需要调用某个接口获取返回值然后再构造一个不可编辑的 Map 返回给下游使用。很可能在测试的时候都没有出现 null 值,发布上线,发现 key 或者 value 为 null,就会造成线上问题 或者 bug。

源码

对于 of的多参数重载:

  /**
   * Returns an immutable map containing the given entries, in order.
   *
   * @throws IllegalArgumentException if duplicate keys are provided
   */
  public static <K, V> ImmutableMap<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3) {
    return RegularImmutableMap.fromEntries(entryOf(k1, v1), entryOf(k2, v2), entryOf(k3, v3));
  }

  /**
   * Verifies that {@code key} and {@code value} are non-null, and returns a new immutable entry
   * with those values.
   *
   * <p>A call to {@link Entry#setValue} on the returned entry will always throw {@link
   * UnsupportedOperationException}.
   */
  static <K, V> Entry<K, V> entryOf(K key, V value) {
    return new ImmutableMapEntry<>(key, value);
  }

  ImmutableMapEntry(K key, V value) {
    super(key, value);
    checkEntryNotNull(key, value);
  }
  static void checkEntryNotNull(Object key, Object value) {
    if (key == null) {
      throw new NullPointerException("null key in entry: null=" + value);
    } else if (value == null) {
      throw new NullPointerException("null value in entry: " + key + "=null");
    }
  }

当然,如果你比较心细的话会发现 IDE 中会有警告,也可以很大程度上避免这个问题。

解法

不如换个“殊途同归”的办法,先用 HashMap 去实现同一个 key 的值覆盖的功能,然后通过 Collections.unmodifiableMap来实现不可编辑功能。

     Map<String, Object> map = new HashMap<>();
        map.put("a", 1);
        map.put("b", 2);
        map.put("c", 3);
        map.put("d", 4);
        map.put("e", 5);
        map.put("f", null);
        Map<String, Object> unmodifiableMap = Collections.unmodifiableMap(map);
        System.out.println(unmodifiableMap);

在这里插入图片描述

2.3 key 重复报错

复现

如果一不小心 key 重复,也会报 java.lang.IllegalArgumentException异常。

        ImmutableMap<Object, Object> build = ImmutableMap.builder()
                .put("a", 1)
                .put("b", 2)
                .put("c", 3)
                .put("d",4)
                .put("f",5)
                .put("f",6)
                .build();
        System.out.println(build);

image.png

源码

  /**
   * Returns an immutable map containing the given entries, in order.
   *
   * @throws IllegalArgumentException if duplicate keys are provided
   */
  public static <K, V> ImmutableMap<K, V> of(K k1, V v1, K k2, V v2) {
    return RegularImmutableMap.fromEntries(entryOf(k1, v1), entryOf(k2, v2));
  }


最底层会对 entry 进行校验:

  /**
   * Checks if the given key already appears in the hash chain starting at {@code keyBucketHead}. If
   * it does not, then null is returned. If it does, then if {@code throwIfDuplicateKeys} is true an
   * {@code IllegalArgumentException} is thrown, and otherwise the existing {@link Entry} is
   * returned.
   *
   * @throws IllegalArgumentException if another entry in the bucket has the same key and {@code
   *     throwIfDuplicateKeys} is true
   * @throws BucketOverflowException if this bucket has too many entries, which may indicate a hash
   *     flooding attack
   */
  @CanIgnoreReturnValue
  static <K, V> @Nullable ImmutableMapEntry<K, V> checkNoConflictInKeyBucket(
      Object key,
      Object newValue,
      @CheckForNull ImmutableMapEntry<K, V> keyBucketHead,
      boolean throwIfDuplicateKeys)
      throws BucketOverflowException {
    int bucketSize = 0;
    for (; keyBucketHead != null; keyBucketHead = keyBucketHead.getNextInKeyBucket()) {
      if (keyBucketHead.getKey().equals(key)) {
        if (throwIfDuplicateKeys) {
          checkNoConflict(/* safe= */ false, "key", keyBucketHead, key + "=" + newValue);
        } else {
          return keyBucketHead;
        }
      }
      if (++bucketSize > MAX_HASH_BUCKET_LENGTH) {
        throw new BucketOverflowException();
      }
    }
    return null;
  }

最终报错:

  static IllegalArgumentException conflictException(
      String conflictDescription, Object entry1, Object entry2) {
    return new IllegalArgumentException(
        "Multiple entries with same " + conflictDescription + ": " + entry1 + " and " + entry2);
  }

解法

ImmutableMapbuilder除了提供 buid 之外, 在 31.0 版本之后还通过了 buildKeepingLastbuildOrThrow
image.png
可以通过 buildKeepingLast设置当 key 重复时取后面的值。

    /**
     * Returns a newly-created immutable map. The iteration order of the returned map is the order
     * in which entries were inserted into the builder, unless {@link #orderEntriesByValue} was
     * called, in which case entries are sorted by value.
     *
     * <p>Prefer the equivalent method {@link #buildOrThrow()} to make it explicit that the method
     * will throw an exception if there are duplicate keys. The {@code build()} method will soon be
     * deprecated.
     *
     * @throws IllegalArgumentException if duplicate keys were added
     */
    public ImmutableMap<K, V> build() {
      return buildOrThrow();
    }

    /**
     * Returns a newly-created immutable map, or throws an exception if any key was added more than
     * once. The iteration order of the returned map is the order in which entries were inserted
     * into the builder, unless {@link #orderEntriesByValue} was called, in which case entries are
     * sorted by value.
     *
     * @throws IllegalArgumentException if duplicate keys were added
     * @since 31.0
     */
    public ImmutableMap<K, V> buildOrThrow() {
      return build(true);
    }

    /**
     * Returns a newly-created immutable map, using the last value for any key that was added more
     * than once. The iteration order of the returned map is the order in which entries were
     * inserted into the builder, unless {@link #orderEntriesByValue} was called, in which case
     * entries are sorted by value. If a key was added more than once, it appears in iteration order
     * based on the first time it was added, again unless {@link #orderEntriesByValue} was called.
     *
     * <p>In the current implementation, all values associated with a given key are stored in the
     * {@code Builder} object, even though only one of them will be used in the built map. If there
     * can be many repeated keys, it may be more space-efficient to use a {@link
     * java.util.LinkedHashMap LinkedHashMap} and {@link ImmutableMap#copyOf(Map)} rather than
     * {@code ImmutableMap.Builder}.
     *
     * @since 31.1
     */
    public ImmutableMap<K, V> buildKeepingLast() {
      return build(false);
    }

低版本的话可以考虑先用 HashMap 构造数据,然后使用 com.google.common.collect.ImmutableMap#copyOf(java.util.Map<? extends K,? extends V>) 转换即可。

    Map<String, Object> map = new HashMap<>();
        map.put("a", 1);
        map.put("b", 2);
        map.put("c", 3);
        map.put("d", 4);
        map.put("f", 5);
        map.put("f", 6);
        ImmutableMap<Object, Object> build = ImmutableMap.copyOf(map);
        System.out.println(build);

三、为什么?

3.1 为什么默认是 5 个键值对?

其实 31.0 版本,已经支持 10 个键值对了。
此处,斗胆猜测,of方法仅是为了提供更简单的构造 ImmutableMap的方法,而“通常” 5 个就足够了。
然而,实践中很多人发现 5 个并不够,因此高版本中支持 10个键值对。

Guava 也有相关 Issues 的讨论 ImmutableMap::of should accept more entries #2071
https://github.com/google/guava/issues/2071

image.png

3.2 为什么不允许键值为 null ?

Github 上也有相关讨论:
Question: Why RegularImmutableMap.fromEntryArray enforces “not null” policy on values? #5844

image.png

wiki 上有相关解释:
https://github.com/google/guava/wiki/UsingAndAvoidingNullExplained

使用 ChatGPT 对上述 wiki 进行关键信息提取:

在谷歌的 Guava 库的设计哲学中,不允许在 ImmutableMap(或其他类似的集合)中使用 null 值有几个关键原因:

防止错误:Guava 团队发现在 Google 的代码库中,大约 95% 的集合不应包含任何 null 值。允许 null 值会增加出错的风险,比如可能导致空指针异常。让这些集合在遇到 null 时快速失败(fail-fast)而不是默默接受 null,对开发者来说更有帮助。

消除歧义:null 值的含义通常不明确。例如,在使用 Map.get(key) 时,如果返回 null,可能是因为映射中该键对应的值为 null,或者该键在映射中不存在。这种歧义会导致理解和使用上的困难。

提倡更清晰的实践:在 Set 或 Map 中使用 null 值通常不是一个好的做法。更清晰的方法是在查找操作中显式处理 null,例如,如果你想在 Map 中使用 null 作为值,最好将那个条目留空,并保持一个单独的非空键集合。这样做可以避免混淆那些映射中键存在但值为 null,和那些映射中根本没有该键的情况。

选择适当的替代方案:如果你确实需要使用 null 值,并且遇到了不友好处理 null 的集合实现时,Guava 建议使用不同的实现。例如,如果 ImmutableList 不满足需求,可以使用 Collections.unmodifiableList(Lists.newArrayList()) 作为替代。

总体而言,Guava 库通过避免在其集合中使用 null,旨在提供更清晰、更健壮、且更易于维护的代码实践。

3.3 为什么重复 key 会报错?

我认为,主要是为了符合“不可变”的语义,既然是不可变,那么相同的 key 不应该重复放入到 map 中。其次,也可以避免意外的数据覆盖或丢失。

四、总结

虽然这个问题并不难,但很多人并不知道会有那么多“坑”,很多人都需要重复思考如何解决这些限制。
因此,本文总结在这里,希望对大家有帮助。


在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/182755.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

练习七-在Verilog中使用任务task

在Verilog中使用任务task 1&#xff0c;任务目的2&#xff0c;RTL代码&#xff0c;交换3&#xff0c;测试代码4&#xff0c;波形显示 1&#xff0c;任务目的 &#xff08;1&#xff09;掌握任务在verilog模块设计中的应用&#xff1b; &#xff08;2&#xff09;学会在电平敏感…

新一代网络监控技术——Telemetry

一、Telemetry的背景 传统的网络设备监控方式有SNMP、CLI、Syslog、NetStream、sFlow&#xff0c;其中SNMP为主流的监控数据方式。而随着网络系统规模的扩大&#xff0c;网络设备数量的增多&#xff0c;网络结构的复杂&#xff0c;相应监控要求也不断提升&#xff0c;如今这些…

CUDA学习笔记9——CUDA 共享内存 / Shared Memory

由于共享内存拥有仅次于寄存器的读写速度&#xff0c;比全局内存快得多。因此&#xff0c;能够用共享内存访问替换全局内存访问的场景都可以考虑做对应的优化。 不利用共享内存的矩阵乘法 不利用共享内存的矩阵乘法的直接实现。每个线程读取A的一行和B的一列&#xff0c;并计…

CVE-2022-0543(Redis 沙盒逃逸漏洞)

简介 CVE-2022-0543是一个与Redis相关的安全漏洞。在Redis中&#xff0c;用户连接后可以通过eval命令执行Lua脚本&#xff0c;但在沙箱环境中脚本无法执行命令或读取文件。然而&#xff0c;攻击者可以利用Lua沙箱中遗留的变量package的loadlib函数来加载动态链接库liblua5.1.s…

jenkins 参数构建

应用保存 [rootjenkins-node1 .ssh]# ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (/root/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved i…

利用GUI实现渲染二维码效果

以下是一个简单的 Java 验证码实现示例&#xff1a; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.image.BufferedImage; import java.util.Random;import javax.imageio.ImageIO;public class CaptchaGenerator {public static …

C++基础(4)——类与对象(默认成员函数)

目录 1.拷贝构造函数&#xff1a; 1.1 为什么要引入拷贝构造&#xff1a; 1.2 拷贝构造函数的定义及特性&#xff1a; 1.3 什么类可以不用编写拷贝构造&#xff1a; 2. 赋值运算符重载&#xff1a; 2.1 为社么要引入运算符重载&#xff1a; 2.2运算符重载的定义以及特性…

轻松管理文件名:文件批量重命名的技巧与操作

在日常工作中&#xff0c;文件管理是一项至关重要的任务。其中&#xff0c;文件名的管理更是关键。文件名是在查找文件时最直观的线索。一个好的文件名简短而准确地反映文件的内容或用途。然而&#xff0c;随着时间的推移&#xff0c;可能会发现文件名变得冗长、混乱甚至无法反…

指针变量与指针类型的深入理解

1.知识总结 相关代码展示 #include <stdio.h> int main() {int n 0x11223344;int *pi &n; *pi 0; return 0; } #include <stdio.h> int main() {int n 0x11223344;char *pc (char *)&n;*pc 0;return 0; } #include <stdio.h> int main() {i…

SSM家具个性定制管理系统开发mysql数据库web结构java编程计算机网页源码eclipse项目

一、源码特点 SSM 家具个性定制管理系统是一套完善的信息系统&#xff0c;结合springMVC框架完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用…

【jvm】虚拟机之堆

目录 一、堆的核心概述二、堆的内存细分&#xff08;按分代收集理论设计&#xff09;2.1 java7及以前2.2 java8及以后 三、堆内存大小3.1 说明3.2 参数设置3.3 默认大小3.4 手动设置3.5 jps3.6 jstat3.7 OutOfMemory举例 四、年轻代与老年代4.1 说明 五、对象分配过程5.1 说明5…

Jackson无缝替换Fastjson

目录 文章目录 一&#xff0c;Fastjson到Jackson的替换方案方案代码序列化反序列化通过key获取某种类型的值类型替换 二&#xff0c;Springboot工程中序列化的使用场景三&#xff0c;SpringMVC框架中的Http消息转换器1&#xff0c;原理&#xff1a;2&#xff0c;自定义消息转换…

Visio学习笔记

1. 常用素材 1.1 立方体&#xff1a;张量, tensor 操作路径&#xff1a;更多形状 ⇒ 常规 ⇒ 基本形状 自动配色 在选择【填充】后Visio会自动进行配色&#xff1b;

【C/C++】排序算法代码实现

这里&#xff0c;汇总了常见的排序算法具体代码实现。使用C语言编写。 排序算法实现 插入排序冒泡排序选择排序快速排序希尔排序归并排序 插入排序 #include <stdio.h> #include <stdlib.h>void InsertSort(int arr[],int n){int i,j,temp;for(i 1;i < n;i){ …

初识Java 18-3 泛型

目录 边界 通配符 编译器的能力范畴 逆变性 无界通配符 捕获转换 本笔记参考自&#xff1a; 《On Java 中文版》 边界 在泛型中&#xff0c;边界的作用是&#xff1a;在参数类型上增加限制。这么做可以强制执行应用泛型的类型规则&#xff0c;但还有一个更重要的潜在效果…

vue el-table (固定列+滚动列)【横向滚动条】确定滚动条是在列头还是列尾

效果图&#xff1a; 代码实现&#xff1a; html&#xff1a; <script src"//unpkg.com/vue2/dist/vue.js"></script> <script src"//unpkg.com/element-ui2.15.14/lib/index.js"></script> <div id"app" style&quo…

实战JVM高CPU、内存问题分析定位

背景&#xff1a; 业务中台组件MOSC开展压测工作&#xff0c;并发场景下发现CPU使用率达到100%&#xff0c;虽然程序没有报错&#xff0c;但是这种情况显然已经达到性能瓶颈&#xff0c;对服务带来了验证的效能影响&#xff0c;所以针对该CPU问题必须进行详细的根因分析处理。…

浅谈Python中的鸭子类型和猴子补丁

文章目录 前言一、鸭子类型二、猴子补丁关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战案例③Python小游戏源码五、面试资料六、Python兼职渠道 前言 Python 开发者可能…

AdaBoost提升分类器性能

目录 AdaBoost算法原理 AdaBoost工作详情 初始权重分配 第一轮 第二轮 后续轮次 最终模型 AdaBoost的API解释 AdaBoost 对房价进行预测 AdaBoost 与决策树模型的比较 结论 AdaBoost算法原理 在数据挖掘中&#xff0c;分类算法可以说是核心算法&#xff0c;其中 Ada…

如何应用ChatGPT撰写、修改论文及工作报告,提供写作能力及优化工作??

如果我想让gpt从pdf文档中提取相关关键词的内容&#xff0c;可以怎么做呢&#xff1f;&#xff1f;我们评论区讨论 ChatGPT 在论文写作与编程方面也具备强大的能力。无论是进行代码生成、错误调试还是解决编程难题&#xff0c;ChatGPT都能为您提供实用且高质量的建议和指导&am…