Java.util——HashMap底层实现原理

Java.util——HashMap底层实现原理

首先介绍一下一般常见的处理哈希冲突的几种方式:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。
HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
除了Entry外,HashMap还包括以下几个重要字段:

//实际存储的key-value键值对的个数
transient int size;
//阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到
int threshold;
//负载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;
//用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException(foreach时)
transient int modCount;

此处对传入的初始容量进行校验,最大不能超过MAXIMUM_CAPACITY = 1<<30(2^30)。
在常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),而是在执行put操作的时候才真正构建table数组。
数组长度一定为2的次幂。
当发生哈希冲突并且size大于阈值(threshold=capacity*loadFactory)的时候,需要进行数组扩容(resize),扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去(遍历,重新计算索引位置,将老数组数据复制到新数组中去),扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。
如果负载因子取得太大,threshold与capacity太接近,当容量增大时,冲突会增加,造成同一地址链表过大;如果太小,哈希表太稀疏,浪费存储空间。负载因子可以大于1(即threshold大于数组长度,因为是链地址法)。
Put时如果key为null,存储位置为table[0]或table[0]的冲突链上(table为HashMap中存的数组),如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value,如果对应数据不存在,则添加到链表的头上(保证插入O(1))
put:首先判断key是否为null,若为null,则直接调用putForNullKey方法。若不为空则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置,如果table数组在该位置处有元素,循环遍历链表,比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。若table在该处没有元素,则直接保存。
最终存储位置的确定流程是这样的:
在这里插入图片描述
get方法的实现相对简单,key(hashcode-返回int)–>hash–>indexFor–>最终索引位置,找到对应位置table[i],再查看是否有链表,遍历链表,通过key的equals方法比对查找对应的记录。(&length-1也将范围较大的hash值缩小到了length内)
我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map。

int capacity = 1;  
    while (capacity < initialCapacity)  
        capacity <<= 1;  

这段代码保证初始化时HashMap的容量总是2的n次方,即底层数组的长度总是为2的n次方。
由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的)
在这里插入图片描述在这里插入图片描述
HashMap中得到数组下标是通过低位掩码(与n-1)(这样比取模速度快),但是这样高位信息就会缺失,而计算哈希值右移16位再异或,保留的高位信息,也减小了哈希冲突。
HashMap的存放自定义类时,需要实现自定义类的什么方法?
下图中的hash,都是用hashCode经过hash()函数的
在这里插入图片描述
重写hashcode()和equals()方法
如果不重写equals()方法,HashMap没有判断两个对象相等的标准。如果不重写hashcode(),将对用object默认的hashcode方法(根据对象地址生成hashcode),如果new了两个对象,它们的属性均相同,但由于是两个对象,所以object生成的hashcode不同,但在hashmap中这两个key应该当做相同的key,但不重写hashcode则无法实现。
Get和put方法通过key的equals方法比对查找对应的记录。要注意的是,有人觉得上面在定位到数组位置之后然后遍历链表的时候,e.hash == hash这个判断没必要,仅通过equals判断就可以。其实不然,试想一下,如果传入的key对象重写了equals方法却没有重写hashCode,而恰巧此对象定位到这个数组位置,如果仅仅用equals判断可能是相等的,但其hashCode和当前对象不一致,这种情况,根据Object的hashCode的约定,不能返回当前对象,而应该返回null(规定,相等的对象,hashcode必须相同)
HashMap中的final属性,不可变final int hash; final K key;
扩容resize()
如果size大于threshold(capacity*loadFactory)就进行扩容,原容量乘以2,再进行rehash的过程。如果capacity已经达到最大(2^30),则threshold变为Integer.MAX_VALUE(没有新建节点,只是新的指针)
多线程同时操作hashmap时会产生死循环。
如果多个线程同时扩容,产生两个新的table,形成一个闭环。
具体原因可参考下边两个网址:
http://ifeve.com/hashmap-infinite-loop/
http://www.cnblogs.com/alexlo/p/4955391.html

HashMap JDK1.8
Jdk1.8中没有indexFor函数,直接使用table[index = (n – 1) & hash](与运算交换左右,结果不变)
这里存在一个问题,即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(TREEIFY_THRESHOLD默认超过8、大于等于)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能(O(logn))。当长度小于(UNTREEIFY_THRESHOLD默认为6、小于等于),就会退化成链表。
下面我们讲解下JDK1.8做了哪些优化。经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
在这里插入图片描述

元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
在这里插入图片描述

因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。(每个节点e的hash早就计算好,并保存在final hash中)。通过if ((e.hash & oldCap) == 0)判定前面那个bit是不是1,如果是1则加上oldCap。
static class Node<K,V> implements Map.Entry<K,V> {},jdk1.8中用node替代了entry。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值