禅与计算机 禅与计算机
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

sharkchili

计算机禅修者
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析 Java 泛型的魅力与机制
    • 来聊聊Java为什么只有值传递
    • 来聊聊大厂常问的SPI工作原理
    • 来聊聊session与token的区别
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
      • 写在文章开头
      • HashMap数据结构概览
      • 不同版本的HashMap底层数据结构
      • HashMap是如何解决哈希冲突的?
      • 为什么1.8之后要加一个链表转红黑树的操作
      • HashMap底层的数据结构红黑树算法在大数据情况下最大高度可能是多少呢?
      • HashMap几种构造方法
        • 默认构造函数的初始化流程
        • 将另一个Map存到当前Map中的构造函数工作流程
        • 指定初始化容量的HashMap
        • 指定容量和负载因子的构造函数
      • HashMap核心源码详解
        • HashMap对应put方法工作流程
        • HashMap的get方法的流程
        • hashMap扩容机制
        • HashMap 的容量为什么需要是 2 的幂次方,而这个幂为什么是31呢?
        • 重写equals为什么要重写hashcode?
        • HashMap 常见遍历以及安全删除代码要怎么做?
      • HashMap多线程可能导致的问题
      • 更多关于HashMap与红黑树的详解
      • 关于我
      • 参考文献
    • 一文带你速通HashMap底层核心数据结构红黑树
    • 深入HashMap底层理解阿里手册的遍历守则
    • LinkedHashMap源码到面试题的全解析
    • 空间预分配思想提升HashMap插入效率
    • 解析Java集合工具类:功能与实践
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • 聊聊Java关于IO流中的设计模式
    • 为什么流不关闭会导致内存泄漏
    • 聊聊java零拷贝的几种实现
    • Java8流式编程入门
    • Java8流式编程详解
    • 来聊聊java8的数值流
    • 聊聊Java8中的函数式编程
    • 一文速通lambda与函数式编程
    • 基于lambda简化设计模式
    • Java8函数式方法引用最佳实践
    • 使用Java8并行流的注意事项
    • 详解java数值类型核心知识点
    • 将一维数组按指定长度转为二维数组
    • 33个非常实用的JavaScript一行代码
    • 多种数组去重性能对比
    • 防抖与节流函数
    • 比typeof运算符更准确的类型判断
    • new命令原理
    • ES6面向对象
    • ES5面向对象
    • 判断是否为移动端浏览器
    • JS随机打乱数组
    • JS获取和修改url参数
    • 三级目录

  • 并发编程

  • JVM相关

  • 深入理解Spring框架

  • Java核心技术
  • Java基础
sharkchili
2022-12-13
目录

Java常用集合类HashMap深度解析

# 写在文章开头

在Java编程中,HashMap 是最常用的数据结构之一,它提供了高效的键值对存储和检索功能。无论是日常开发还是技术面试,对 HashMap 的深入理解和熟练应用都是至关重要的。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的技术人,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。

同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# HashMap数据结构概览

HashMap是我们比较常用的集合类型,它是以键值对的逻辑结构来存储数据的。

  1. HashMap允许存储null键或者null值的键值对。
  2. HashMap非线程安全。
  3. HashMap底层初始化用的是数组+链表,当链表长度大于8(默认值)时,若size小于64则进行2倍扩容,反之会对对应的数组桶进行链表转红黑树操作。
  4. HashMap默认容量大小为16。

# 不同版本的HashMap底层数据结构

在JDK1.8 之前,底层采用数组+链表,用(n - 1) & hash找到数组索引位置,若冲突则用拉链法解决冲突,当冲突碰撞加剧的时候,可能存在查询性能退化为O(n)的情况。

JDK1.8 之后底层采用数组+链表作为初始结构,当某个桶链表长度大于8时,默认情况下,会判断数组长度是否小于64,若小于64则使用resize()进行扩容。反之调用treeifyBin()将bucket内所有节点转红黑树节点:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
       //......
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //当链表元素大于8时,调用treeifyBin查看当前bucket数是否大于64,若大于则将bucket节点转红黑树节点,反之进行bucket扩容
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //......
                }
            }
            //......
        }
       //......
        return null;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# HashMap是如何解决哈希冲突的?

在JDK7之前HashMap采用的是链式寻址法解决哈希冲突的,而JDK8之后则未转红黑树前采用的就是链式寻址法,转红黑树之后就借用红黑树天然有序性(黑平衡)解决哈希冲突。

# 为什么1.8之后要加一个链表转红黑树的操作

