HashMap源码学习笔记

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

HashMap 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 e : table) {
            while (null != e) {
                Map.Entry 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[] tab; HashMap.Node 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 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)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源码学习笔记)