当前位置: 首页 > news >正文

什么?你不知道 ConcurrentHashMap 的 kv 不能为 null?

一、背景

最近设计某个类库时使用了 ConcurrentHashMap 最后遇到了 value 为 null 时报了空指针异常的坑。
什么?.png
本文想探讨下以下几个问题:
(1) Map接口的常见子类的 kv 对 null 的支持情况。
(2)为什么 ConcurrentHashMap 不支持 key 和 value 为 null?
(3)如果 value 可能为 null ,该如何处理?
(4)有哪些线程安全的 Java Map 类?
(5) 常见的 Map 接口的子类,如 HashMapTreeMapConcurrentHashMapConcurrentSkipListMap 的使用场景。

二、探究

2.1 Map接口的常见子类的 kv 对 null 的支持情况

下图来源于孤尽老师 《码出高效》 第 6 章 数据结构与集合
Xnip2023-03-10_20-29-05.png

2.2 为什么 ConcurrentHashMap 不支持 key 和 value 为 null?

java.util.concurrent.ConcurrentHashMap#put 方法的注释和源码中可以非常容易得看出,不支持 key 和 value null。

    /*** Maps the specified key to the specified value in this table.* Neither the key nor the value can be null.** <p>The value can be retrieved by calling the {@code get} method* with a key that is equal to the original key.** @param key key with which the specified value is to be associated* @param value value to be associated with the specified key* @return the previous value associated with {@code key}, or*         {@code null} if there was no mapping for {@code key}* @throws NullPointerException if the specified key or value is null*/public V put(K key, V value) {return putVal(key, value, false);}/** Implementation for put and putIfAbsent */final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();int hash = spread(key.hashCode());// 省略其他}

那么,为什么不支持 key 和 value 为 null 呢?
据查阅资料,ConcurrentHashMap 的作者 Doug Lea 自己的描述:

The main reason that nulls aren’t allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can’t be accommodated. The main one is that if map.get(key) returns null, you can’t detect whether the key explicitly maps to null vs the key isn’t mapped. In a non-concurrent map, you can check this via map.contains(key), but in a concurrent one, the map might have changed between calls.

可知 ConcurrentHashMap 是线程安全的容器,如果 ConcurrentHashMap 允许存放 null 值,那么当一个线程调用 get(key) 方法时,返回 null 可能有两种情况:
(1) 一种是这个 key 不存在于 map 中
(2) 另一种是这个 key 存在于 map 中,但是它的值为 null。
这样就会导致线程无法判断这个 null 是什么意思。
在非并发的场景下,可以通过 map.contains(key)检查是否包括该 key,从而断定是不存在 key 还是存在key 但值为 null,但是在并发场景下,判断后调用其他 api 之间 map 的数据已经发生了变化,无法保证对同一个 key 操作的一致性。

2.3 怎么解决?

2.3.1 封装 put 方法,使用前判断

建议封装 put 方法,统一使用该方法对 ConcurrentHashMap 的 put 操作进行封装,当 value 为 null 时,直接 return 即可。

Map<String, Person> map = new ConcurrentHashMap<>();// 封装 put 操作,为 null 时返回
private void putPerson(String key, Person value){if(value == null){return;}map.put(key, value);
}

2.3.2 使用 Optional 类型

使用 Optional

// 创建一个 ConcurrentHashMap<String, Optional<String>>
Map<String, Optional<String>> map = new ConcurrentHashMap<>();// 插入或更新 key-value 对
map.computeIfAbsent("name", k -> Optional.ofNullable("Alice")); // 如果 name 不存在,则插入 ("name", Optional.of("Alice"))
map.computeIfAbsent("age", k -> Optional.ofNullable(null)); // 如果 age 不存在,则插入 ("age", Optional.empty())// 获取 value
Optional<String> name = map.get("name"); // 返回 Optional.of("Alice")
Optional<String> age = map.get("age"); // 返回 Optional.empty()
Optional<String> gender = map.get("gender"); // 返回 null

