当前位置: 首页 > 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…...

OpenLayers 可视化之热力图

注&#xff1a;当前使用的是 ol 5.3.0 版本&#xff0c;天地图使用的key请到天地图官网申请&#xff0c;并替换为自己的key 热力图&#xff08;Heatmap&#xff09;又叫热点图&#xff0c;是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...

使用VSCode开发Django指南

使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架&#xff0c;专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用&#xff0c;其中包含三个使用通用基本模板的页面。在此…...

docker详细操作--未完待续

docker介绍 docker官网: Docker&#xff1a;加速容器应用程序开发 harbor官网&#xff1a;Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台&#xff0c;用于将应用程序及其依赖项&#xff08;如库、运行时环…...

椭圆曲线密码学(ECC)

一、ECC算法概述 椭圆曲线密码学&#xff08;Elliptic Curve Cryptography&#xff09;是基于椭圆曲线数学理论的公钥密码系统&#xff0c;由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA&#xff0c;ECC在相同安全强度下密钥更短&#xff08;256位ECC ≈ 3072位RSA…...

(十)学生端搭建

本次旨在将之前的已完成的部分功能进行拼装到学生端&#xff0c;同时完善学生端的构建。本次工作主要包括&#xff1a; 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...

3.3.1_1 检错编码(奇偶校验码)

从这节课开始&#xff0c;我们会探讨数据链路层的差错控制功能&#xff0c;差错控制功能的主要目标是要发现并且解决一个帧内部的位错误&#xff0c;我们需要使用特殊的编码技术去发现帧内部的位错误&#xff0c;当我们发现位错误之后&#xff0c;通常来说有两种解决方案。第一…...

大数据零基础学习day1之环境准备和大数据初步理解

学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 &#xff08;1&#xff09;设置网关 打开VMware虚拟机&#xff0c;点击编辑…...

Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

C++ 求圆面积的程序(Program to find area of a circle)

给定半径r&#xff0c;求圆的面积。圆的面积应精确到小数点后5位。 例子&#xff1a; 输入&#xff1a;r 5 输出&#xff1a;78.53982 解释&#xff1a;由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982&#xff0c;因为我们只保留小数点后 5 位数字。 输…...

涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战

“&#x1f916;手搓TuyaAI语音指令 &#x1f60d;秒变表情包大师&#xff0c;让萌系Otto机器人&#x1f525;玩出智能新花样&#xff01;开整&#xff01;” &#x1f916; Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制&#xff08;TuyaAI…...