HashMap---1.7源码阅读
JDK1.7
注意点:1、缺省值length为空,初始大小默认为16.每次扩容为原来的2倍,负载因子为0.75.扩容阈值为16*0.75=12,是超过12的时候进行扩容
2、数组+链表。冲突时,头插法。先扩容,再插入(1.8先插入再扩容)
3、key为null,且数组不为空,则放在table[0]的位置
4、hashcode&(length-1)为数组下标。扩容后,元素要么在原位置,要么在原位置+原length的位置
5、hashMap大小必是2的幂次方数。目的是尽量较少碰撞,也就是要尽量把数据分配均匀
1、数组 存放元素的是Entry<K,V>[] table数组,初始为空
1 | transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE |
无参构造方法,默认空,插入元素的时候初始化为16,负载因子为0.75
1 2 3 | public HashMap( int initialCapacity) { this (initialCapacity, DEFAULT_LOAD_FACTOR); } |
2、put方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public V put(K key, V value) { if (table == EMPTY_TABLE) { //数组为空,则先初始化 inflateTable(threshold); } if (key == null ) //key为null,则方法 return putForNullKey(value); int hash = hash(key); //计算key的hashcode值,先去key的hashcode,再经过4次位移运算和5次异或运算,得到hashcode int i = indexFor(hash, table.length); //与运算,得到放在table的数组下表 for (Entry<K,V> e = table[i]; e != null ; e = e.next) { //该数组位置无参数则直接放,有参则遍历链表,key的hashcode相等且equals相同则覆盖, Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess( this ); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null ; } private V putForNullKey(V value) {<br> for (Entry<K,V> e = table[ 0 ]; e != null ; e = e.next) { //已有key为null的值则覆盖<br> if (e.key == null) {<br> V oldValue = e.value;<br> e.value = value;<br> e.recordAccess(this);<br> return oldValue;<br> }<br> }<br> modCount++;<br> addEntry(0, null, value, 0); //无key为null,则放在0的位置 <br> return null;<br>}final int hash(Object k) { //计算key的hashcode值<br> int h = hashSeed;<br> if (0 != h && k instanceof String) {<br> return sun.misc.Hashing.stringHash32((String) k);<br> }<br><br> h ^= k.hashCode();<br><br> h ^= (h >>> 20) ^ (h >>> 12);<br> return h ^ (h >>> 7) ^ (h >>> 4);<br>}static int indexFor(int h, int length) { //利用hashcode和数组长度-1“与”运算得到数组位置<br> return h & (length-1);<br>}<br><br>void addEntry(int hash, K key, V value, int bucketIndex) {//插入方法<br> if ((size >= threshold) && (null != table[bucketIndex])) {<br> resize(2 * table.length);<br> hash = (null != key) ? hash(key) : 0;<br> bucketIndex = indexFor(hash, table.length);<br> }<br><br> createEntry(hash, key, value, bucketIndex);<br>} |
数组为空,则先初始化
key为null则判断,已有key为null则覆盖,无key为null则放在table数组的0位置
计算key的hashcode值,先去key的hashcode,再经过4次位移运算和5次异或运算,得到hashcode
hashcode和数组长度进行与运算h & (length-1),得到应放的数组下标
该数组位置无参数,则直接放
有参则遍历链表,key的hashcode相等且equals相同则覆盖,无相同则使用头插法,插到链表头部
插入前先判断,是否需要扩容,如果要,则先扩容,在放元素
注:先扩容,再插入。头插法。因为是头插法,多线程的情况下会导致死循环。头插法速度比尾插法快
3、get方法
1 2 3 4 5 6 7 | public V get(Object key) { if (key == null ) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } private V getForNullKey() {<br> if (size == 0 ) {<br> return null ;<br> }<br> for (Entry<K,V> e = table[ 0 ]; e != null ; e = e.next) { //数组下表为0的元素<br> if (e.key == null)<br> return e.value;<br> }<br> return null;<br>}final Entry<K,V> getEntry(Object key) {<br> if (size == 0) {<br> return null;<br> }<br><br> int hash = (key == null) ? 0 : hash(key);<br> for (Entry<K,V> e = table[indexFor(hash, table.length)];<br> e != null;<br> e = e.next) {<br> Object k;<br> if (e.hash == hash &&<br> ((k = e.key) == key || (key != null && key.equals(k))))<br> return e;<br> }<br> return null;<br>} |
先判断key是否为null,是则判断数组是否为空,空则返回null.非空则返回table[0]
key不是null,先判断数组是否为空,是则返回null.
不是则先计算key的hash值,在找到通过hash&(length-1)数组下标找到对应链表
遍历链表,hashcode相同并且keyequals相同时,找到元素,返回value
4、扩容
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = ( int )Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1 ); } |
每次扩容为原来的2倍
遍历数组和链表,重新计算元素的hashcode和位置,依次放入元素。
补充:扩容后的元素,要么在原位,要么在原位+原数组长度=新位置。1.7是每次都重新计算,1.8做了优化,直接计算高位是否为1,为1则原位+原数组长度=新位置,为0则在原位置
头插法,元素倒叙
5、hashMap的中table数组位置的计算方法
1 2 3 4 5 6 7 8 9 10 11 | final int hash(Object k) { int h = hashSeed; if ( 0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20 ) ^ (h >>> 12 ); return h ^ (h >>> 7 ) ^ (h >>> 4 ); } static int indexFor( int h, int length) {<br> // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";<br> return h & (length-1);<br>} |
问:为什么要进行^运算和>>运算呢?因为要让hashcode的高位数参数计算,减少hash碰撞
举例:length=16 length-1=15 : 0000 1111
h: :1111 0011
length-1:0000 1111
0000 0011 结果为3,放在数组【3】的位置
再为进行>>和^运算时,hashcode只有地位进行了运算,高位是什么不影响结果,因此要通过5次异或运算和4次位移运算使hashcode尽量分散,减少碰撞
6、hashMap的大小,始终为2的幂次方,无论带参设置多少,都取大于该值得最小2的幂次方
1 2 3 4 5 6 7 8 | private void inflateTable( int toSize) { <strong> // Find a power of 2 >= toSize</strong> int capacity = roundUpToPowerOf2(toSize); threshold = ( int ) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1 ); table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }<br> |
问:为什么要保证为2的幂次方?
因为hashcode&(length-1)=数组位置,只有当length为2的幂次方时,length-1才能是低位为都为1的二进制,进行与运算才不会数组越界。因为都为1111才有与运算的意义,如果都为0,则无论如何都为0,会加剧hashcode的碰撞
16:0001 0000
15:0000 1111
问:为什么扩容后的数组元素,要么在原位置,要么为原位置+原来length的位置?
length*2-1 实质上是二进制的高位从0变成1
15: 0000 1111
31:0001 1111
进行与运算时,要么是1,要么是0。是1,则原位置+原来length的位置。是0则位置不变
7、多线程下,扩容变成死循环的问题 参考:https://www.cnblogs.com/devilwind/p/8044291.html
根本原因:头插法,会使原有元素倒叙
线程1:扩容后,遍历元素前停止
线程2:扩容后,遍历元素,顺利完成
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)