HashMap源码
一、Hash表
拿hashMap举例,因为hashMap是hash表实现的,在jdk1.8之前,都是通过数组+链表来实现的hashmap,数组就是一个桶,存储元素和查找都要先找到桶的位置,就像是一个烤串的炉子,而桶的每个槽位需要通过key的hash来计算,再用hash值和数组长度-1做按位与运算,或者取模运算,就可以得到桶槽的位置,那么每个槽位存储的元素可以想象成不同数量的肉块,而位置相同的肉块是需要用签子将他们串起来,也就是图中看到的链表,是用来解决hash碰撞或者说hash冲突的
下面用表格总结一下hash的增删改查操作和原理
二、hash值怎么计算
2.1 为什么用hash计算
java中任何对象都有object.hashCode()
怎么计算hash
h ^ (h>>>7) ^ (h>>>4)
2.2 计算数组下标
index = h & (length-1)
等同于index = hash % n
三、如何添加键值对
hashmap,put源码解析,先看一下伪代码
put(K k,V v){
hash(k)
index= hash&(n-1)
}
一、计算key的hash,我们暂且忽略for循环中的逻辑,主要看addEntry方法,增加一个元素
二、addEntry方法中,首先根据key计算hash值,然后根据hash值传入indexFor方法,计算出数组下标bucketIndex,最后调用createEntry方法来创建元素
三、保存找到的bucketIndex位置的数组元素e,然后new Entry创建一个新的元素,next指针指向刚保存的e,此时相当于新增节点的next域指针指向了要插入位置的原来的节点
紧接着,看一下Entry的源码中,next还是一个Entry<K,V>,
截止到这里,我们就完成了链表中添加了一个节点,而且是在链表头节点添加的,速度很快,而key和hash算法的出现就是为了解决快速定位数组下标的问题,如果key相同,值不同,会采用链表的形式存储,也成为了hash冲突的解决方式之一:拉链法,下面我们理解一下什么是哈希冲突
四、hash碰撞|冲突
前边我们研究了通过Key的hash计算hash值,然后通过hash和数组长度按位与可以计算出数组的下标,那么如果不同的key计算出的下标相同,怎么办?往哪放?
jdk1.8之前hashmap源码都是采用数组+链表来实现,而链表就是为了解决hash冲突问题的,也成为拉链法。每当Put一个新节点的时候,不同的Key计算出的hash值相同,数组下标也相同,桶的位置确定了,这个时候新的节点就会插入到链表的头部,不仅将新的节点存储,而且效率达到最高,头插法的方式实现链表的元素新增
五、如何通过key找value
我们围绕以下这两个思考点来分析,问题便迎刃而解
1、怎么找数组位置?
2、怎么轮询找具体的元素?
问题1:看一下下面的伪代码,数组位置也就是数组下标,原理和put方法一模一样,都是通过hash(key),和hash&(n-1)来计算的
V get(Object key){
hash(key)
index= hash&(n-1)
}
问题2:我们来研究一下get方法,get(key)方法中会调用getEntry(key)
getEntry(key)方法中,重点看for循环中的逻辑,我们发现,查找元素的过程就是轮询链表元素的过程,indexFor很熟悉上文解释了它可以返回数组下标,table[数组下标]就是循环中访问的当前元素e节点,而if条件判断中的意思是如果该e节点的hash值和传入的key的hash值相同,并且key值也相同才会返回该e节点
六、为什么需要加载因子
6.1 为什么需要加载因子?
在极端的情况下,hash表中的数组下标如果都相同,那么,链表就是一个单链表,增删速度也最慢,为了减少冲突的发生,我们需要扩容,也就是扩大数组的长度来解决冲突
6.2 什么时候扩容呢?
这是就需要定义加载因子,源码默认是0.75,假如数组长度16,当16*0.75=12,也就是填充量>12的时候需要扩容
6.3 扩容会带来什么问题呢?
扩容也就是将数组的长度增加,下面举一个例子,来证明hash值改变,扩容以后get可能算出的位置有变化,所以,需要所有节点需要重新计算hash值,也就是rehash操作
如果hash=1
hash%15 = 1
扩容以后
hash%31 =1
但是如果hash =16
hash%15 = 1
扩容以后
hash%31=16
6.4 hashMap java8有什么改进
Hash冲突以后不再是采用链表来保存相同index节点,
相应的采用红黑树来保存冲突节点
节点查找优先级由O(n) -> 提高到O(logn)