2.3.3 自定义一个表示 null 的类

自定义表示 null 的类, 然后对 put 和 get 操作进行二次封装,参考代码如下:

// 定义一个表示 null 的类
public class NullValue extends Person{}// 创建一个 ConcurrentHashMap<String, Object>
private Map<String, Person> map = new ConcurrentHashMap<>();private static final NullValue nullValue = new NullValue();//使用示例: 值不为 null 时
putPerson("1002", new Person("张三"));//使用示例: 值为 null 时
putPerson("1003", null);// 封装设置操作
private void putPerson(String key,Person person){if(person == null){map.put(key, nullValue);return;}map.put(key, person);
}// 封装获取操作
private Person getPerson(String key){if(key == null){return;}Person person = map.get(key);if(person instanceof NullValue){return null;}return person;
}

2.3.4 使用其他线程安全的 Java Map 类

Java 中也有支持 key 和 value 为 null 的线程安全的集合类,比如 ConcurrentSkipListMap (JDK) 和 CopyOnWriteMap (三方)

  • ConcurrentSkipListMap 是一个基于跳表的线程安全的 map,它使用锁分段的技术来提高并发性能。它允许 key 和 value 为 null,但是它要求 key 必须实现 Comparable 接口或者提供一个 Comparator
  • CopyOnWriteMap 是一个基于数组的线程安全的 map,它使用写时复制的策略来保证并发访问的正确性。它允许 key 和 value 为 null。

注意 JDK 中没有提供 CopyOnWriteMap,很多三方类库提供了对应的工具类。如org.apache.kafka.common.utils.CopyOnWriteMap

2.4 常见的 Map 接口的子类的使用场景

Map 接口有很多子类,那么他们各自的适用场景是怎样的呢?
Xnip2023-03-10_20-30-43.png

使用场景主要取决于以下几个方面:

  • 是否需要线程安全:如果需要在多线程环境下操作 Map,那么应该使用 ConcurrentHashMapConcurrentSkipListMap,它们都是并发安全的。而 HashMapTreeMapHashTableLinkedHashMap则不是,并且 HashTable已经被 ConcurrentHashMap取代。
  • 是否需要保证键的顺序:如果需要按照键的自然顺序或者插入顺序遍历 Map,那么应该使用 TreeMap或者 LinkedHashMap,它们都是有序的。而 ConcurrentSkipListMap也是有序的,并且支持范围查询。其他类则是无序的。
  • 是否需要高效地访问和修改:如果需要快速地获取和更新 Map中的元素,那么应该使用 HashMap或者 ConcurrentHashMap,它们都是基于散列函数实现的,具有较高的性能。
    TreeMapConcurrentSkipListMap则是基于平衡树实现的,具有较低的性能。CopyOnWriteMap 则是基于数组实现的,并发写操作会复制整个数组,因此写操作开销很大。

在选择合适的 Map 接口实现时,需要根据具体需求和场景进行权衡。

三、总结

基本功很重要,有时候基本功不扎实,更容易遇到一些奇奇怪怪的坑。假设你不了解 ConcurrentHashMap 的 kv 不能为 null, 测试的时候没有覆盖这种场景,等上线以后遇到这个问题可能直接导致线上问题,甚至线上故障。

ConcurrentHashMap 作者在 put 方法注释中给出了 kv 不允许为 null 的提示,并没有在注释中给出设计原因,给众多读者带来了诸多困惑。这也给我们很大的启发,当我们的某些设计容易引起别人的困惑和好奇时,不仅要将注意事项放在注释中,更应该将设计原因放在注释里,避免给使用者带来困扰

“适合自己的才是最好的”。正如不同的 Map 实现类各有千秋,使用场景各有不同,我们需要根据具体需求和场景进行权衡一样,我们在设计方案时也会遇到类似的场景,我们能做的是根据场景选择最适合的方案。

