美文网首页
HashMap源码分析(JDK1.8)

HashMap源码分析(JDK1.8)

作者: 进击的波仔 | 来源:发表于2019-12-12 16:40 被阅读0次

    本文将针对jdk8中HashMap用到的几个知识做一个总结,主要涉及到源码及面试中相关的问题

    jdk版本 实现
    1.8之前 数组+链表
    1.8 数组+链表+红黑树

    成员变量及常量

    
        /**
         *初始化容量(必须是二的n次幂)
         */
        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;
    
        /**
         *放弃链表而使用红黑树的阈值,即链表转用红黑树的阈值(转红黑树条件之一 1/2)
         */
        static final int TREEIFY_THRESHOLD = 8;
    
        /**
         *放弃红黑树而使用链表,即链表的值小于6会由红黑树转回链表
         */
        static final int UNTREEIFY_THRESHOLD = 6;
    
        /**
         *当整个hashMap中元素数量大于64时,且链表的值超过8会转用红黑树(转红黑树条件之一 2/2)
         */
         static final int MIN_TREEIFY_CAPACITY = 64;
         
        /**
         *储存元素的数组(必须是二的幂)
         */
        transient Node<K,V>[] table;
    
        /**
         *存放缓存
         */
        transient Set<Map.Entry<K,V>> entrySet;
        
        /**
         *map中元素的数量
         */
        transient int size;
    
        /**
         *该map修改的次数
         */
        transient int modCount;
    
        /**
         *扩容的临界值
         */
        int threshold;
    
        /**
         *hash表的负载因子
         */
        final float loadFactor;
        
    

    初始化

    先来看看它的构造方法

    1. HashMap(int initialCapacity, float loadFactor);
      自定义初始容量及负载因子
        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);
        }
    
    1. HashMap(int initialCapacity);
      自定义初始容量,使用默认负载因子 0.75f
        public HashMap(int initialCapacity) {
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }
    
    1. HashMap();
      最常用的构造方法,默认负载因为0.75f,默认初始化容量 1<<<4
        public HashMap() {
            this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
        }
    
    1. HashMap(Map<? extends K, ? extends V> m); 接收一个Map类型的集合
        public HashMap(Map<? extends K, ? extends V> m) {
            this.loadFactor = DEFAULT_LOAD_FACTOR;
            putMapEntries(m, false);
        }
    

    原来想着重看看使用最多的第三个构造方法,貌似它给负载因子赋了个值然后啥都没干,算了,跳过

    重要内部类

    链表

    这个只有next,并没有prev,它是个单向链表

        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;
            }
        }
    
    红黑树

    红黑树的实现,内容有点多,有兴趣的同学可以去研究下,我们只要知道它查询效率高于链表,以及红黑树与链表转换条件就行

       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;
            TreeNode(int hash, K key, V val, Node<K,V> next) {
                super(hash, key, val, next);
            }
    
            /**
             * Returns root of tree containing this node.
             */
            final TreeNode<K,V> root() {
                for (TreeNode<K,V> r = this, p;;) {
                    if ((p = r.parent) == null)
                        return r;
                    r = p;
                }
            }
       }
    

    画图理解下:
    HashMap ↓

    HashMap内存结构

    我大致画了个图,里面有个hash桶(非要说它是个椭圆也行),到目前为止,可以大致了解HashMap的结构了,即:

    ==数组(transient Node<K,V>[] table)+链表(Node<K,V> )+红黑树(TreeNode<K,V>)==

    当然,在一个HashMap实例中,同一个桶(也可以说同一个数组下标中)不会既存在链表又存在红黑树结构,他们会在==链表长度变化时互相转换==。

    使用

    存放 PUT
    public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    

    这里它用到了hash(key)这个方法,我们先来看看hash做了什么事

    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    

    先给忘了java位运算的同学回忆下:

    1. ==^==:亦或运算,即==相同取0,不同则取1==

          3^4:
          0011
          0100
          ---------
          0111
          
          结果:7
      
    2. ==>>>==:无符号右移,即忽略符号右移

          注意:无符号右移:高位补0;     有符号右移:正数高位补0,负数高位补1
          
          3>>>2:
          0011
          0000
          ---------
          0000
          
          结果:0
          
          -3>>>2:
          11111111 11111111 11111111 11111101
          00111111 11111111 11111111 11111111
          -----------------------------------
          1073741823
      

    int是32位的,上面正数只写了4位是为了方便,不要纠结

    可以看出,hash的结果就是==key的hashCode与key无符号右移16位的结果进行亦或运算==,为什么这么做呢?这样可以提高hash的离散性,减少hash碰撞,从而提高效率

    扩容

       final Node<K,V>[] resize() {
            //旧表(旧数组)
            Node<K,V>[] oldTab = table;
            //旧数组长度
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            //旧的扩容临界值 默认是负载因子 0.75*初始长度16
            int oldThr = threshold;
            //定义新的数组长度,新的扩容阈值
            int newCap, newThr = 0;
            //如果旧数组初始化过
            if (oldCap > 0) {
                //如果旧数组长度已经不小于集合最大容量,则不能扩容
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                //如果新的数组扩容一倍后小于设定的最大容量,且旧数组长度不小于16,则扩容一倍,且新扩容阈值也增大一倍
                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;
            //旧数组、旧扩容阈值均为0,则取默认值(16,16*0.75)给新的数组
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            //如果新的扩容阈值为0,则用数组长度*负载因子(默认0.75f)
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            //给扩容阈值重新赋值
            threshold = newThr;
            //定义新的数组
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            //给成员变量table赋值
            table = newTab;
            if (oldTab != null) {
                //遍历旧数组
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    //如果下标为j的桶节点不为null,赋值给e
                    if ((e = oldTab[j]) != null) {
                        //设置旧的节点指向null
                        oldTab[j] = null;
                        //如果只存在一个节点直接计算该节点中的元素在新的table中的位置并赋值
                        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 { 
                            //扩容后,一个链表均匀分布到高区与低区
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                /**
                                *e.hash & oldCap 保证了50%的几率将所有元素放置到高、低区
                                *e.hash     xxxx xxxx xxxx xxxx
                                *oldCap     0000 0000 0100 0000 (由于它是2个幂,所以一定是0100这样的格式,即只存在一个1)
                                *结果       取决于e.hash本身与oldCap对应位置的数据为0还是1
                                /
                                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;
        }
    

    注意:链表数据扩容时,用(e.hash & oldCap) == 0来将旧的数组数据==平均地==分布在新的数组中,分==高区==与==低区==

    添加(put)

    我们先来看看它的方法

    public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    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 {
            //hash碰撞后的处理
                Node<K,V> e; K k;
                //如果之前存在与该元素key相同的元素,则替换之前的元素!!!暂时跳过,后面会替换(注意:这里要先判断hash码是否相等,所以两个元素equals时,放入同一个map不一定会被替换)
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                    //如果桶的数据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);
                            //如果此时节点长度达到了转红黑树的阈值(为什么是8-1=7?因为放入了当前这个元素,长度就达到了8),则进行红黑树的转换判断,是扩容?(MIN_TREEIFY_CAPACITY=64 记得这个参数吗?map长度如果小于64则扩容)还是转红黑树?(TREEIFY_THRESHOLD达到了8与map长度达到MIN_TREEIFY_CAPACITY 64)
                            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;
                    }
                }
                //遍历完后e还有值,说明遇到了需要替换的元素
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    //如果onlyIfAbsent为false或者旧值为null,则替换旧值,所以参数onlyIfAbsent的意思就是,当旧值存在时,遇到一样的元素,是否进行替换,默认为false,就是要替换
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                        //将新put的元素移动到最后一个节点,我感觉这里应该是用来更新元素的年龄,有点像jvm中老年代的感觉
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            //修改次数+1
            ++modCount;
            //扩容判断
            if (++size > threshold)
                resize();
                //根据传入参数判断是否移除该链表(红黑树)中最老的节点
            afterNodeInsertion(evict);
            return null;
        }
    

    大概就是这么回事儿,我觉得主要应该关注有一下几点

    1.当发生hash碰撞时它做了什么事?
    根据入参onlyIfAbsent(默认false)判断(如果旧值为null则直接替换)是否替换,如果为false则替换之前的元素
    2.为什么要将新加入的元素放置在链表尾端?(红黑树也继承于Node)
    我猜想应该是将新旧元素进行排序,最旧的在head,而最新的在tail

    获取(Get)

    public V get(Object key) {
            Node<K,V> e;
            return (e = getNode(hash(key), key)) == null ? null : e.value;
        }
    final Node<K,V> getNode(int hash, Object key) {
            Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
            //判断map是否为空
            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)

    public V remove(Object key) {
            Node<K,V> e;
            return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
        }
    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;
            //判断该key对应节点是否存在
            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;
                //直接定位到下标 (n - 1) & hash 的桶,并判断头部节点是否命中
                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);
                    }
                }
                //根据传入参数判定值 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);
                        //移除链表中的节点
                    else if (node == p)
                        tab[index] = node.next;
                    else
                        p.next = node.next;
                    ++modCount;
                    --size;
                    afterNodeRemoval(node);
                    return node;
                }
            }
            return null;
        }
    

    hash

    在hashMap中大量使用了hash

    1. 计算key的hash值时,==(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)==;
    2. 计算数据存放位置时,==e.hash & (newCap - 1)==

    相关问题

    1.为什么HashMap容量需要2的幂?

    在计算桶的位置时,会通过hash&(size-1)来确定该map在数组中的下标,这里设计size(HashMap)的容量,就是为了在运算时,保证size-1在做位运算时,永远以...1111结尾,即:
    
    
            hash:      xxxx xxxx 0101
            size-1:    0000 0000 1111
            结果:      0000 0000 0101 = 5
            
    HashMap就是通过这种方式来保证元素的均匀分布
    

    2.HashMap线程不安全主要是在哪个地方?

    HashMap的不安全主要体现在两个地方:
    ps:jdk1.7之前在多线程环境下,扩容的时候,链表可能会行成一个闭环,当我们在查询一个不存在的key调用get()时,遍历链表进入死循环,但是1.8已经不存在这个问题了,原因就是jdk1.7对链表元素进行了一次倒序处理,而jdk1.8中使用了正序处理。
       1、数据丢失
       2、size()不准确
    

    原因的话,可以参考大佬写的:https://blog.csdn.net/fumitzuki/article/details/82760236

    3.为什么重写equals时一定要重写hashCode?

    其实从我们了解了HashMap源码后,就能从这个角度来解释这个问题,打个比方,如果一个对象O重写equals但是并未重写hashCode,在存放到HashMap时及操作HashMap时均会出现一系列问题;
    put:由于未重写hashCode,在我们存入对象时,两个equals的对象hashCode可能是不等的,那么在通过 e.hash & (newCap - 1) 计算元素位置时,极有可能放置到不同的地方,即map中会存放两个我们认为是一样的对象;就算过了第一关,hash计算出来的位置相等,也会在插入同一个链表时,判断两个元素相等,HashMap中的逻辑是先判断hashCode是否相等,所以还是存放了两个我们认为一样的对象。
    get:取的时候,也会根据hashCode计算位置,所以我们取出来的对象很可能不是我们需要的那一个,当然了remove也是一样的道理
    

    作者能力有限,如果大家发现有不对的地方或者疑问,欢迎指正,大家一起进步!3q

    相关文章

      网友评论

          本文标题:HashMap源码分析(JDK1.8)

          本文链接:https://www.haomeiwen.com/subject/bxutnctx.html