欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

HashMap工作原理回顾 (基于JDK1.8源码分析)

程序员文章站 2022-03-03 23:07:25
...

HashMap工作原理回顾 (基于JDK1.8源码分析)

空的HashMap()构造方法

    /**
     * 默认初始容量为16.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大容量为2的30次方,如果有参构造器指定了更大的数值,那么仍然以2的30次方为准
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认加载因子,用来在扩容的时候使用
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    /**
     * 空参构造器,使用初始容量16和默认加载因子0.75
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }

put(K key, V value) 方法

    /**
    * put方法源代码
    */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
  • 我们可以看到方法内部实际调用了一个putVal方法,hash(key)是对key取hash值。
    /**
    *  putVal源代码
    *  onlyIfAbsent参数:如果为true,则不改变已存在的value。
    *  evict参数:该参数在HashMap中并没有具体使用到,暂且忽略。
    */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;

        // 如果table为空或长度为0, 则初始化一个初始容量和默认阈值的Node数组
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        // 经过(n-1)&hash这样的逻辑与运算得到数组下标,在该数组下标位置的元素如果为空,则根据参数构造一个Node放入该下标位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

        // 如果数组下标位置的Node不为空    
        else {
            Node<K,V> e; K k;
            
            // 如果下标位置的节点的hash值与给定的hash值相同且key相同,那么为同一个Node,替换之。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            
            // 如果Node为树节点的处理
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            
            // 如果下标位置的节点hash值以及key值对比不一样(其中包括hash值相同而key值不同的情况,也就是我们常遇到的hash冲突,应如下处理)
            else {
                for (int binCount = 0; ; ++binCount) {

                    // 如果下标位置的节点next节点为空,则指定该下标节点的next节点为新节点,跳出循环
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

                    // 如果下标位置的节点next节点不为空且hash值及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;
        // 如果size超过阈值(如16*0.75=12),则调用resize方法进行扩容且以2的N次幂进行扩容。扩容过程中会对数组内节点的位置进行重新计算从而进行变动。
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

get(K key) 方法

    /**
    * 方法源码
    */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
  • 我们可以看到getNode方法,我们进入此方法看看其具体实现。
    /**
    * 此处的hash值与put方法中的hash值计算算法是一样的,有兴趣可以自己看下源码
    */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

        // 如果节点数组table不为空且长度大于0,经过(n-1)&hash这样的逻辑与运算得到下标后,在节点数组table中该下标位置的节点如果不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            
            // 如果该下标位置的节点hash值与参数hash值相等且key相同,则返回该节点
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            
            // 如果该下标位置的节点hash值与参数hash值不相等或者key不相同,且该节点的next节点不为空
            if ((e = first.next) != null) {

                // 处理如果是树节点的情况
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                
                // 不断循环查找next节点,直到找到匹配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(K key) 方法

    /**
    * 方法源码
    */
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
  • 同样的,我们可以看到一个removeNode方法,我们再看看这个方法的具体实现。
    /**
    * 此处的hash也是和put方法中的hash值运算算法相同得到的,
    * matchValue指是否需要匹配value值,movable指是否需要移除。
    */
    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;

        // 如果Node数组table不为空且长度大于0,经过(n-1)&hash这样逻辑与运算得到数组下标后,在数组该下标位置的Node不为空
        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值与指定hash值相等且key相同,则匹配
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            
            // 若下标位置的节点hash值与指定hash值不相等或key不相同
            else if ((e = p.next) != null) {
                
                // 处理树节点的情况
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                
                // 循环查找next节点直到找到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);
                }
            }

            // 如果匹配到hash值以及key值都相同的节点,且不需要匹配value或者value匹配也相等
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                // 处理树节点情况
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                
                // 如果node节点与数组下标位置节点是同一个节点,那么将下标位置指向node的next节点,这样下标位置的原节点就不存在了。
                else if (node == p)
                    tab[index] = node.next;

                // 如果node节点与下标位置节点不是同一个节点,而是其链表中的其它节点(循环匹配到的其中一个next节点),那么就循环后p节点(看循环代码处实际是原来的e节点)的next节点指向node节点(看循环代码处实际是新的e节点)的next节点。
                else
                    p.next = node.next;
                
                // 修改变动变量加一,容量减一。
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

至此,HashMap的三个常用的基本方法源码分析就介绍到这里,欢迎各位同僚提出批评与建议,转载请说明出处,谢谢!

相关标签: HashMap