什么?你不知道 ConcurrentHashMap 的 kv 不能为 null?
一、背景
最近设计某个类库时使用了 ConcurrentHashMap 最后遇到了 value 为 null 时报了空指针异常的坑。

本文想探讨下以下几个问题:
(1) Map接口的常见子类的 kv 对 null 的支持情况。
(2)为什么 ConcurrentHashMap 不支持 key 和 value 为 null?
(3)如果 value 可能为 null ,该如何处理?
(4)有哪些线程安全的 Java Map 类?
(5) 常见的 Map 接口的子类,如 HashMap、TreeMap 、ConcurrentHashMap 、ConcurrentSkipListMap 的使用场景。
二、探究
2.1 Map接口的常见子类的 kv 对 null 的支持情况
下图来源于孤尽老师 《码出高效》 第 6 章 数据结构与集合

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 接口有很多子类,那么他们各自的适用场景是怎样的呢?

使用场景主要取决于以下几个方面:
- 是否需要线程安全:如果需要在多线程环境下操作
Map,那么应该使用ConcurrentHashMap、ConcurrentSkipListMap,它们都是并发安全的。而HashMap、TreeMap、HashTable和LinkedHashMap则不是,并且HashTable已经被ConcurrentHashMap取代。 - 是否需要保证键的顺序:如果需要按照键的自然顺序或者插入顺序遍历
Map,那么应该使用TreeMap或者LinkedHashMap,它们都是有序的。而ConcurrentSkipListMap也是有序的,并且支持范围查询。其他类则是无序的。 - 是否需要高效地访问和修改:如果需要快速地获取和更新
Map中的元素,那么应该使用HashMap或者ConcurrentHashMap,它们都是基于散列函数实现的,具有较高的性能。
而TreeMap和ConcurrentSkipListMap则是基于平衡树实现的,具有较低的性能。CopyOnWriteMap则是基于数组实现的,并发写操作会复制整个数组,因此写操作开销很大。
在选择合适的 Map 接口实现时,需要根据具体需求和场景进行权衡。
三、总结
基本功很重要,有时候基本功不扎实,更容易遇到一些奇奇怪怪的坑。假设你不了解 ConcurrentHashMap 的 kv 不能为 null, 测试的时候没有覆盖这种场景,等上线以后遇到这个问题可能直接导致线上问题,甚至线上故障。
ConcurrentHashMap 作者在 put 方法注释中给出了 kv 不允许为 null 的提示,并没有在注释中给出设计原因,给众多读者带来了诸多困惑。这也给我们很大的启发,当我们的某些设计容易引起别人的困惑和好奇时,不仅要将注意事项放在注释中,更应该将设计原因放在注释里,避免给使用者带来困扰。
“适合自己的才是最好的”。正如不同的 Map 实现类各有千秋,使用场景各有不同,我们需要根据具体需求和场景进行权衡一样,我们在设计方案时也会遇到类似的场景,我们能做的是根据场景选择最适合的方案。
我们遇到的任何问题,都是彻底掌握某个知识的绝佳机会。当我们遇到问题时,应该主动掌握相关知识,希望大家不仅能够知其然,还要知其所以然。
创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
相关文章:
什么?你不知道 ConcurrentHashMap 的 kv 不能为 null?
一、背景 最近设计某个类库时使用了 ConcurrentHashMap 最后遇到了 value 为 null 时报了空指针异常的坑。 本文想探讨下以下几个问题: (1) Map接口的常见子类的 kv 对 null 的支持情况。 (2)为什么 ConcurrentHashM…...
SQL复习04 | 复杂查询
1. 视图 视图和表的区别: 表保存的是实际的数据视图保存的是SELECT语句 视图的优点: 视图无需保存数据,可节省存储设备的容量可以将频繁使用的SELECT语句保存成视图,可大大提高效率 1.1 创建视图 CREATE VIEW 视图名称&…...
【面试题】Java面试题汇总(无解答)
此内容会持续补充。。。 基础 short s1 1; s1 s1 1;有错吗? short s1 1; s1 1; 有错吗?String str”aaa”,与 String strnew String(“aaa”)一样吗?String 和 StringBuilder、StringBuffer 的区别?Sring最大能存多大内容?…...
C++---背包模型---收服精灵(每日一道算法2023.3.11)
注意事项: 本题是"动态规划—01背包"的扩展题,优化的思路不多赘述,dp思路会稍有不同,下面详细讲解。 本题偏向阅读理解,给每种变量归类起名字很有帮助哦。 切记先看思路,再看代码。(大…...
day30_JS
今日内容 上课同步视频:CuteN饕餮的个人空间_哔哩哔哩_bilibili 同步笔记沐沐霸的博客_CSDN博客-Java2301 零、 复习昨日 一、作业 二、BOM 三、定时器 四、正则表达式 零、 复习昨日 事件 事件绑定方式鼠标事件 onmouseoveronmouseoutonmousemove 键盘事件 onkeydownonkeyupon…...
【Java学习笔记】19.Java 正则表达式(2)
前言 本章继续介绍Java的正则表达式。 Matcher 类的方法 索引方法 索引方法提供了有用的索引值,精确表明输入字符串中在哪能找到匹配: 序号方法及说明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分栏识别:两栏识别三栏识别都可以,本地部署完美拼接
大家好,我是微学AI,今天给大家带来OCR的分栏识别。 一、文本分栏的问题 在OCR识别过程中,遇到文字是两个分栏的情况确实是一个比较常见的问题。通常情况下,OCR引擎会将文本按照从左到右,从上到下的顺序一行一行地识别…...
Gin框架路由拆分与注册详解析
Gin框架路由拆分与注册详解析1.基本的路由注册2.路由拆分成单独文件或包3.路由拆分成多个文件4.路由拆分到不同的APP1.基本的路由注册 下面最基础的gin路由注册方式,适用于路由条目比较少的简单项目或者项目demo // StatCost 是一个统计耗时请求耗时的中间件 func…...
2020蓝桥杯真题凯撒加密 C语言/C++
题目描述 给定一个单词,请使用凯撒密码将这个单词加密。 凯撒密码是一种替换加密的技术,单词中的所有字母都在字母表上向后偏移 3 位后被替换成密文。即 a 变为 d,b 变为 e,⋯,w 变为z,x 变为 a࿰…...
taro+vue3小程序使用v-html渲染的内容为class写了样式无效
taro小程序如果是直接引入的一个less文件是包含scoped,只是当前页面采用。<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之前,建议先看下FPN网络,Faster-CNN和FCN的介绍:下面附上链接: R-CNN、Fast RCNN和Faster RCNN网络介绍FCN网络介绍…...
导航技术调研(CSDN_0023_20221217)
文章编号:CSDN_0023_20221217 目录 1. 惯性导航 2. 组合导航技术 3. 卡尔曼滤波 1. 惯性导航 惯性导航系统(INS-Inertial Navigation System)是上个世纪初发展起来的。惯性导航是一种先进的导航方法,但实现导航定位的原理却非常简单,它是…...
买卖股票的最佳时机 I II III IV
121. 买卖股票的最佳时机 自己的思路:采用求最长连续子串和题目的思路 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(Liquid Crystal Display)是一种工业字符型液晶,能够同时显示 1602 即 32 字符(16列两行) 第 1 脚: VSS 为电源地 第 2 脚: VDD 接 5V 正电源 第 3 脚: VL 为液晶显示器对比度调整端,接正电源时对比度最弱,接地时对比度最…...
英雄算法学习路线
文章目录零、自我介绍一、关于拜师二、关于编程语言三、算法学习路线1、算法集训1)九日集训2)每月算法集训2、算法专栏3、算法总包四、英雄算法联盟1、英雄算法联盟是什么?2、如何加入英雄算法联盟?3、为何会有英雄算法联盟&#…...
【设计模式】备忘录模式和迭代器模式
备忘录模式和迭代器模式备忘录模式代码示例迭代器模式代码示例使用迭代器遍历集合的同时不能删除/增加元素总结备忘录模式 备忘录模式,也叫快照(Snapshot)模式。 在 GoF的《设计模式》⼀书中,备忘录模式是这么定义的:…...
rapidcsv 写csv文件实例
csv实质是一个文本文件,可以使用rapidcsv写文件操作,如下实例: 第一行实质是从-1行开始,列是从0开始 #include "rapidcsv.h" #include <string> using namespace std; void CMFCApplication1Dlg::OnBnClickedBu…...
深度学习在微纳光子学中的应用
深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向: 逆向设计 通过神经网络快速预测微纳结构的光学响应,替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...
【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放
简介 前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
IT供电系统绝缘监测及故障定位解决方案
随着新能源的快速发展,光伏电站、储能系统及充电设备已广泛应用于现代能源网络。在光伏领域,IT供电系统凭借其持续供电性好、安全性高等优势成为光伏首选,但在长期运行中,例如老化、潮湿、隐裂、机械损伤等问题会影响光伏板绝缘层…...
C# 表达式和运算符(求值顺序)
求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如,已知表达式3*52,依照子表达式的求值顺序,有两种可能的结果,如图9-3所示。 如果乘法先执行,结果是17。如果5…...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...
VisualXML全新升级 | 新增数据库编辑功能
VisualXML是一个功能强大的网络总线设计工具,专注于简化汽车电子系统中复杂的网络数据设计操作。它支持多种主流总线网络格式的数据编辑(如DBC、LDF、ARXML、HEX等),并能够基于Excel表格的方式生成和转换多种数据库文件。由此&…...
全面解析数据库:从基础概念到前沿应用
在数字化时代,数据已成为企业和社会发展的核心资产,而数据库作为存储、管理和处理数据的关键工具,在各个领域发挥着举足轻重的作用。从电商平台的商品信息管理,到社交网络的用户数据存储,再到金融行业的交易记录处理&a…...