链表查询的时间复杂度为O(n)在数据量较少的情况下查询效率不错,一旦冲突非常厉害,链表数量暴增的话查询效率或者添加效率都会十分低下,所以就需要转为红黑树,通过黑平衡结构保证插入和查询效率都为O(logN),并且红黑树是黑平衡树,所以相较于AVL不会进行频繁的翻转保证平衡的操作。

# HashMap底层的数据结构红黑树算法在大数据情况下最大高度可能是多少呢?

理想情况为2log2 (n+1),但是Java中这个情况考虑的因素就很多了:

  1. 得看看堆区大小以及这个节点的大小。
  2. 就Java而言这种情况很少见,如果大数据都在一个bucket中,就说明设置的哈希算法有问题了。

# HashMap几种构造方法

# 默认构造函数的初始化流程

如下所示,仅仅初始化负载因子,默认为0.75f,这个负载因子的作用即当当前数组大小>数组容量*负载因子时会进行扩容操作。

public HashMap() {
        this.loadFactor = 0.75F;
    }
1
2
3

# 将另一个Map存到当前Map中的构造函数工作流程

该方法会将阈值设置为默认值DEFAULT_LOAD_FACTOR(0.75f),然后将传入的map通过putMapEntries方法将键值对逐个存入。

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
1
2
3
4

# 指定初始化容量的HashMap

通过外部参数传入initialCapacity,初始化map底层数据的大小。


public HashMap(int initialCapacity) {
         this(initialCapacity, DEFAULT_LOAD_FACTOR);
     }
     
1
2
3
4
5

# 指定容量和负载因子的构造函数

public HashMap(int initialCapacity, float loadFactor) {
         if (initialCapacity < 0)
             throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
         if (initialCapacity > MAXIMUM_CAPACITY)
             initialCapacity = MAXIMUM_CAPACITY;
         if (loadFactor <= 0 || Float.isNaN(loadFactor))
             throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
         this.loadFactor = loadFactor;
         this.threshold = tableSizeFor(initialCapacity);
     }
1
2
3
4
5
6
7
8
9
10

# HashMap核心源码详解

# HashMap对应put方法工作流程

HashMap的put方法的逻辑比较清晰,大体的逻辑为:

  1. 通过hash算法得到桶的位置
  2. 尝试将键值对存到hash计算后的桶的位置中。
  3. 无冲突直接创建新节点保存。
  4. 有冲突则按照链表或者红黑树的逻辑进行插入。

入口代码

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
1
2
3

进入putVal,可以看到要做的就是计算出桶的位置然后完成插入。



