HashMap源码全解析从一道面试题说起:请一行一行代码描述下hashmap put方法

栏目: 数据库 · 发布时间: 6年前

内容简介:本文原创地址,jdk1.8相对于jdk1.7有较大改动,本次将只会详细分析jdk1.8的代码,对于1.7只会比较两者不同之处。

HashMap源码全解析从一道面试题说起:请一行一行代码描述下hashmap put方法

本文原创地址, 我的博客https://jsbintask.cn/2019/02/27/jdk/jdk8-hashmap-sourcecode/ (食用效果最佳),转载请注明出处!

前言

前阵子(估计也快半年了吧)遇到这么一个面试题:请一行代码一行代码描述下 HashMap put 方法。

我:。。。

哈哈,其实也没有无语,当时知道 HashMap 的原理,数据结构,以及一些要注意的点,没想到面试官这么狠,所以本文的目的就是全方位的从源码角度分析下 HashMap

注意点

jdk1.8相对于jdk1.7有较大改动,本次将只会详细分析jdk1.8的代码,对于1.7只会比较两者不同之处。

源码

数据结构

jdk1.8以前, HashMap 使用的是 数组+链表 的结构存储数据。如下:

HashMap源码全解析从一道面试题说起:请一行一行代码描述下hashmap put方法

维护一个 Enyry[] 数组存储数据,当发生 hash 冲突时,数组节点则会变成一个链表,用于存储 hash冲突 的数据,而在 jdk1.8 中,这个链表则变成了 红黑树 ,如下:

HashMap源码全解析从一道面试题说起:请一行一行代码描述下hashmap put方法

值得注意的是,jdk8中不是一开始就使用 红黑树 维护这些hash冲突的节点,而是当链表长度超过某个 阈值 时才将 链表转换为红黑树 ,在代码分析中看到。

源码分析

  1. 首先查看 HashMap 中的静态成员常量
    public class HashMap {
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
        static final int MAXIMUM_CAPACITY = 1 << 30;
    
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
        static final int TREEIFY_THRESHOLD = 8;
    
        static final int UNTREEIFY_THRESHOLD = 6;
    
        static final int MIN_TREEIFY_CAPACITY = 64;
    }
    

各个静态常量如图:

HashMap源码全解析从一道面试题说起:请一行一行代码描述下hashmap put方法

各个静态成员常量意义已经注明.

  1. HashMap 成员变量,接着,我们看下成员变量:
    public class HashMap<K, V> {
    
        /**
         * 一开分析的 `储存数据的数组`
         */
        transient java.util.HashMap.Node<K,V>[] table;
    
        /**
         * 用于 **entrySet()和values()**方法,返回一个迭代器遍历Map结构
         */
        transient Set<Map.Entry<K,V>> entrySet;
    
        /**
         * 整个hashmap 所包含的节点数
         */
        transient int size;
    
        /**
         * hashmap 的结构修改次数,比如 Put,remove的次数,
         * 和上面的 迭代器配合使用,在迭代过程中,如果其它线程更改了这个值,则会抛出 `ConcurrentModificationException`异常
         */
        transient int modCount;
    
        /**
         * hashmap扩容的阈值,值为 loadFactor*table.length  e.g: 0.75 * 16 = 12
         * 也就是说默认是当数组大小超过 12时就会进行数组扩容
         */
        int threshold;
    
        /**
         * 加载因子,默认值上图已经说明
         */
        final float loadFactor;
    }
    

其中各个值的含义已经在注释中说明,配合上图默认值更能理解。

  1. 接着我们继续看下 Node 类的数据结构:
    static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            Node<K,V> next;
    }
    

很清楚,四个成员变量, key , key的hash值 , key对应的value , 下一个节点的引用 ,其中链表的形成就是 next 这个引用的作用。

  1. 好了,准备条件都做好了,接下来就是分析 put 方法了:
    public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
    }
    

很清楚,通过 hash(key) 方法获取到了key的hash值,然后调用了 putVal() 方法:

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)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            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))))
                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);
                        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
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

