美文网首页
HashMap源码学习笔记

HashMap源码学习笔记

作者: 皮多堡 | 来源:发表于2019-08-10 14:25 被阅读0次

    Java中HashMap源码学习笔记。1.8 / 1.7 中设计思路比较

    HashMap<String, String> map = new HashMap<>();
            map.put("key", "value");
            String v = map.get("key");
            System.out.println(v);
    

    jdk1.7

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始桶容量
    static final float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子
    
    • 1.确定数组下标
      int hashCode = hash(key)
      int index = hashCode % table.length
    
    • 2.put hash冲突
    对于每一个新的元素,首先放到链表头部块 (效率最快)
        --> 导致由于是单向链表所以上面的节点找不到
        -->解决办法 讲新的链表赋值给head头部(相当于整个链表下移)
    
    • 3.优化
      对于2的整次幂的任意数X ---> hashCode % X == hashCode & (X -1)
    • 4.扩容
    扩容后的节点复制操作
        void transfer(Map.Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            for (Map.Entry<k, v> e : table) {
                while (null != e) {
                    Map.Entry<k, v> next = e.next();
                    //计算扩容后的下标
                    int i = indexFor(e.hash, newCapacity);
                    //将整个e节点指向扩容后新的数组对应链表头节点
                    e.next = newTable[i];
                    //将新的链表下移
                    newTable[i] = e;
                    //e重新指向原始链表的下一个节点(准备进行下一次移动操作)
                    e = next;
                }
            }
        }
        
    
    
    • 扩容的问题
    1.扩容后新链表的元素与之前链表元素位置颠倒
    2.多线程下扩容的并发死锁问题  是因为线程共享链表 并且扩容后链表倒置
    如何避免 
    - 1.使用CurrentHashMap 
    - 2.new HashMap(15,1) 指定已知容量和负载因子
    
    • 问题思考
    1. 为什么要找一个2的整次幂的数作为桶的容量
    为了计算下标效率更高 位运算效率更高
    
    1. 计算hashCode时为什么要进行右移以及异或操作
    例如:由于上面的思路中2的整次幂数-1 高位全部为0 低位全部为1
          15: 0000 1111
           &
           h: 0110 0111
           =  0000 0111
    只有低位参与了下标计算,hash碰撞的几率会会很大,导致hashMap链表高度过高,效率降低
    所以右移之后将高位移到低位 再进行异或运算,这样计算出来的下标值更均匀,hash碰撞几率更低
    
    1. HashMap中key可以为null吗
    可以只有一个 key为null直接put
    
    1. 计算扩容后数组的大小
    设传入大小为L
        计算: L -1 | L >>> 1  L >>> 2  L >>> 4  L >>> 8  L >>> 16
        原理: 最高1位 依次右移得到以低位全为1的树 即 2的n次方 -1
        最终结果 + 1
    

    jdk1.8

    数组 + 链表 + 红黑树

    • hash计算 通过hashCode()的高16位异或低16位实现 更加散列
        static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    
    • 两个变量
        // 当链表长度大于8时转为红黑树
        static final int TREEIFY_THRESHOLD = 8;
        //当红黑树高度小于6转为链表
        static final int UNTREEIFY_THRESHOLD = 6;
    

    1.8 往链表插值时直接插入到链表的尾部(区别与1.7)因为反正需要遍历,而这样做可以避免链表死锁

    • 关键代码分析
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
                //当前数组位置没有元素,直接放入
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
            //当前数组位置已有元素
                HashMap.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 HashMap.TreeNode)
                    e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    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
                                treeifyBin(tab, hash);
                            break;
                        }
                        //当前节点不为空key相同 更新操作
                        if (e.hash == hash &&
                                ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
            //判断当前长度是否大于阈值 进行扩容
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    

    1.8 扩容

         do {
                                next = e.next;
                                //为0不变为1则下标为结果加上oldLength
                                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;
                                }
                            }
    

    注意:
    在1.8中扩容是的算法有所变化,之前的 hash & newTable.length -1 改为 (e.hash & oldCap) == 0

    总结

    JDK源码中确实有许多代码设计精致的地方值得我们取挖掘学习. coding...

    相关文章

      网友评论

          本文标题:HashMap源码学习笔记

          本文链接:https://www.haomeiwen.com/subject/vprrjctx.html