final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

    //计算(n - 1) & hash并查看是否在桶中,若不在则直接创建一个结点放到这个桶中
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);

    // 桶中已经存在元素(处理hash冲突)
    else {
        Node<K,V> e; K k;
        // 判断table[i]中的元素是否与插入的key一样,若相同那就直接使用插入的值p替换掉旧的值e。
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

        // 判断插入的是否是红黑树节点
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

        // 不是红黑树节点则说明为链表结点
        else {
            // 不断遍历到达链表尾部
            for (int binCount = 0; ; ++binCount) {
                // 不断往链表后面走,若为空则说明到达尾部,直接添加节点
                if ((e = p.next) == null) {
                    
                    p.next = newNode(hash, key, value, null);
                    // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
                    // 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
                    // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) {
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

# HashMap的get方法的流程

整体逻辑和put也差不多,计算桶的位置,然后看看是那种数据结构,若是链表则遍历链表然后进行hashCode和equals方法比较是否一致然后返回,红黑树同理。

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}



final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 数组元素相等
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 桶中不止一个节点
        if ((e = first.next) != null) {
        
            // 若是红黑树,则走红黑树的遍历逻辑
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                
            // 反之说明这是一个链表
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# hashMap扩容机制

扩容方法整体做的就是数组迁移,注释都在下方,这里我们只需注意JDK核心设计,就是迁移的核心逻辑代码如下。 是JDK1.8中的优化操作,可以不需要再重新计算每一个元素的哈希值,如下图所示,将当前元素的hash值&容器旧的容量,如果高位有1则说明他要落到新的bucket中。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;

		// 计算扩容的容量以及新的阈值
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
        				//遍历,将旧链表元素迁移到新的链表上
                        } while ((e = next) != null);
                        //维护原来的尾节点
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 维护新扩容的尾节点
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# HashMap 的容量为什么需要是 2 的幂次方,而这个幂为什么是31呢?

先来回答第一个问题,容量为什么是2的幂次方,首先我们步入hashMap的源码中查看。hashMap计算键值对存到桶中索引位置的代码。

i = (n - 1) & hash
1

在n为2的次幂情况下,(n - 1) & hash通过数学公式其实可以推导为 hash%n。

我们假设hash为1,使用不同的2次幂可以印证我们上面的论述。

1. n为2的2次方:  4===> 3&1==1%4
2. n为2的3次方:  8===> 7&1==1%8
3. .....
1
2
3

除此之外,使用2的次幂进行计算时碰撞次数会少于非2的次幂。同样的,我们假设hash值的7、8、9、10。hashMap的容量n分别假设为8(2的3次方)和9。

n为16的计算结果如下,碰撞0次。

7===>7
8===>0
9===>1
10===>2
1
2
3
4

n为9的计算结果,碰撞2次。

7===>0
8===>8
9===>8
10===>8
1
2
3
4

再来了解一下hashCode的东西可以看到计算机hash的乘积数写死为31,这是为什么呢?

int hash = 0;
    private char[] value;

    public int hashCode() {

        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

我们再来看看StackOverflow的回答:

The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as
multiplication by 2 is equivalent to shifting. The advantage of using
a prime is less clear, but it is traditional. A nice property of 31 is
that the multiplication can be replaced by a shift and a subtraction
for better performance: 31 * i == (i << 5) - i. Modern VMs do this
sort of optimization automatically.

大意说的是如果使用双数的话,计算就是使用<<1,这样的计算很可能会出现数据溢出,使用奇数31则会JVM会将其优化成一个数学公式:31 * i == (i << 5) - i,如此依赖无论怎么计算hash值都不会超过int的最大值2^31-1 (0111 1111 | 1111 1111 | 1111 1111 | 1111 1111) ,那么问题又来了,别的小于31的奇数不会超过int的范围,为什么乘积数不用别的值而一定要用31呢?我们不妨写一个demo进行实验一下,不同的乘积数计算出的hash的值的碰撞数是多少

基于源码推导出hashCode优化后的公式,31 * i == (i << 5) - i 推导过程就在下方:

private static int hash;
    /**
     * 31 * h
     * ===> (2^5-1) * h
     * ====> (1<< 5-1 ) * h
     *  ===> (1<< 5) * h -h
     * 最终结果
     * ====> h << 5 - h
     * 从而避免计算溢出 以及使用位移提升性能
     */
    public int hashCode(char[] value, int num) {
        hash = resetHash();
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                

                h = num * h + val[i];
            }
            hash = h;
        }
        return h;
    }

    private int resetHash() {
        return 0;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

而且乘积数为为是31还有下面两个好处:

  1. 冲突最少。
  2. 31计算的值都在取值范围内。

对此,笔者使用了下面这段代码印证这个结果:



    public int hashCode(char[] value, int num) {
        hash = resetHash();
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                /**
                 * 31 * h
                 * ===> (2^5-1) * h
                 * ====> (1<< 5-1 ) * h
                 *  ===> (1<< 5) * h -h
                 * 最终结果
                 * ====> h << 5 - h
                 * 从而避免计算溢出 以及使用位移提升性能
                 */

                h = num * h + val[i];
            }
            hash = h;
        }
        return h;
    }

    private int resetHash() {
        return 0;
    }

    @Test
    public void hashCodeTest() {
        int size = 1000_0000;
        caculHashCode(size,2);
        caculHashCode(size,4);
        caculHashCode(size,7);
        caculHashCode(size,31);
        caculHashCode(size,32);
        caculHashCode(size,33);
        caculHashCode(size,64);
        /**
         * 输出结果 31碰撞率低 31之后的质数也行 但是最大值超过int 范围了
         * 2:重复了9997896
         * 4:重复了9939942
         * 7:重复了8696061
         * 31:重复了0
         * 32:重复了5900000
         * 33:重复了8
         * 64:重复了9590000
         */


    }

    private void caculHashCode(int size, int num) {
        HashSet<Integer> set2 = new HashSet<>();
        for (int i = 0; i < size; i++) {
            set2.add(hashCode(String.valueOf(i).toCharArray(), num));
        }
        System.out.println(num + ":重复了" + (size - set2.size()));
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

总结一下:

  1. 通过2的次幂使得公式,可以使得公式变为取模运算,提升计算效率。
  2. 次幂为31计算结果永远小于int类型避免计算溢出,在int类型区间中31次幂碰撞率最低。

# 重写equals为什么要重写hashcode?

我们在日常开发中,某些情况下我们判断对象是否相等需要有自己的一套逻辑,这时候我们就需要重写equals方法,但我们在重写equals方法时不重写hashCode方法,很可能会造成很严重的集合操作事故。

我们以以这样的一个场景为例,由于业务的需要,我们判断产品对象的逻辑需要重写,只有id相等的产品对象才是相等的对象。所以我们重写了产品对象的equals方法,关键代码如下所示:


public class Product {
    private Integer id;
    private String name;

    public Product(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

.....get、set

    @Override
    public String toString() {
        return "Product{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    // 重写equals
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Product product = (Product) o;
        return Objects.equals(id, product.id);
    }

  


    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

这时候有个场景要求我们对产品进行去重操作,代码以及运行结果如下所示,可以看到明明是两个我们逻辑上相同的产品却都被存到set集合中,这是为什么呢?我们不妨看看set的add源码

public static void main(String[] args) {
        Product product1 = new Product(1, "id为1的馒头旧版本");
        Product product2 = new Product(1, "id为1的馒头新版本");
       


        HashSet<Product> products = new HashSet<Product>();
        boolean contains = products.contains(product1);
        products.add(product1);
        products.add(product2);
        // 使用equals判断是否相等
        System.out.println(product1.equals(product2));
        // 查看HashSet中元素个数
        System.out.println(products.size());
        for (Product product : products) {
            System.out.println(product.toString());
        }
    /**
         * true
         * 2
         * Product{id=1, name='id为1的馒头旧版本'}
         * Product{id=1, name='id为1的馒头新版本'}
         */


    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

首先来到add的浅层调用,我们不妨直接步入查看:

public boolean add(E e) {
	//调用map方法完成元素插入
        return map.put(e, PRESENT)==null;
    }
1
2
3
4

可以看到put代码本质上就算通过key得到一个hash值,这个值和上一次插入的元素hash值一样,然后调用putVal尝试将元素插入:

public V put(K key, V value) {
		//通过hash计算得到hash值,然后调用putVal进行元素插入
        return putVal(hash(key), key, value, false, true);
    }
1
2
3
4

假设我们没有重写equals就会发现,因为上一步hash值一样,但是equals没有重写导致两个元素比较的是引用地址,因为引用地址的不同导致hashMap认定两者不为同一个key进而导致逻辑上认为相同的元素,却都插入到map中:

对此我们给出putVal的源码,读者可基于该说法进行代入性的调试:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
      //......
        else {
            Node<K,V> e; K k;
			//如果hash相同且对象相等则走这段逻辑,设置一个值直接返回不进行插入操作
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
		//否则进行插入操作
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
       .......
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# HashMap 常见遍历以及安全删除代码要怎么做?

示例代码如下,读者可自行调试,大抵是建议使用entrySet,以及在循环时安全删除建议使用entrySet的迭代器形式:

private static HashMap<String, String> map = new HashMap();

    @Before
    public void before() {
        int size = 1000_0000;
        for (int i = 0; i < size; i++) {
            map.put(String.valueOf(i), String.valueOf(i));
        }
    }

    @Test
    public void CycleTest() {
        long start = System.currentTimeMillis();
        Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, String> entry = iterator.next();
            String key = entry.getKey();
            String value = entry.getValue();
        }
        long end = System.currentTimeMillis();
        System.out.println("entry iterator遍历:" + (end - start));


        start = System.currentTimeMillis();
        Iterator<String> keySetIterator = map.keySet().iterator();
        while (keySetIterator.hasNext()) {
            String key = keySetIterator.next();
            String value = map.get(key);

        }
        end = System.currentTimeMillis();
        System.out.println("keySet Iterator遍历:" + (end - start));

        start = System.currentTimeMillis();
        for (Map.Entry<String, String> entry : map.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
        }
        end = System.currentTimeMillis();
        System.out.println("entrySet 遍历:" + (end - start));


        start = System.currentTimeMillis();
        for (String key : map.keySet()) {
            String resultKey = key;
            String value = map.get(key);
        }
        end = System.currentTimeMillis();
        System.out.println("foreach keyset 遍历:" + (end - start));


        start = System.currentTimeMillis();
        map.forEach((k, v) -> {
            String key = k;
            String value = v;
        });
        end = System.currentTimeMillis();
        System.out.println("lambda 遍历:" + (end - start));



        start = System.currentTimeMillis();
        map.entrySet().stream().forEach((entry)->{
            String key=entry.getKey();
            String value=entry.getValue();
        });
        end = System.currentTimeMillis();
        System.out.println("stream 遍历:" + (end - start));



        start = System.currentTimeMillis();
        map.entrySet().parallelStream().forEach((entry)->{
            String key=entry.getKey();
            String value=entry.getValue();
        });
        end = System.currentTimeMillis();
        System.out.println("并行流 遍历:" + (end - start));

        /**
         * 输出结果 entrySet性能最好
         * entry iterator遍历:228
         * keySet Iterator遍历:284
         * entrySet 遍历:228
         * foreach keyset 遍历:284
         * lambda 遍历:237
         * stream 遍历:230
         * 并行流 遍历:134
         */


        /**
         * 两个entry反编译的字节码一样说明时长一样
         * long start = System.currentTimeMillis();
         *
         *         Entry entry;
         *         String var6;
         *         for(Iterator iterator = map.entrySet().iterator(); iterator.hasNext(); var6 = (String)entry.getValue()) {
         *             entry = (Entry)iterator.next();
         *             String key = (String)entry.getKey();
         *         }
         *
         *         long end = System.currentTimeMillis();
         *         System.out.println("entry iterator遍历:" + (end - start));
         *
         *
         *          start = System.currentTimeMillis();
         *
         *         String var10;
         *         Iterator var13;
         *         Entry entry;
         *         for(var13 = map.entrySet().iterator(); var13.hasNext(); var10 = (String)entry.getValue()) {
         *             entry = (Entry)var13.next();
         *             String key = (String)entry.getKey();
         *         }
         *
         *         end = System.currentTimeMillis();
         *         System.out.println("entrySet 遍历:" + (end - start));
         */


        /**
         * 安全删除
         */
        Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, String> entry = it.next();
            if (entry.getKey() .equals("1") ) {
                // 删除
                System.out.println("del:" + entry.getKey());
                iterator.remove();
            } else {
                System.out.println("show:" + entry.getKey());
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136

# HashMap多线程可能导致的问题

具体可以参考笔者这篇文章,大致原因是JDK7版本的HashMap在多线程扩容期间,一个线程指向迁移节点后被挂起,另一个线程完成扩容后。这个线程重新那会CPU执行权在执行原有的迁移逻辑,会造成死循环进而打爆CPU 100%问题,而JDK8则可能会导致同一个两个key计算到相同的hash值进而导致后put的元素将前一个元素覆盖。

更多可以参考笔者写的这篇文章:

Java并发容器小结:https://blog.csdn.net/shark_chili3007/article/details/122784001 (opens new window)

# 更多关于HashMap与红黑树的详解

具体可以参考笔者写的这篇文章:

数据结构与算法之红黑树小结:https://mp.weixin.qq.com/s?__biz=MzkwODYyNTM2MQ==&mid=2247486785&idx=1&sn=a269faca6f162975d85c9bd6d5f7ff57&chksm=c0c659fff7b1d0e96f96470e72d5ce2fcfffa4fcd23dcb8df9b31a2f48ff2136aa67bc15bbb0#rd (opens new window)

如果读者想更加直观查看红黑树生成过程可以看看这个网站

Red/Black Tree Visualization:https://www.cs.usfca.edu/~galles/visualization/RedBlack.html (opens new window)

# 关于我

我是 sharkchili ,CSDN Java 领域博客专家,mini-redis的作者,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。

同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 参考文献

hashCode为什么使用31作为乘积数:https://bugstack.cn/md/java/interview/2020-08-04-面经手册 · 第2篇《数据结构,HashCode为什么使用31作为乘数?》.html (opens new window)

重写equals为什么要重写hashCode:https://www.modb.pro/db/49230 (opens new window)

HashMap源码&底层数据结构分析:https://javaguide.cn/java/collection/hashmap-source-code.html#hashmap-简介 (opens new window)

HashMap如何解决哈希冲突?:https://blog.csdn.net/ahuangqingfeng/article/details/124286368 (opens new window)

HashMap扩容大小为什么是2的幂:https://www.jianshu.com/p/5ddf1b664641 (opens new window)

编辑 (opens new window)
上次更新: 2026/03/26, 01:05:31
Java集合框架深度解析与面试指南
一文带你速通HashMap底层核心数据结构红黑树

← Java集合框架深度解析与面试指南 一文带你速通HashMap底层核心数据结构红黑树→

最近更新
01
基于EasyExcel实现高效导出
03-25
02
从开源框架中学习那些实用的位运算技巧
03-25
03
浅谈分布式架构设计思想和常见优化手段
03-25
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×