这是 putVal 的原始方法,看起来有点复杂,很多操作在一行代码中写完,我们稍微改下写法,为每行代码加上注释:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        /* 声明本地变量 tab,p,n,i(提高性能,effective java),可以先多记两边,防止后面不知道变量怎么来的! */
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, i;

        /* 将成员变量 table 赋值给本地变量 tab,并且将tab的长度赋值给本地变量 n */
        tab = table;
        if (tab != null) {
            n = tab.length;
        }

        /* 如果tab为空或者 数组长度为0,进行初始化,调用 resize()方法,并且获取赋值后的数组长度 */
        if (tab == null || n = 0) {
            tab = resize();
            n = tab.length;
        }

        /* 根据key的hash值得到当前key在数组中的 位置,赋值给 i */
        i = (n - 1) & hash;
        /* 将i在数组中对应的key值去除赋值给p,所以p代表当前的key */
        p = tab[i];

        /* 判断当前数组中取出来的key是否为空(数组中没有),就new一个新的节点,并且放在这个索引 i的位置 */
        if (p == null) {
            tab[i] = newNode(hash, key, value, null);

            /* 如果不为空,那就表示已经有这样的hash 值已经存在了,可能存在hash冲突 或者 直接替换原来的value */    
        } else {
            /* 声明本地变量 e, k */
            Node<K, V> e;
            K k;

            /* 如果取出来的节点 hash值相等,key也和原来的一样( == 或者 equals方法为true),直接将 这个节点 
            * p 赋值给刚刚声明的本地变量 e (这个操作很重要,在心中记住)
            * 另外这里还将 节点 p的key 赋值给了本地变量 k
            * */
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
                e = p;
                
                /* 如果 hash值一样,但不是同一个 key,则表示hash冲突,接着判断这个节点是不是 红黑树的节点
                 * 如果是,则生成一个红黑树的节点然后赋值给本地变量 e */
            } else if (p instanceof TreeNode) {
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);

                /* 不是红黑树,hash冲突了,这个时候开始扩展链表 */
            } else {
                /* 声明一个本地变量 binCount,开始遍历 p节点后面的链表 */
                for (int binCount = 0; ; ++binCount) {
                    /* 首先将p节点的 next(链表的下一个)赋值给 本地变量e */
                    e = p.next;
                    
                    /* 如果e为空,表示p指向的下一个节点不存在,这个时候直接将 新的 key,value放在链表的最末端 */
                    if (e == null) {
                        p.next = newNode(hash, key, value, null);

                        /* 放入后,还要判断下 这个链表的长度是否已经大于等于红黑树的阈值 (前面分析静态成员变量已经说明), 
                        *  一旦大于,就可以变形,调用 treeifyBin方法将原来的链表转化为红黑树 !
                        * */
                        if (binCount >= TREEIFY_THRESHOLD - 1) { // -1 for 1st
                            treeifyBin(tab, hash);
                        }

                        break;

                    }

                    /* 如果不为空,表示还没有到链表的末端, 
                    将 e 赋值给 p(p的下一个节点赋值给p),开启下一次循环 */
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                        break;
                    }
                    p = e;
                }
            }
            
            /* e不等于null,则表示 key值相等,替换原来的value即可,
             * 这里需要注意,这里不是表示 hash冲突(再观察下前面的分析),
             * hash冲突链表的扩展已经在最后一个 else完成了!
             * */
            if (e != null) { // existing mapping for key
                V oldValue = e.value;

                if (!onlyIfAbsent || oldValue == null) {
                    e.value = value;
                }

                /* 替换新值后,回调该方法(子类可扩展) */
                afterNodeAccess(e);

                /* 返回原来的 key对应的旧值 */
                return oldValue;
            }
        }

        /* 完成一次 put方法后,加一次 modCount,看前面成员变量分析 */
        ++modCount;

        /* 加入了新节点,把 size 自加,并且 判断是否已经大于要扩容的阈值(观察前面成员变量分析),开始扩容 */
        if (++size > threshold)
            resize();
        
        /* 插入新节点后,回调方法(子类可扩展) */
        afterNodeInsertion(evict);
        
        /* 插入的新节点,直接返回 null即可 */
        return null;
    }

其中所有代码均已经加上详细注释,这里值得注意的是,由于这个方法没有任何 线程同步手段,所以不论是在查找对应的key,还是扩容,插入节点,增加size,modCount等,肯定会出现问题( 这里先预留一篇文章,ConCurrentHashMap源码分析 ),所以多线程环境下,绝对不能使用 HashMap

