hashmap知识点

参考自:https://blog.csdn.net/LE_912/article/details/80599869

https://www.cnblogs.com/txfsheng/p/9140614.html

首先谈一下hashmap的数据结构

    

hashmap底层使用数组加链表的数据结构,每一个数组空间都会存储一个链表结构,每个链表节点都是一个node对象,里面包含存储的hash,key,value,next(下一个节点node的值),那么问题来了,如果执行一个map.put操作的时候,整个流程是什么样子的呢?

1.首选对key值进行hash算法,key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位是不变的。) 

[java]  view plain  copy
  1. public V put(K key, V value) {  
  2.    return putVal(hash(key), key, value, falsetrue);  
  3. }  
[java]  view plain  copy
  1. static final int hash(Object key) {  
  2.    int h;  
  3.    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  
  4. }  
2.然后执行putval方法,首先判断table是不是为空,如果为空,说明是第一次执行put操作,需要确认数组的大小,从源码中可以看到,数组的初始大小是16,最大值为Integer.max_value。数组的大小必须是2的n次方。然后需要确认数据落在数组的哪一个位置上,也就是确认数组下标,确认数组下标是通过hash&(数组大小-1)相当于取余数的方式,如果数组下标位置没有存在其他节点,那么直接放入数组桶中(如图中的node0、node2),如果发生了碰撞(数组下标位置存在其他节点),如果key已经存在,说明节点已经存在,将对应节点的旧value换成新value,如果不存在将node链接到链表的后面(如图中的node1和node3)。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 数组初始大小

3.hashmap如果数据变大,数组是可以扩充的,常量定义了一个负载因子,默认是0.75,也就是说当数组开始有大于16*0.75=12个桶有数据的时候,数组就开始扩充,扩充的大小是原来的两倍,因为要保证数组大小是2的n次方。如果链表过长会影响查询速度,jdk1.8对此做了改进,有一个常量TREEIFY_THRESHOLD=8和UNTREEIFY_THRESHOLD=6,如果链表的长度大于TREEIFY_THRESHOLD=8时,链表会转换成红黑树。如果执行remove操作的时候,红黑树节点又会变少,如果节点小于UNTREEIFY_THRESHOLD=6时,又会从红黑树转成链表。

static final float DEFAULT_LOAD_FACTOR = 0.75f; // 负载因子

以上就是hashmap执行一次put操作的历程,此时肯定会有一些疑问:

(1)数组大小为什么是2的n次方?

数组的下标是用hash值&(数组大小-1)计算的,2的n次方的转换为二进制最高位是1,后面都是0,例如2的4次方是16,二进制表示为10000,16-1=15(二进制表示1111),如果不是2的n次方,比如数组大小为20。20-1=19(10011),这样于hashcode与运算,第2.3位为0,这样得到的数组下标只由第1.4.5位决定,大大减小了数组下标的利用率。

(2)hash算法为什么要高16位不变,低16位与高16位异或作为key的最终hash值?

因为数组的大小是2的n次方并且初始大小为16,假设目前数组大小为16,如果不进行这样运算的话,直接进行hash值与16-1=15的二进制与运算的话,其实只有hash值的后四位参与了运算,这样发生碰撞的概率会提高,而且高16位只有在数组很大的时候才能参与运算,所以用高16位和低16位进行运算,让高位也参与运算,在数组很小的时候增加运算可能性,减少碰撞。

(3)为什么负载因子是0.75?

如果负载因子是0.5的话:有一半的数组空间会被浪费,随着数组的增大,浪费的越多。

如果负载因子是1的话:数组扩充是为了减少hash碰撞的次数,如果是1,需要每个数组下标都有值才会扩充,如果有一个数组下标迟迟没有值,很有可能其他下标的链表已经非常长了,已经经历过很多次的碰撞,也经历过很多次链表查找并比较的操作,大大影响性能

(4)为什么使用红黑树不使用平衡二叉树?

平衡二叉树追求绝对平衡,条件比较苛刻,左右两个子树的高度差的绝对值不超过1,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
红黑树放弃了追求完全平衡,追求大致平衡,保证每次只需要三次旋转就能达到平衡,实现起来简单。
(5)数组扩充后,原来的节点放在新数组的哪里?
上面提到,数组扩充后大小是原来的两倍。扩充后会对每个节点进行重hash,从下图源码可以看到这个值只有可能存在两个地方,一个是原下标位置,另一个是原下标+原数组大小的位置

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
    next = e.next;
    if ((e.hash & oldCap) == 0) { //节点hash值与原数组大小与运算,等于0后面操作把节点放在原下标位置
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    else {       //节点hash值与原数组大小与运算,不等于0后面操作把节点放在原下标+数组大小位置
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);
if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;          //将节点放入原下标位置
}

if (hiTail != null) { hiTail.next null; newTab[j + oldCap] = hiHead//将节点放入原下标+原数组大小位置

(6)HashMap进行put时放入头部还是尾部?

在jdk1.8之前是插入头部的,在jdk1.8中是插入尾部的。

另外,

jdk1.8中Entry不见了

在jdk1.6中,HashMap中有个内置Entry类,它实现了Map.Entry接口;而在jdk1.8中,这个Entry类不见了,变成了Node类,也实现了Map.Entry接口,与jdk1.6中的Entry是等价的。

猜你喜欢

转载自blog.csdn.net/liwenxia626/article/details/80789693
今日推荐