zoukankan      html  css  js  c++  java
  • HashMap 那点事

    HashMap

    一、默认参数

    // 默认初始容量16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    // 最大容量,容量必须是 2 的倍数,且小于最大容量。要是大于则取最大容量。
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认负载因子的值是 0.75,当负载因子是这个数的时候 hash 分布的更加均匀,泊松分布
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当大于此值的时候链表转化为红黑树
    static final int TREEIFY_THRESHOLD = 8;
    // 当小于此值的时候红黑树转化为链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 控制着转化红黑树的条件,只有节点总数大于此值的时候,并且满足单个链表长度大于8才会转化红黑树
    static final int MIN_TREEIFY_CAPACITY = 64;
    

    二、初始化那点事

    HashMap的初始化种类还是挺全的,有参的,无参的,半参的都有。如果使用无参的构造函数的话,存储数据的 table 不会被初始化,只有等到真正使用到的时候才会去初始化。

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    
    public HashMap() {
        // 这里只会将负载因子赋值,并没有去 tableSizeFor
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }
    

    tableSizeFor

    这个方法是将当前值 -1 后,从最左边开始不为零的位数开始向右填充 1,最后再 + 1 完成收尾工作。这番操作下来将会得到大于 cap 的最大的 2 幂次方(7 -> 8,8 -> 8,10 -> 16)。

    // 以 10 为栗子来说的话 cap = 0000 0000 0000 1010
    static final int tableSizeFor(int cap) {
        int n = cap - 1; // 0000 0000 0000 1001 9
        n |= n >>> 1; // 0000 0000 0000 1101 13
        n |= n >>> 2; // 0000 0000 0000 1111 15
        n |= n >>> 4; // 15
        n |= n >>> 8; // 15
        n |= n >>> 16; // 15
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; // 16
    }
    

    三、增(put是个什么玩法)

    会根据增加一个值的流程来逐步看方法

    1、put

    当调用 put 的方法的时候,会先 hash 得到要存放的位置,然后再进行逐步的操作

    // 会将旧值返回出来,如果没有旧值则返回 null
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    

    2、hash 怎么跟 hashCode 还不一样?

    将 key 的 hashCode 与其高 16 位的值做 ^ 操作,得到的值为真正的 hash 值,让高 16 位参与 hash 运算会减小 hash 冲突的概率。

    /**
     * 1001 ^ 0010 -> 1011
     * 9 ^ 2 -> 11
     **/
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

    3、putVal 好长啊,还要被 put 调用

    这个方法可就有点意思了,包含的知识点也比较多,首先从他的方法注解看起(翻译至方法上),至于步骤什么的都放在了方法上的注释上,这里就先总结一下 HashMap 的 put :

    1. 先做判空处理,如果为空就先扩容
    2. 根据 hash 找 table 的位置,如果找到的位置上是 nulll 的话就直接赋值
    3. 找到的位置有数据了,就循环遍历那个 链表/红黑树
    4. 如果没找到相同的 hash 与 key,则将最后一个节点的 next 指向新建的 Node,并且判断是否是大于转化成树的值,如果大于则开始使用 treeifyBin 方法尝试将链表转化为树结构,如果大小小于最小转化树结构的阈值,则进行一次扩容,而不是树结构的转化。
    5. 如果找到了的话直接结束循环
    6. 判断是找到相同值结束还是未找到值结束,如果是找到值结束则判断是否可以覆盖旧值,可以则覆盖掉,并且 return 旧值,不可以的话则直接 return 旧值
    7. 对 modCount +1,并且判断是否要扩容,需要则扩容
    8. 返回 null
    9. 流程图点击一下
    /**
      * Implements Map.put and related methods.
      *
      * @param hash 		key经过 hash 方法后的值
      * @param key  		key值
      * @param value 		将要被 put 的value值
      * @param onlyIfAbsent 如果为真,则对现有的值不进行改变
      * @param evict 		如果为false,则表处于创建模式。这里在 hashmap 中并未使用,在linked 才有用
      * @return previous value, or null if none
      */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 保存数据的 Node 数组
        Node<K,V>[] tab; 
        Node<K,V> p; 
        // 用来保存 table 的长度
        int n, i;
        // 疯狂的赋值加判断,如果 table 为空则先扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // n 是 2 的幂次方,所以 (n - 1) & hash 相当于 hash % n
        // 如果这里取得值是null的话,说明此空间还没有被占领,可以直接创建一个 Node 并且赋值给当前位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 取到的内容不为空的情况,e属于中间值
            Node<K,V> e; 
            K k;
            // 如果 hash 相同,并且 key 也相同的话,就直接将值覆盖掉
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                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) {
                        p.next = newNode(hash, key, value, null);
                        // 如果链表长度大于等于 8 的话,就开始尝试转化链表为红黑树
                        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;
                }
            }
            // 结束查找与操作,当查到 hash 与 key.hashCode() 相同的对象的时候执行以下操作
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // 判断是否可以覆盖,可以覆盖的话就直接去覆盖值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 用以判断是否 fast-fail 的标志位 +1
        ++modCount;
        // 如果增加新值后的大小大于 容量*负载因子 的话就进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

    4、resize 扩容怎么这么麻烦 ?

    很巧妙的点是在对旧值迁移到新集合中 ,如果要迁移数据的 hash 与原先集合的 最高位 最高位& 运算,如果为 0 则表示不需要迁移,为 1 的话也只是将位置直接计算出来 现在的位置 + 旧集合的大小 ,不用再执行 hash 操作。

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        // 这里都是按照 length 来进行操作的
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
           	// 最大也不能超过 Integer.MAX_VALUE 
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 新值等于旧值 << 1,并且与 1<<30 做对比,还要判断是否大于 16(初始大小是 16)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 可以的话扩容至旧值的 2 倍
                newThr = oldThr << 1;
        }
        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) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        // 如果旧的数据集不为 null 的话就开始数据的转移
        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;
                            // 这是一个很巧妙的点,将 e 的hash值与旧值做 & 操作,因为是 2倍扩容,所以如果是0的话就不需要移动table上的位置
                            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;
    }
    

    5、将链表尝试转化为红黑树的黑势力 treeifyBin

    为啥叫做尝试性转换为 树结构 呢,因为有 MIN_TREEIFY_CAPACITY 给限制住了,如果小于次参数也只能乖乖地扩容去了

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 判断逻辑
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
    

    至此添加的逻辑也就这样了,但是又有很大的坑没有被填补,红黑树是怎么进行添加操作的呢?等以后再来填吧。

    6、欣赏一下 Node 的容貌

    Node 里面保存了 hash ,key 与 value ,其中 hash 是 hashCode 的高位与低位算出来的,赋值后即变为不可更改的状态,与 key 一样。

    这里的 value 与 next 不为 final 是因为还需要覆盖与地址的重新指向。

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    
        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
    
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
    
        public final V setValue(V newValue) {
            ...
        }
    
        public final boolean equals(Object o) {
            ...
        }
    }
    

    7、其他 put 的风采

    对于 put 整个集合的话,都会用到以下的这个方法

    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    // 扩容至 t 的容量
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                // 遍历写入,这里不会再扩容,因为已经提前扩容过了
                putVal(hash(key), key, value, false, evict);
            }
        }
    }
    

    四、删(remove 是怎么用的?)

    从 remove 方法入手,看 remove 要执行哪些操作,调用哪些方法

    1、remove 顶层方法

    public V remove(Object key) {
        Node<K,V> e;
        // 还是先计算 hash 值,并且不会去判断 value 的值是否与被删除的值相同
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    

    2、removeNode

    这个方法相比于 putVal 方法可以说还算简单一些了,也就是先找到要移除的 Node 的位置,然后去移除掉,分三种情况移除而已

    1. 如果是树结构则直接走树的移除方法
    2. 如果是链表结构,但是是 Tab 结构上的第一个节点,则直接指向第一个节点的 next 节点
    3. 如果是链表上的节点,则执行链表移除节点的方式。

    只要没有真正移除数据,就不会去修改 modCount 的结构,也不会引起 fast-fail 的结果。

    /**
     * Implements Map.remove and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to match if matchValue, else ignored
     * @param matchValue 如果为 True 的话就会匹配 value的值,只有相同才会去删除
     * @param movable if false do not move other nodes while removing
     * @return the node, or null if none
     */
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            // 找符合条件的(hash & key)的 Node
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    // tree 的删除逻辑
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    // 如果是 tab 位的第一个 Node 则直接指向下一个Node(可能为 null)
                    tab[index] = node.next;
                else
                    // 链表方式移除数据
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
    

    五、改(replace)

    相比于增加的花里胡哨的操作,修改的操作确实是简单了很多,找到要修改的元素,如果找不到就返回 F,找到了之后将 Node 的 Value 替换成传入的 V 即可,如果调用带有旧值的方法的话,会有一个比较旧值的操作,相同才会去替换。

    @Override
    public boolean replace(K key, V oldValue, V newValue) {
        Node<K,V> e; V v;
        if ((e = getNode(hash(key), key)) != null &&
            ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
            e.value = newValue;
            afterNodeAccess(e);
            return true;
        }
        return false;
    }
    
    @Override
    public V replace(K key, V value) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) != null) {
            V oldValue = e.value;
            e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
        return null;
    }
    

    六、查(get*)

    1、get

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    

    2、getNode

    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;
    }
    
    /**
     * 树的查找方式
     **/
    final TreeNode<K,V> getTreeNode(int h, Object k) {
        return ((parent != null) ? root() : this).find(h, k, null);
    }
    
    final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
        TreeNode<K,V> p = this;
        do {
            int ph, dir; K pk;
            TreeNode<K,V> pl = p.left, pr = p.right, q;
            if ((ph = p.hash) > h)
                p = pl;
            else if (ph < h)
                p = pr;
            else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                return p;
            else if (pl == null)
                p = pr;
            else if (pr == null)
                p = pl;
            else if ((kc != null ||
                      (kc = comparableClassFor(k)) != null) &&
                     (dir = compareComparables(kc, k, pk)) != 0)
                p = (dir < 0) ? pl : pr;
            // 开始递归起来了
            else if ((q = pr.find(h, k, kc)) != null)
                return q;
            else
                p = pl;
        } while (p != null);
        return null;
    }
    

    3、getOrDefault

    这个方法还是能被提及一下的,特别是 map.getOrDefault(k, new ArrayList()) 的时候。先查一遍,为 null 就返回默认值。

    @Override
    public V getOrDefault(Object key, V defaultValue) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
    }
    

    七、链表与数据之间的转换

    我们都知道当 HashMap 的链表达到一定的条件的时候会转化为红黑树,但是这里的条件是属于哪种场景的,还有没有其他场景,存在转换关系?

    链表转化为红黑树就一种,resize 的时候判断节点数量与链表是否达到转化的阈值,而将红黑树转化为链表却又是另外的一套逻辑。移除节点与 resize 的 split 都会有可能将树转化为链表。

    final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                              boolean movable) {
    	...
        if (root == null
            || (movable
                && (root.right == null
                    || (rl = root.left) == null
                    || rl.left == null))) {
            tab[index] = first.untreeify(map);  // too small
            return;
        }
    	...
    }
    // 将红黑树转化为链表的方法
    final Node<K,V> untreeify(HashMap<K,V> map) {
        Node<K,V> hd = null, tl = null;
        for (Node<K,V> q = this; q != null; q = q.next) {
            Node<K,V> p = map.replacementNode(q, null);
            if (tl == null)
                hd = p;
            else
                tl.next = p;
            tl = p;
        }
        return hd;
    }
    

    上方的代码中表达了红黑树转化为链表的条件,当树的根节点root、root.left、root.right、root.left.left 中任意一个为 null 的时候都会触发转化为链表的逻辑。

    也就是说当节点有四个的时候,移除掉任何一个节点都会触发转化的逻辑,而当节点为 3 - 10 个之间,如果 remove 了这四个节点中的任意一个节点,同样会在下一次 remove 中将树转化为链表。

    八、总结

    总的来说要注意的点的数量放在整个 HashMap 方法中占比还是不算特别大的,需要重点看的还是他的 初始化的时候(二的幂次方)扩容的时候(达到阈值就要扩容了,new 一个Node数组,迁移数据)、树转化成链表的时候,整体还是不难的,也是很容易理解的,除了红黑树的添加那部分,查询反而是很简单了。

    看完代码也就知道了,不安全、尾插法、容量是2的幂次方这些东西是怎么来了的。

  • 相关阅读:
    F. The Treasure of The Segments
    D. Zigzags
    C. Binary String Reconstruction
    B. RPG Protagonist
    中国计量大学同步赛补题
    Teacher Ma专场补题
    2020ICPC上海站总结&补题
    华东202011月赛补题
    算法学习之旅——树状数组
    迷宫
  • 原文地址:https://www.cnblogs.com/lovestart/p/14146666.html
Copyright © 2011-2022 走看看