hashmap源码剖析

JDK1.7和JDK1.8中hashmap的对比

JDK1.7中如果出现大量的key冲突之后,对长链表遍历找一个key-value对,性能是O(n),如果是直接根据array[index]获取到某个元素,性能是O(1)

JDK 1.8以后,优化了一下,如果一个链表的长度超过了8,就会自动将链表转换为红黑树,查找的性能,是O(logn),这个性能是比O(n)要高的

链表的遍历性能,时间复杂度是O(n),红黑树是O(logn),所以如果出现了大量的hash冲突以后,红黑树的性能比链表高得多,几倍到几十倍。

JDK 1.8以后,hashmap的数据结构是,数组 + 链表 + 红黑树

一些关键成员变量的分析

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

应该是数组的默认的初始大小,是16,这个跟ArrayList是不一样的,初始的默认大小是10

static final float DEFAULT_LOAD_FACTOR = 0.75f;

这个数组的大小,一般会自己手动指定一下,就跟你用ArrayList一样,你需要去预估一下你的这个数据结构里会放多少key-value对,指定的大一些,避免频繁的扩容。

这个参数,默认的负载因子,如果你在数组里的元素的个数达到了数组大小(16) * 负载因子(0.75f),默认是达到12个元素,就会进行数组的扩容

    static class Node<K,V> implements Map.Entry<K,V> {

        final int hash;

        final K key;

        V value;

        Node<K,V> next;

}

这是一个很关键的内部类,他其实是代表了一个key-value对,里面包含了key的hash值,key,value,还有就是可以有一个next的指针,指向下一个Node,也就是指向单向链表中的下一个节点

通过这个next指针,就可以形成一个链表

transient Node<K,V>[] table;

这个是什么东东?Node<K, V>[],这个数组就是所谓的map里的核心数据结构的数组,数组的元素就可以看到是Node类型的,天然就可以挂成一个链表,单向链表,Node里面只有一个next指针

transient int size;

这个size代表的是就是当前hashmap中有多少个key-value对,如果这个数量达到了指定大小 * 负载因子,那么就会进行数组的扩容

int threshold;

这个值,其实就是说capacity(就是默认的数组的大小),就是说capacity * loadFactory,就是threshold,如果size达到了threshold,那么就会进行数组的扩容。扩容又涉及到了rehash算法,下文会解释~!

final float loadFactor;,默认就是负载因子,默认的值是0.75f,你也可以自己指定,如果你指定的越大,一般就越是拖慢扩容的速度,一般不要修改

hash()

在hashmap里面有优化过后的hash算法,高性能的。

