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

Posted Neo_Gamer

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap工作原理回顾 (基于JDK1.8源码分析)相关的知识,希望对你有一定的参考价值。

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工作原理回顾 (基于JDK1.8源码分析)的主要内容,如果未能解决你的问题,请参考以下文章

JDK1.8版本HashMap源码原理分析

Map源码解析之HashMap源码分析

JDK1.8版本HashMap源码原理分析

JDK1.8版本HashMap源码原理分析

Java集合源码剖析——基于JDK1.8中ConcurrentHashMap的实现原理

Java集合源码剖析——基于JDK1.8中ConcurrentHashMap的实现原理