java jdk 中HashMap的源码解读
HashMap是我们在日常写代码时最常用到的一个数据结构,它为我们提供key-value形式的数据存储。同时,它的查询,插入效率都非常高。
在之前的排序算法总结里面里,我大致学习了HashMap的实现原理,并制作了一个简化版本的HashMap。 今天,趁着项目的间歇期,我又仔细阅读了Java中的HashMap的实现。
HashMap的初始化:
- public HashMap(int initialCapacity, float loadFactor)
- public HashMap(int initialCapacity)
- public HashMap()
- public HashMap(Map<? extends K, ? extends V> m)
最近看到几篇精彩的文章:
这些文章让我收获良多, 但是有些地方说的不够详细, 在此写下本人对上述文章的总结和理解, 希望可以给需要的朋友带来一些帮助.
1. 概述
HashMap在底层采用数组+链表的形式存储键值对.
在HashMap中定义了一个内部类Entry<K, V>, 该内部类是对key-value的抽象. Entry类包含4个成员: key, value, hash, next. key和value的意义很清晰, hash表示key的hash值, next是指向下一个Entry对象的引用.
HashMap内部维护了一个Entry<K, V>[] table, 数组table中的Entry元素是一个Entry链表的头结点(理解这一点很重要).
2. put/get方法
向HashMap中添加键值对时, 程序会根据key的hashCode值计算出hash值, 然后对hash值取模, 模数是table.length. 假如取模的结果为index, 则取出table[index]. table[index]可能为null, 也可能是一个Entry对象. 如果为null, 则直接存储. 否则计算key.equals(table[index].key), 如果为false, 就取出table[index].next, 继续调用key的equals方法, 直到equals方法返回true, 或者比较完链表中所有Entry对象.
- public V put(K key, V value) {
- if (key == null)
- return putForNullKey(value);
- // 对hashCode值进行二次hash得到最终的hash值
- int hash = hash(key.hashCode());
- // 根据hash值定位数组中的索引位置
- int i = indexFor(hash, table.length);
- // 遍历table[i]位置处的链表
- for (Entry<K, V> e = table[i]; e != null; e = e.next) {
- Object k;
- // 如果hash值相同且equals返回true, 则替换原来的value值
- if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- modCount++;
- // 如果之前函数没有return, 将该键值对插入table[i]链表中
- addEntry(hash, key, value, i);
- return null;
- }
理解了put方法, 那么get方法就会很容易理解:
- public V get(Object key) {
- if (key == null)
- return getForNullKey();
- int hash = hash(key.hashCode());
- // 首先根据hash值计算index, 然后取出index处的链表的头结点. 遍历链表.
- for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
- Object k;
- if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
- return e.value;
- }
- return null;
- }
3. HashMap的容量和索引位置确定
前面没有叙述HashMap的容量问题, 是因为容量是与索引位置计算紧密相关的.
理解HashMap的容量就需要关注成员变量size, loadFactor, threshold.
size表示HashMap中实际包含的键值对个数.
loadFactor表示负载因子, loadFactor的值越大, 则对table数组的利用率越大, 相当于节省内存空间. 但是loadFactor的值增大, 同时也会导致hash冲突的概率增加, 从而使得程序效率降低. loadFactor的取值应该兼顾内存空间和效率, 默认值为0.75.
threshold表示极限容量, 计算公式为threshold = (int)(capacity * loadFactor); 当size达到threshold时, 就需要对table数组扩容.
HashMap的容量大小就是table.length. 由于java中取模是一个效率低下的操作, 所以出于性能的考虑, HashMap的容量被设计为2的N次方. 如此hash%table.length就可以转换为hash&(table.length-1). 与运算的效率比取模运算高效很多.
- 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);
- // 计算大于initialCapacity的最小的2的N次方数
- int capacity = 1;
- while (capacity < initialCapacity)
- capacity <<= 1;
- this.loadFactor = loadFactor;
- // 求出极限容量
- threshold = (int) (capacity * loadFactor);
- // table的容量被设计为2的N次方
- table = new Entry[capacity];
- init();
- }
如果使用无参的构造函数创建HashMap, 则容量默认为16, 负载因子默认为0.75.
indexFor函数用于确定索引位置:
- static int indexFor(int h, int length) {
- // 当length为2的N次方时相当于h%table.length, 但效率要高效很多
- return h & (length - 1);
- }
4. rehash
前面提到过, 当size达到threshold时, 就需要对table数组扩容. 调用put函数向HashMap中插入一个键值对时会调用到addEntry(hash, key, value, i)方法:
- void addEntry(int hash, K key, V value, int bucketIndex) {
- // 取出索引处的Entry对象
- Entry<K, V> e = table[bucketIndex];
- // 更新索引处链表的头结点, 并使新的头结点的next属性指向原来的头结点
- table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
- // 当size大于threshold时扩容数组, 容量增加至原来的2倍. 保证table的容量始终是2的N次方
- if (size++ >= threshold)
- resize(2 * table.length);
- }
resize用于扩容数组. 数组的length增加大了, 那么HashMap中已有的键值对就必须重新进行hash, 这就是rehash. 如果不进行rehash, 就会使得put和get时table数组的length不同, 从而导致get方法无法取出原先put存入的键值对.
- void resize(int newCapacity) {
- Entry[] oldTable = table;
- int oldCapacity = oldTable.length;
- if (oldCapacity == MAXIMUM_CAPACITY) {
- threshold = Integer.MAX_VALUE;
- return;
- }
- Entry[] newTable = new Entry[newCapacity];
- transfer(newTable);
- table = newTable;
- threshold = (int) (newCapacity * loadFactor);
- }
- void transfer(Entry[] newTable) {
- Entry[] src = table;
- int newCapacity = newTable.length;
- // 对已有的键值对进行rehash
- for (int j = 0; j < src.length; j++) {
- // 得到j处的链表的头结点
- Entry<K, V> e = src[j];
- // 遍历链表
- if (e != null) {
- src[j] = null;
- do {
- // 进行rehash
- Entry<K, V> next = e.next;
- int i = indexFor(e.hash, newCapacity);
- e.next = newTable[i];
- newTable[i] = e;
- e = next;
- } while (e != null);
- }
- }
- }
从源码可以看出, rehash对性能的影响是非常大的, 因此我们应该尽量避免rehash的发生. 这就要求我们预估需要存入HashMap的键值对的数量, 根据数量在创建HashMap对象时指定合适的容量和负载因子.
5. hash碰撞和HashMap的退化
hash碰撞在HashMap中的表现为: 不同的key, 计算出相同的index. 如果对所有的key调用indexFor方法的返回值都是相同的, 那么HashMap就退化为链表, 这对性能的影响也是非常大的. 几个月前的闹得沸沸扬扬的hash碰撞攻击就是基于这样的原理.
常用的web框架都会将请求中的参数保存在HashMap(或HashTable)中, 如果客户端根据Web应用框架采用的Hash函数来通过某种Hash攻击的方式获得大量的碰撞, 那么HashMap就会退化为链表, 服务器有可能处理一次请求要花上十几分钟甚至几个小时的时间...
6. 线程安全
HashMap是线程不安全的, 如果遍历HashMap的过程中修改了HashMap, 那么就会抛出java.util.ConcurrentModificationException异常:
- final Entry<K, V> nextEntry() {
- if (modCount != expectedModCount)
- throw new ConcurrentModificationException();
- Entry<K, V> e = next;
- if (e == null)
- throw new NoSuchElementException();
- if ((next = e.next) == null) {
- Entry[] t = table;
- while (index < t.length && (next = t[index++]) == null)
- ;
- }
- current = e;
- return e;
- }
modCount是HashMap的成员变量, 用于表示HashMap的状态. expectedModCount是遍历初始时modCount的值. 如果在遍历过程中改变了modCount的值就会导致modCount和expectedModCount不相等, 从而抛出异常. put, clear, remove等方法都会导致modCount的值改变.
java jdk 中HashMap的源码解读的更多相关文章
- Java中HashMap的源码分析
先来回顾一下Map类中常用实现类的区别: HashMap:底层实现是哈希表+链表,在JDK8中,当链表长度大于8时转换为红黑树,线程不安全,效率高,允许key或value为null HashTable ...
- 浅析JDK中ServiceLoader的源码
前提 紧接着上一篇<通过源码浅析JDK中的资源加载>,ServiceLoader是SPI(Service Provider Interface)中的服务类加载的核心类,也就是,这篇文章先介 ...
- java.io.BufferedWriter API 以及源码解读
下面是java se 7 API 对于java.io.BufferedWriter 继承关系的描述. BufferedWriter可以将文本写入字符流.它会将字符缓存,目的是提高写入字符的效率. bu ...
- java.io.writer API 以及 源码解读
声明 我看的是java7的API文档. 如下图所示,java.io.writer 继承了java.lang.Object,实现的接口有Closeable, Flushable, Appendable, ...
- go中sync.Cond源码解读
sync.Cond 前言 什么是sync.Cond 看下源码 Wait Signal Broadcast 总结 sync.Cond 前言 本次的代码是基于go version go1.13.15 da ...
- go中sync.Mutex源码解读
互斥锁 前言 什么是sync.Mutex 分析下源码 Lock 位运算 Unlock 总结 参考 互斥锁 前言 本次的代码是基于go version go1.13.15 darwin/amd64 什么 ...
- go中semaphore(信号量)源码解读
运行时信号量机制 semaphore 前言 作用是什么 几个主要的方法 如何实现 sudog 缓存 acquireSudog releaseSudog semaphore poll_runtime_S ...
- go中sync.Once源码解读
sync.Once 前言 sync.Once的作用 实现原理 总结 sync.Once 前言 本次的代码是基于go version go1.13.15 darwin/amd64 sync.Once的作 ...
- JDK容器类Map源码解读
java.util.Map接口是JDK1.2开始提供的一个基于键值对的散列表接口,其设计的初衷是为了替换JDK1.0中的java.util.Dictionary抽象类.Dictionary是JDK最初 ...
随机推荐
- Redis入门学习(学习过程记录)
Redis(入门笔记) 学习一个大的技术点,然后顺带着就把这个技术点的面试题给学习了. 学习完一个技术后,如果面试题还不能够解答的话,只能说明学的不精,需要查漏补缺. 下一个学习的方向:Redis-非 ...
- linux 配置网卡、远程拷贝文件、建立软硬链接、打包/解包、压缩/解压缩、包操作、yum配置使用、root密码忘记
目录 一.配置网卡 二.xshell连接 三.远程拷贝文件 四.建立软硬连接 五.打包/解包和压缩/解压缩 六.包操作 七.配置yum源 配置yum源 配置阿里云源 常用命令 yum其他命令 八.重置 ...
- 控制游戏/app使用期限/过期,证书有效期,弹出公告支持离线使用/后端控制,支持Swift、Objective-C集成非常简单,永久免费框架
CertCheck.framework (for iOS) 只能运行于真机上,切勿在模拟器上使用 这是一个控制游戏/app使用期限(到设定的时间无法打开app),或发布弹出公告永久免费的框架,可以全版 ...
- Java网络编程——Socket
网络是连接不同计算机的媒介,不同的计算机依靠网络来互相通信,即传递数据. Java中与网络编程相关的部分主要是Socket(套接字),它作为一种抽象的结构,实现了与通信相关的各类方法,构成一套完整的通 ...
- OSLab:开启保护模式
日期:2019/5/22 关键词:操作系统:OS:保护模式:A20地址线激活:分页开启:二级页表的设置 PS:OSLAB实验课的整理. 本文主要内容是分析操作系统中一个简易的MBR. 建议先阅读:ht ...
- redis处理高并发
参考: https://www.cnblogs.com/wanlei/p/10464517.html 关于Redis处理高并发 Redis的高并发和快速原因 1.Redis是基于内存的,内存的读写速度 ...
- redis++:Redis持久化 rdb & aof 工作原理及流程图 (三)
RDB的原理: 在Redis中RDB持久化的触发分为两种:自己手动触发与Redis定时触发. 针对RDB方式的持久化,手动触发可以使用: 1):save:会阻塞当前Redis服务器,直到持久化完成,线 ...
- iOS 性能优化点
用 ARC 管理内存 在正确的地方使用 reuseIdentifier 尽量把 views 设置为完全不透明 如果你有透明的 Views 你应该设置它们的 opaque(不透明)属性为 YES.例如一 ...
- JS烟花案例优化版
不明白为什么是烟花优化版本的先参考作者的烟花基础版本 烟花优化版本主要实在优化爆炸的范围和运动上做了优化,爆炸范围我们采用已圆的爆炸方式,以鼠标点击的位置为圆形爆炸的烟花效果 <!DOCTYPE ...
- js拖拽效果的实现及原理
元素拖拽分成3个步骤:按下鼠标,移动鼠标,松开鼠标. 拖拽原理:按下拖拽元素后开始监听文档中鼠标移动事件,然后再监听鼠标松开事件:鼠标移动时,元素div要随着鼠标一起移动,需要计算元素div位移的距离 ...