我们遇到的任何问题,都是彻底掌握某个知识的绝佳机会。当我们遇到问题时,应该主动掌握相关知识,希望大家不仅能够知其然,还要知其所以然。


创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
在这里插入图片描述

相关文章:

什么?你不知道 ConcurrentHashMap 的 kv 不能为 null?

一、背景 最近设计某个类库时使用了 ConcurrentHashMap 最后遇到了 value 为 null 时报了空指针异常的坑。 本文想探讨下以下几个问题&#xff1a; &#xff08;1&#xff09; Map接口的常见子类的 kv 对 null 的支持情况。 &#xff08;2&#xff09;为什么 ConcurrentHashM…...

SQL复习04 | 复杂查询

1. 视图 视图和表的区别&#xff1a; 表保存的是实际的数据视图保存的是SELECT语句 视图的优点&#xff1a; 视图无需保存数据&#xff0c;可节省存储设备的容量可以将频繁使用的SELECT语句保存成视图&#xff0c;可大大提高效率 1.1 创建视图 CREATE VIEW 视图名称&…...

【面试题】Java面试题汇总(无解答)

此内容会持续补充。。。 基础 short s1 1; s1 s1 1;有错吗? short s1 1; s1 1; 有错吗&#xff1f;String str”aaa”,与 String strnew String(“aaa”)一样吗&#xff1f;String 和 StringBuilder、StringBuffer 的区别&#xff1f;Sring最大能存多大内容&#xff1f…...

C++---背包模型---收服精灵(每日一道算法2023.3.11)

注意事项&#xff1a; 本题是"动态规划—01背包"的扩展题&#xff0c;优化的思路不多赘述&#xff0c;dp思路会稍有不同&#xff0c;下面详细讲解。 本题偏向阅读理解&#xff0c;给每种变量归类起名字很有帮助哦。 切记先看思路&#xff0c;再看代码。&#xff08;大…...

day30_JS

今日内容 上课同步视频:CuteN饕餮的个人空间_哔哩哔哩_bilibili 同步笔记沐沐霸的博客_CSDN博客-Java2301 零、 复习昨日 一、作业 二、BOM 三、定时器 四、正则表达式 零、 复习昨日 事件 事件绑定方式鼠标事件 onmouseoveronmouseoutonmousemove 键盘事件 onkeydownonkeyupon…...

【Java学习笔记】19.Java 正则表达式(2)

前言 本章继续介绍Java的正则表达式。 Matcher 类的方法 索引方法 索引方法提供了有用的索引值&#xff0c;精确表明输入字符串中在哪能找到匹配&#xff1a; 序号方法及说明1public int start()返回以前匹配的初始索引。2public int start(int group)返回在以前的匹配操作…...

华为云arm架构轻松安装kubeedge

先安装k8s 华为云arm架构安装k8s(kubernetes) 下载kubeedge需要的软件 官方github下载kubeedge地址 cloudcore.service文件下载地址 注意:下载对应的版本和arm架构 keadm-v1.6.1-linux-arm64.tar.gz 下面的2个文件可以不用下载,安装kubeedge时也会自动去下载到/etc/kubee…...

33--Vue-前端开发-使用Vue脚手架快速搭建项目

一、vue脚手架搭建项目 node的安装: 官方下载,一路下一步 node命令类似于python npm命令类似于pip 使用npm安装第三方模块,速度慢一些,需换成淘宝镜像 以后用cmpm代替npm的使用 npm install -g cnpm --registry=https://registry.npm.taobao.org安装脚手架: cnpm inst…...

TMS WEB Core开发Web应用优势说明

一、Delphi开发Web应用的三大框架如下: IntraWEB适合于WEB前、后端的开发,其自带的网络服务器非常强大、稳定,笔者使用Cesium框架开发的WEB GIS地理信息系统前端不需要Apache Tomcat或Nginx即可稳定运行; uniGUI是对JavaScript库Sencha ExtJS的封装,它带有两套VCL组件包,…...