static final int hash(Object key) {

        int h;

        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

h = key.hashCode():这个就是直接获取了key的hash值,通过的是hashCode()方法

将他的高16位和低16位进行一个异或运算,就可以保证说,在hash值的低16位里面,可以同时保留他的高16位和低16位的特征,保证降低hash冲突的概率

put()

public V put(K key, V value) {

        return putVal(hash(key), key, value, false, true);

}

假设,hashmap是空的,数组大小就是默认的16,负载因子就是默认的12

 

        if ((tab = table) == null || (n = tab.length) == 0)

            n = (tab = resize()).length;

 

刚开始table数组是空的,所以会分配一个默认大小的一个数组,数组大小是16,负载因子是0.75,threshold是12

他的hash寻址的算法:(n - 1) & hash, 并不是说用hash值对数组大小取模,取模就可以将任意一个hash值定位到数组的一个index那儿去,取模的操作性能不是太高

位运算,性能很高,&与操作,来实现取模的效果

他这个hash寻址算法优化的前提是:他后面每次扩容,数组的大小就是2的n次方,只要保证数组的大小是2的n次方,就可以保证说,(n - 1) & hash,可以保证就是hash % 数组.length取模的一样的效果。

因为他不想用取模,取模的性能相对较低,这个是他的一个提升性能的优化点。

hash冲突问题:

(1)就是说,假设某两个key的hash值一样的,两个key不同,hash值一样, 这个概率其实很低很低,除非是什么呢?就是说你自己乱写了hashCode()方法,你自己人为的制造了两个不同的key,但是hash值一样

(2)两个key的hash值不一样,但是通过寻址算法,定位到了数组的同一个key上去,此时就会出现典型的hash冲突,默认情况下,会用单向链表来处理

if ((p = tab[i = (n - 1) & hash]) == null)

这个分支,他的意思是说tab[i],i就是hash定位到的数组index,tab[i]如果为空,也就是hash定位到的这个位置是空的,之前没有任何人在这里,此时直接是放一个Node在数组的这个位置即可

else

如果进入else,就说明通过hash定位到的数组位置,是已经有了Node了。

相同的key在进行value的覆盖。

if (p.hash == hash &&

                ((k = p.key) == key || (key != null && key.equals(k))))

                e = p;

如果人家的key是不一样的,hash值不一样,那么就进行链表/红黑树处理。

else if (p instanceof TreeNode):这个分支,如果这个位置已经是一颗红黑树的话,会红黑树处理。

else 

直到进入这个else分支,才是说,key不一样,出现了hash冲突,然后此时还不是红黑树的数据结构,还是链表的数据结构,在这里,就会通过链表来处理

                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

                            treeifyBin(tab, hash);

                        break;

这串东西的意思,就是说如果当前链表的长度(binCount),大于等于TREEIFY_THRESHOLD - 1的话,如果链表的长度大于等于8的话,链表的总长度达到8的话,那么此时就需要将这个链表转换为一个红黑树的数据结构

红黑树优化处理

如果说出现大量的hash冲突之后,假设某给位置挂的一个链表特别的长,就很恶心了,如果链表长度太长的话,会导致有一些get()操作的时间复杂度就是O(n),正常来说,table[i]数组索引直接定位的方式的话,O(1)。

JDK 1.8以后人家优化了这块东西,会判断,如果链表的长度达到8的时候,那么就会将链表转换为红黑树,如果用红黑树的话,get()操作,即使对一个很大的红黑树进行二叉查找,那么时间复杂度会变成O(logn),性能会比链表的O(n)得到大幅度的提升。

当链表的长度超过8的时候,链表就先是变成双向链表,然后是变成红黑树。

假设现在某个地方已经是一颗红黑树了。如果此时在那个地方再次出现一个hash冲突的话,怎么办呢?此时就应该是在红黑树里插入一个节点了。

 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

这行代码里面,会调用TreeNode.putTreeVal()方法,基于红黑树的规则,保持平衡的前提下,插入一个节点在红黑树里面。

扩容机制&&rehash()

hashmap底层是基于数组来实现的核心的数据结构,如果是用数组的话,就天然会有一个问题,就跟ArrayList一样,就是数组如果满了,就必须要扩容,hashmap所以也是有扩容的问题存在的

数组2倍扩容,重新寻址(rehash),hash & n - 1,判断二进制结果中是否多出一个bit的1,如果没多,那么就是原来的index,如果多了出来,那么就是index + oldCap,通过这个方式,就避免了rehash的时候,用每个hash对新数组.length取模,取模性能不高,位运算的性能比较高

JDK 1.8以后,为了提升rehash这个过程的性能,不是说简单的用key的hash值对新数组.length取模,取模给大家讲过,性能较低,而是用hash&length - 1进行寻址

JDK 1.8,扩容一定是2的倍数,从16到32到64到128。可以保证说,每次扩容之后,你的每个hash值要么是停留在原来的那个index的地方,要么是变成了原来的index(5) + oldCap(16) = 21

通过这个方式的话,可以有效的将原来冲突在一个位置的多个key,给分散到新数组的不同的位置去

if (++size > threshold)

       resize();

意思是说,每次你如果是put了一个新的key-value对之后,人家就会size++,每次都会比较一下size和threshold(数组的长度 * 负载因子),resize()方法就是在扩容

            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                     oldCap >= DEFAULT_INITIAL_CAPACITY)

                newThr = oldThr << 1; // double threshold

newCap = oldCap << 1,就是乘以2,新数组的大小是老数组的2倍

 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

新的数组是老数组的大小的两倍

                    if (e.next == null)

                        newTab[e.hash & (newCap - 1)] = e;

如果e.next是null的话,这个位置的元素不是链表,也不是红黑树

那么此时就是用e.hash & newCap(新数组的大小) - 1,进行与运算,直接定位到新数组的某个位置,然后直接就放在新数组里了

                   else if (e instanceof TreeNode)

                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

如果这个位置是一个红黑树的话,此时会调用split()方法,人家肯定会去里面遍历这颗红黑树,然后将里面每个节点都进行重新hash寻址,找到新数组的某个位置

else { // preserve order

进入这个else分支的话,证明是链表

                        if (loTail != null) {

                            loTail.next = null;

                            newTab[j] = loHead;

                        }

                        if (hiTail != null) {

                            hiTail.next = null;

                            newTab[j + oldCap] = hiHead;

                        }

遍历链表,然后将里面每个节点都进行重新hash寻址,找到新数组的某个位置

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值