而应该使用 ConCurrentHashMap

当然到了现在,我们那个面试题的答案也已经能够较为完整的回答出来了!(大笑)

  1. 上面较为详细的分析了put方法后,我们注意到 resize() 方法在这个方法中起到了关键作用,初始化,以及扩容。那我们接着来观察下 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) {
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                        oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            } 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;
            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;
        }
    

这个方法看起来也较为复杂,我们同样作下简单分析:

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) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
                /* 将原来的数组长度 * 2 判断是否小于最大值,并且原来的数组长度大于 默认初始长度(16)
                * 直接双倍扩容, 阈值,长度都 * 2
                * */
            } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold

        } else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;

        else {               // zero initial threshold signifies using defaults
            /* 第一次调用 resize方法,初始化数组长度,阈值,这里就对应我们前面成员变量的分析了:
             * 阈值 = 加载因子 * 数组长度
            * */
            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;

        /* 开始将旧数组的长度复制到新数组 */
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K, V> e;
                if ((e = oldTab[j]) != null) {
                    /* 原数组的值先置换为null,帮助gc */
                    oldTab[j] = null;

                    /* 如果节点的next不为空(没有形成链表),直接复制到新数组 */
                    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;
    }

注释已经简要说明流程,这里可以看出有数组复制以及重新计算hash的操作, 所以我们在实际开发中使用HashMap的时候,最好设置一个初始容量,防止经常扩容操作耗费性能!

  1. 好了, HashMap 两个关键方法都分析完毕了,接下来我们最后分析一个方法, get(key) :
    public V get(Object key) {
            Node<K, V> e;
            return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    

get方法首先通过 hash(key) 方法获取到了hash值,接着通过 getNode(hash) 方法获取节点,所以我们重点看下 getNode 方法:

final Node<K, V> getNode(int hash, Object key) {
        /* 声明本地变量,提高性能 */
        Node<K, V>[] tab;
        Node<K, V> first, e;
        int n;
        K k;

        /* 本地变量赋值,n为数组长度 */
        tab = table;
        if (tab != null) {
            n = tab.length;
        }
        /* 通过 hash值算出key在数组中的 位置,取出该节点 */
        first = tab[n - 1] & hash;

        /* 不为空,表示key在数组中存在,接下来开始遍历链表获取红黑树,找出具体位置 */
        if (tab != null && n > 0 && first != null) {

            /* 如果链表或者红黑树的第一个节点 hash值,key相等,这个节点就是我们要找的,直接返回 */
            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);
            }
        }

        /* 最后没有找到,直接返回null */
        return null;
    }

所有代码均已经加上详细注释,这里值得注意的是, 我们发现在链表中查找节点采用的是遍历的方式,所以一旦链表过长,查找性能就较慢,这也是为什么jdk1.8会在 链表长度超过阈值的时候将链表转换为红黑树的原因!(链表时间复杂度为O(n),红黑树为 O(logn).

总结

相信到了现在,HashMap的各类问题各位应该都能够明白了,我们通过阅读源码的方式较为详细的分析了 HashMap (jdk1.8)中的关键方法(put,get,resize),明白了 HashMap 中的每一个成员变量,静态常量的含义,

另外我们还通过源码知道了多线程环境下HashMap会出现的问题,引申出了 ConCurrentHashMap 的解析,下一章,我们同样将通过源码解析 ConCurrentHashMap

关注我,这里只有干货!

谢谢你支持我分享知识

HashMap源码全解析从一道面试题说起:请一行一行代码描述下hashmap put方法

扫码打赏,心意已收

HashMap源码全解析从一道面试题说起:请一行一行代码描述下hashmap put方法

打开 微信 扫一扫,即可进行扫码打赏哦


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Art of Computer Programming, Volume 3

The Art of Computer Programming, Volume 3

Donald E. Knuth / Addison-Wesley Professional / 1998-05-04 / USD 74.99

Finally, after a wait of more than thirty-five years, the first part of Volume 4 is at last ready for publication. Check out the boxed set that brings together Volumes 1 - 4A in one elegant case, and ......一起来看看 《The Art of Computer Programming, Volume 3》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具