人工智能简单应用1-OCR分栏识别:两栏识别三栏识别都可以,本地部署完美拼接

大家好&#xff0c;我是微学AI&#xff0c;今天给大家带来OCR的分栏识别。 一、文本分栏的问题 在OCR识别过程中&#xff0c;遇到文字是两个分栏的情况确实是一个比较常见的问题。通常情况下&#xff0c;OCR引擎会将文本按照从左到右&#xff0c;从上到下的顺序一行一行地识别…...

Gin框架路由拆分与注册详解析

Gin框架路由拆分与注册详解析1.基本的路由注册2.路由拆分成单独文件或包3.路由拆分成多个文件4.路由拆分到不同的APP1.基本的路由注册 下面最基础的gin路由注册方式&#xff0c;适用于路由条目比较少的简单项目或者项目demo // StatCost 是一个统计耗时请求耗时的中间件 func…...

2020蓝桥杯真题凯撒加密 C语言/C++

题目描述 给定一个单词&#xff0c;请使用凯撒密码将这个单词加密。 凯撒密码是一种替换加密的技术&#xff0c;单词中的所有字母都在字母表上向后偏移 3 位后被替换成密文。即 a 变为 d&#xff0c;b 变为 e&#xff0c;⋯&#xff0c;w 变为z&#xff0c;x 变为 a&#xff0…...

taro+vue3小程序使用v-html渲染的内容为class写了样式无效

taro小程序如果是直接引入的一个less文件是包含scoped&#xff0c;只是当前页面采用。<script setup>import ./index.less</script><view v-html"itehtml" class"article-content"></view>let itehtml"<p class"line…...

MASK-RCNN网络介绍

目录前言一.MASK R-CNN网络1.1.RoIPool和RoIAlign1.2.MASK分支二.损失函数三.Mask分支预测前言 在介绍MASK R-CNN之前&#xff0c;建议先看下FPN网络&#xff0c;Faster-CNN和FCN的介绍&#xff1a;下面附上链接&#xff1a; R-CNN、Fast RCNN和Faster RCNN网络介绍FCN网络介绍…...

导航技术调研(CSDN_0023_20221217)

文章编号&#xff1a;CSDN_0023_20221217 目录 1. 惯性导航 2. 组合导航技术 3. 卡尔曼滤波 1. 惯性导航 惯性导航系统(INS-Inertial Navigation System)是上个世纪初发展起来的。惯性导航是一种先进的导航方法&#xff0c;但实现导航定位的原理却非常简单&#xff0c;它是…...

买卖股票的最佳时机 I II III IV

121. 买卖股票的最佳时机 自己的思路&#xff1a;采用求最长连续子串和题目的思路 class Solution {public int maxProfit(int[] prices) {if(prices.length 1) return 0;int[] nums new int[prices.length - 1];for(int i 0;i < prices.length - 1;i){nums[i] prices[…...

STM32—LCD1602

LCD1602&#xff08;Liquid Crystal Display&#xff09;是一种工业字符型液晶&#xff0c;能够同时显示 1602 即 32 字符(16列两行) 第 1 脚: VSS 为电源地 第 2 脚: VDD 接 5V 正电源 第 3 脚: VL 为液晶显示器对比度调整端,接正电源时对比度最弱&#xff0c;接地时对比度最…...

英雄算法学习路线

文章目录零、自我介绍一、关于拜师二、关于编程语言三、算法学习路线1、算法集训1&#xff09;九日集训2&#xff09;每月算法集训2、算法专栏3、算法总包四、英雄算法联盟1、英雄算法联盟是什么&#xff1f;2、如何加入英雄算法联盟&#xff1f;3、为何会有英雄算法联盟&#…...

【设计模式】备忘录模式和迭代器模式

