HashMap源码分析

Posted milicool

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap源码分析相关的知识,希望对你有一定的参考价值。

一、要点

1. 如何减少哈希碰撞

  1. 将哈希桶长度设置为2的倍数,这样在计算下标时(n-1)& hash 的(n-1)二进制最后一位也会参与运算,

  2. 当Map中元素增加时,势必会造成碰撞的增加,这时候通过扩容来,来减少碰撞

2. 何时初始化HashMap

  在put值时,初始化hashMap

3. 哈希桶的寻址方法

  计算下标的算法 (n-1)& hash

4. 链表何时转红黑树

  当哈希桶中链表长度大于7时,则链表转红黑树,因为红黑树的查找效率更高

5. 扩容时扩大几倍

  扩大两倍,同时阈值也同样扩大两倍

6. 为什么hashMap容量都是2的倍数

  1. 计算下标的算法是 (n-1)& hash

  2. indexFor代码,正好解释了为什么HashMap的数组长度要取2的整次幂。因为这样(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问,

  3. 以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。

  技术图片

7. 讲一下Put的过程

  1. 哈希桶为空的话,调用扩容函数初始化哈希桶,默认长度16

  2. (n-1)& hash计算下标,不发生hash碰撞的话,直接赋值

  3. 发生hash碰撞,如果链头的key值就相同,直接替换,如果是红黑树就进入红黑树对比赋值

  4. 否则遍历链表,比较赋值(比较方式hash+equals)

  5. 最后判断是否需要扩容

8. 讲一下resize()过程

  1. 判断是否需要初始化哈希桶的容量值、阈值

  2. 遍历老数组,用hash值重新计算下标位置,要么将原来的链表放入低位、要么将要来的链表放入高位

9. 能否让HashMap同步

  Map m = Collections.synchronizeMap(hashMap);

10. HashMap的长度为什么设置为2的n次方

  1. 在寻址过中,一般用取余的方式来,这样的效率不高,当容量为2次方时,按位运算&上length-1时,效果和取余相同

  2. 方便扩容时移位操作,效率高,同时扩容后还是2的次方

二、源码

hash()方法

1. 扰动函数就是为了解决hash碰撞的

key的hash值 异或 hash值的低16位

static final int hash(Object key) 
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

put()方法

public V put(K key, V value) 
    return putVal(hash(key), key, value, false, true);


final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) 
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 判断数组是否为空
    if ((tab = table) == null || (n = tab.length) == 0)
        // 数组为空则初始化数组, 并获取长度
        n = (tab = resize()).length;
    // 判断数组中值是否为空,index是 哈希值 & 哈希桶长度-1, 代替模运算
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 数组中对应下标放入值
        tab[i] = newNode(hash, key, value, null);
    else 
        // 数组中对应下标不为空,哈希碰撞
        Node<K,V> e; K k;
        // 判断数组取到的第一个节点的key值是否和要存入的相同
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 则把原来的值替换掉
            e = p;
        else if (p instanceof TreeNode)
            // 如果p是红黑树, 则进入红黑树存值的流程
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else 
            // 否则p是链表, 且第一个节点key值与要存入的不相同, 则对单项链表进行遍历
            for (int binCount = 0; ; ++binCount) 
                // 遍历到节点尾部
                if ((e = p.next) == null) 
                    // 链表的下个节点为空时, 则放入要存的值
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 链表长度大于8,则将链表转成红黑树
                        treeifyBin(tab, hash);
                    break;
                

                //如果e不是null,说明有需要覆盖的节点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;

                // 遍历下一个节点
                p = e;
            
        

        // 判断是否找到了与待插入元素的hash值与key值都相同的元素
        if (e != null)  // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        
    
    // 记录修改次数
    ++modCount;
    //更新size,并判断是否需要扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;

resize()扩容

final Node<K,V>[] resize() 
    Node<K,V>[] oldTab = table;
    // 获取旧的数组的长度,和阈值
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 旧的容量大于0的情况
    if (oldCap > 0) 
        // 如果数组长度等于最大容量值,则不扩容直接返回
        if (oldCap >= MAXIMUM_CAPACITY) 
            // 设置阈值为2的31次方-1
            threshold = Integer.MAX_VALUE;
            // 不扩容了,直接返回旧的数组
            return oldTab;
        
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 如果容量变为2倍后小于最大容量,且大于等于默认容量16时
            // 阈值也扩大一倍
            newThr = oldThr << 1; // double threshold
    
    //如果当前表是空的,但是有阈值。代表是初始化时指定了容量、阈值的情况
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else                // zero initial threshold signifies using defaults
        // 当数组还未被初始化时,设置默认容量和阈值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    
    if (newThr == 0)  // 如果新的阈值是0,对应的是  当前表是空的,但是有阈值的情况
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
    
    threshold = newThr;
    // 扩容重新创建一个大小为2倍的数组
    @SuppressWarnings("rawtypes","unchecked")
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 更新哈希桶引用
    table = newTab;
    if (oldTab != null) 
        // 循环将老数组中的放入新数组
        for (int j = 0; j < oldCap; ++j) 
            // 当前节点e
            Node<K,V> e;
            if ((e = oldTab[j]) != null) 
                // 将原哈希桶置空,以便GC
                oldTab[j] = null;
                // 如果链表中只有一个元素
                if (e.next == null)
                    // 直接将这个元素放入新的哈希桶
                    // 注意这里重新计算了下标,这里相当于取模运算
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 如果发生了Hash碰撞, 节点小于8,要遍历节点,依次放入新的节点
                else  // preserve order
                    //因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位=  low位+原哈希桶容量
                     //低位链表的头结点、尾节点
                    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) 
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        
                        else 
                            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;
                    
                
            
        
    
    return newTab;

三、疑问点

1. 扩容时,hiTail = e表示什么意思

以上是关于HashMap源码分析的主要内容,如果未能解决你的问题,请参考以下文章

HashMap源码分析

HashMap源码分析 (3. 手撕源码) 学习笔记

[java源码解析]对HashMap源码的分析

HashMap源码分析

hashMap源码分析

HashMap源码分析--jdk1.8