1121

5. HashMap详解

简介

HashMap作为我们日常开发中最常用的一个Map类,值得我们把它研究的明明白白,本篇会对其等进行探索。

结构

先看下面几行源码

    transient Node<K,V>[] table;存放节点的数组,
    transient Set<Map.Entry<K,V>> entrySet;
    int threshold;//下次resize的阈值
    final float loadFactor;//负载因子,默认0.75
    static final int TREEIFY_THRESHOLD = 8; //链表转红黑树的阈值
    static final int UNTREEIFY_THRESHOLD = 6; //红黑树转链表的阈值
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);//hashcode为key、value值异或操作。
        }
    }
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { // 红黑树上的节点
        TreeNode<K,V> parent;  // red-black tree links 父节点
        TreeNode<K,V> left; // 左子节点
        TreeNode<K,V> right; // 右子节点
        TreeNode<K,V> prev;    // needed to unlink next upon deletion 前驱结点
        boolean red; // 是红色还是黑色
    }
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);对一个key求hash,为高16位和低16为异或,决定放到哪个位置
    }

构造方法

可以看下源码

    public HashMap() {//默认负载因子0.75
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    public HashMap(int initialCapacity) {//设置初始容量
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap(int initialCapacity, float loadFactor) {//手动设置初始容量,负载因子
      //....
    }

PUT方法

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true); // 首先,计算key的hash值是key的hashcode与右移16位后异或,计算存放位置
    }
    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) // 如果存放node的数组为null或者长度为0
            n = (tab = resize()).length; // resize
        if ((p = tab[i = (n - 1) & hash]) == null) // 根据容量最大索引值和hash取与,这样可以确定个位置,如果这个位置为null,直接newNode即可。
            tab[i] = newNode(hash, key, value, null);
        else { // 该位置上有值。
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) // 如果hash相等且(key是同一个或key的equals相等)
                e = p;
            else if (p instanceof TreeNode) // 如果该位置上是红黑树
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //调用红黑树添加值的方法
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) { //调用链表的下一个节点,如果是null,说明是尾部
                        p.next = newNode(hash, key, value, null); //在尾部追加新节点,把当前的值放进去
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 如果当前超过链转树的阈值
                            treeifyBin(tab, hash); // 转为红黑树
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key 如果存在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;
    }
    // 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;
        if (oldCap > 0) { // 如果旧的容量大于0
            if (oldCap >= MAXIMUM_CAPACITY) { // 如果旧的容量大于等于最大容量1 << 30
                threshold = Integer.MAX_VALUE; // 则把扩容阈值设置为int最大值
                return oldTab; //没法扩容了,返回老的数组
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY) // 把老容量左移一位即扩大一倍小于最大容量,且老容量大于初始容量大小16
                newThr = oldThr << 1; // double threshold 新的阈值也扩大一倍
        }
        else if (oldThr > 0) // initial capacity was placed in threshold 如果旧的容量为0,扩容阈值大于0
            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); // 新容量小于最大值且刚计算的值小于最大容量的话就按刚才算的值作为阈值,否则说明没扩容可能性了,扩容阈值为int最大值
        }
        threshold = newThr;
        @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) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    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);
                    else { // preserve order
                        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;
    }

GET操作

还是看源码讲

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value; //先拿node,从node上拿value,空就算了
    }
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node 
                ((k = first.key) == key || (key != null && key.equals(k)))) // 链表只有一个节点
                return first;
            if ((e = first.next) != null) { 
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 如果是红黑树,则用红黑树取的方法。
                do { // 不是红黑树,从前向后遍历链表,直到找到位置。
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

REMOVE方法

和前面类似,如果正好只有一个节点,直接删就是了,如果是链表,把前后处理好就行,如果是红黑树,用红黑树的理论去删除

线程不安全问题

1.7 体现在多个线程同时插入值时,如果需要resize,会产生循环链表 1.8 会有覆盖问题

与1.7的区别

  1. 扰动函数执行1次,1.7要多次 1次就够了
  2. 使用链表红黑树、1.7只用链表 防止链表过长,时间由O(n)降为O(logn)
  3. 链表插入由头插法改成了尾插法 扩容的时候头插法会使链表反转,多线程下产生环
  4. 插入时,1.7先扩容,后插入,1.8先插入,后扩容
  5. 扩容的时候为什么1.8 不用重新hash就可以直接定位原节点在新数据的位置,1.7需要重新hash 扩容2倍要不是原位置,要不高位多个1
posted @ 2020-07-31 15:56  凡夫俗子90  阅读(149)  评论(0)    收藏  举报