备忘录模式和迭代器模式备忘录模式代码示例迭代器模式代码示例使用迭代器遍历集合的同时不能删除/增加元素总结备忘录模式 备忘录模式&#xff0c;也叫快照&#xff08;Snapshot&#xff09;模式。 在 GoF的《设计模式》⼀书中&#xff0c;备忘录模式是这么定义的&#xff1a;…...

rapidcsv 写csv文件实例

csv实质是一个文本文件&#xff0c;可以使用rapidcsv写文件操作&#xff0c;如下实例&#xff1a; 第一行实质是从-1行开始&#xff0c;列是从0开始 #include "rapidcsv.h" #include <string> using namespace std; void CMFCApplication1Dlg::OnBnClickedBu…...

Java 语言特性(面试系列1)

一、面向对象编程 1. 封装&#xff08;Encapsulation&#xff09; 定义&#xff1a;将数据&#xff08;属性&#xff09;和操作数据的方法绑定在一起&#xff0c;通过访问控制符&#xff08;private、protected、public&#xff09;隐藏内部实现细节。示例&#xff1a; public …...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具

作者&#xff1a;来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗&#xff1f;了解下一期 Elasticsearch Engineer 培训的时间吧&#xff01; Elasticsearch 拥有众多新功能&#xff0c;助你为自己…...

vscode(仍待补充)

写于2025 6.9 主包将加入vscode这个更权威的圈子 vscode的基本使用 侧边栏 vscode还能连接ssh&#xff1f; debug时使用的launch文件 1.task.json {"tasks": [{"type": "cppbuild","label": "C/C: gcc.exe 生成活动文件"…...

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放

简介 前面两期文章我们介绍了I2S的读取和写入&#xff0c;一个是通过INMP441麦克风模块采集音频&#xff0c;一个是通过PCM5102A模块播放音频&#xff0c;那如果我们将两者结合起来&#xff0c;将麦克风采集到的音频通过PCM5102A播放&#xff0c;是不是就可以做一个扩音器了呢…...

推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)

推荐 github 项目:GeminiImageApp(图片生成方向&#xff0c;可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...

排序算法总结(C++)

目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指&#xff1a;同样大小的样本 **&#xff08;同样大小的数据&#xff09;**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...

深入浅出深度学习基础:从感知机到全连接神经网络的核心原理与应用

文章目录 前言一、感知机 (Perceptron)1.1 基础介绍1.1.1 感知机是什么&#xff1f;1.1.2 感知机的工作原理 1.2 感知机的简单应用&#xff1a;基本逻辑门1.2.1 逻辑与 (Logic AND)1.2.2 逻辑或 (Logic OR)1.2.3 逻辑与非 (Logic NAND) 1.3 感知机的实现1.3.1 简单实现 (基于阈…...

NPOI操作EXCEL文件 ——CAD C# 二次开发

缺点:dll.版本容易加载错误。CAD加载插件时&#xff0c;没有加载所有类库。插件运行过程中用到某个类库&#xff0c;会从CAD的安装目录找&#xff0c;找不到就报错了。 【方案2】让CAD在加载过程中把类库加载到内存 【方案3】是发现缺少了哪个库&#xff0c;就用插件程序加载进…...

在golang中如何将已安装的依赖降级处理,比如:将 go-ansible/v2@v2.2.0 更换为 go-ansible/@v1.1.7

在 Go 项目中降级 go-ansible 从 v2.2.0 到 v1.1.7 具体步骤&#xff1a; 第一步&#xff1a; 修改 go.mod 文件 // 原 v2 版本声明 require github.com/apenella/go-ansible/v2 v2.2.0 替换为&#xff1a; // 改为 v…...

动态规划-1035.不相交的线-力扣(LeetCode)

一、题目解析 光看题目要求和例图&#xff0c;感觉这题好麻烦&#xff0c;直线不能相交啊&#xff0c;每个数字只属于一条连线啊等等&#xff0c;但我们结合题目所给的信息和例图的内容&#xff0c;这不就是最长公共子序列吗&#xff1f;&#xff0c;我们把最长公共子序列连线起…...