HashMap源码解析

2021-03-13  本文已影响0人  如愿以偿丶

1.简介

  HashMap它是基于哈希表的Map接口的实现,是以key-value的像是存在,主要存放的是键值对。他不是线程安全的。

  &(按位与运算):如果相同二进制数位上,数字都是1的时候为1,否则为0
  ^ (按位异或运算):如果相同二进制数位上,数字相同,结果为0,不同为1
  | (按位或运算):如果相同二进制数位上,数字都是1的时候为1,否则为0

2.HashMap数据结构(数据存储方式)

  HashMap它内部是以数组 + 链表 + 红黑树三种数据结构进行存储。
  ArrayList它内部是以数组进行存储
  LinkList它内部是以双向链表进行存储。

  2.1.HashMap的存储流程:

    HashMap首先是以数组进行存储,通过存储元素的key的hashCode值进行hash计算。获取存储的索引值,如果数组索引位置没有元素,那么直接进行存储,如果数组索引值位置已经有元素,那么就会通过进行对比两元素的hashCode值,如果hashCode值不相等,那么就会在数组索引值上创建链接,进行存储,如果hashCode值相等,那么会通过equals方法获取内容信息进行对比,如果不相等,存储在链表上,如果相等,那么就会进行覆盖,并赋予新的value值。

image.png

3.HashMap集合类成员

    /**
     * 默认的初始容量-必须是2的n次幂。如果不指定默认为16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大的容量必须是2的幂<= 1<<30,也就是2^30次幂
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认的加载因子0.75(用于扩容计算)
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    /**
     * Because TreeNodes are about twice the size of regular nodes, we
     * use them only when bins contain enough nodes to warrant use
     * (see TREEIFY_THRESHOLD). And when they become too small (due to
     * removal or resizing) they are converted back to plain bins.  In
     * usages with well-distributed user hashCodes, tree bins are
     * rarely used.  Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     * 翻译:因为树节点的大小是普通节点的两倍,我们仅当垃圾箱(数组角标)包含足够的节点时才使用
     * (见TREEIFY_THRESHOLD)。当它们变得太小(由于移除或调整大小)它们被转换回普通箱子。在
     * 使用分布良好的用户hashCodes, tree bins很少使用。在理想情况下,在随机hashCodes下,
     * 频率为bin中的节点遵循泊松分布
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006    能达到8个节点的概率
     *
     * 红黑树的最大阈值,当链表节点数超过8时,并且数组长度要大于等于64,就会转为红黑树,他是根据泊松分布图
     * 由文档可知,转为红黑树的概率极低,其实设计者也不想让转为红黑树,毕竟树节点占用内存的大小是普通节点的两倍
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 当红黑树长度小于6时,重新转为链表
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 当节点数大于8并且数组长度大于等于64,会转为红黑树
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
    
    /**
     * Hashmap中存储键值对的个数,不是table的长度
     */
    transient int size;

    /**
     * 用来记录Hashmap的修改次数,每次扩容和修改map结构的计数器
     */
    transient int modCount;


    /**
     * 用来扩容Hashmap容量大小下一个容量。临界值计算方式 =  map容量 *  负载因子
     * 也就是说当容量达到临界值,就会进行扩容
     */
    int threshold;

    /**
     * 负载因子
     */
    final float loadFactor;

4.HashMap构造方法分析:


    public HashMap() {
        // 设置负载因子,默认为0.75
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }
    
    //指定容量大小
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    
    public HashMap(int initialCapacity, float loadFactor) {
        //判断初始化容量是否小于0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //判断初始化容量是否大于最大容量2^30次幂
        if (initialCapacity > MAXIMUM_CAPACITY)
            //超过最大容量,就将容量设置为最大容量
            initialCapacity = MAXIMUM_CAPACITY;
        //判断负载因子是否小于0 或者 负载因子是否是一个非数值
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        //指定负载因子
        this.loadFactor = loadFactor;

        //指定扩容阈值。(注意:这里还并没有 * 负载因子,最终会在put方法中进行赋值)
        this.threshold = tableSizeFor(initialCapacity);
    }
    
    //计算容量大小,当容量不是2的n次幂时,会将之设置为大于指定容量的最小2的次幂数。
    static final int tableSizeFor(int cap) {
        //为什么会先减去1呢,如果不减去1时,那么指定的值刚好为2的n次幂的话,计算的结果会是指定值的双倍。
        //比如:指定的8,不减去1,计算结果就是16
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

5.HashMap的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;

        //默认数组长度tab为null,
        if ((tab = table) == null || (n = tab.length) == 0)
            //默认情况都未0,先扩容大小
            n = (tab = resize()).length;
        //(n - 1) & hash通过数组长度-1&hash值获取数组索引值,如果当前位置为空
        if ((p = tab[i = (n - 1) & hash]) == null)
            //数组索引位置,创建一个Node节点,并赋值
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //如果数组位置有值,对比元素的hash值,key值是否相等或者key的equals值是否相等。如果都相等,直接替换
            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) {
                        //表示没有链表,创建链表并赋值
                        p.next = newNode(hash, key, value, null);
                        //判断链表节点长度 >= 7,如果大于,进行判断数组长度是否超过64,未超过进行扩容,否则转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //对比hash值,和key的equals方法内容是否相等,相等直接赋值,否则添加节点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //当key相同时,进行value值替换,并返回老的value值
            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;
    }


   /**
    * 扩容方法
    */
   final Node<K,V>[] resize() {
        //table为数组长度,默认为null
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //默认请款都为0
        if (oldCap > 0) {
            //当oldCap是否大于最大值2^30
            if (oldCap >= MAXIMUM_CAPACITY) {
                //扩容边界值,也为最大。也就是说不能在进行扩容了
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //新的长度 = 老的数组长度左移1位,也就是老的长度的2倍大小。
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //新的扩容边界值 = 为老的边界值左移1位,也是老边界值得2倍,比如12变为24
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //首次默认会进入,进行扩容
        else {               // zero initial threshold signifies using defaults
            //默认数组长度为16
            newCap = DEFAULT_INITIAL_CAPACITY;
            //扩展边界值为数组长度 16 *  负载因子0.75
            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
        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 void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //如果tab不等于空,并且<64,此时就进行扩容
        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);
        }
    }

4.面试问题:

  4.1.为什么HashMap的长度必须是2的n次幂?如果不是2的n次幂,比如为10会怎样?

   答:为了防止哈希碰撞,提高性能。所以HashMap数组的长度一定是2的n次幂,如果不是2的n次幂,会通过右移,按位或运算将长度变为指定容量大的最小的2的n次幂。
   数组索引 = hash值&(length - 1)
   例如:长度为8的时候, 4&(8-1)=4  2&(8-1)=2
       hashCode值:  4     00000100
       length-1      7     00000111
       --------------------------------
       索引值:            00000100   4
    
       hashCode值:  2     00000010
       length-1      7     00000111
       --------------------------------
       索引值:            00000010   2
      总结:不容易产生哈希碰撞。
    
    例如:长度为10的时候
       hashCode值:  4     00000100
       length-1      9     00001001
       --------------------------------
       索引值:            00000000   0
    
       hashCode值:  2     00000010
       length-1      9     00001001
       --------------------------------
       索引值:            00000000   0
      总结:hash值相等,就会产生哈希碰撞。
    
    //会通过我们指定的值的无符号右移,与运算计算出长度。
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

  4.2.为什么指定容量计算时先减1后,才进行右移和与运算?

   答:当我们不减1直接进行运算是,如果我们指定的值刚好为2的n次幂,那么计算出来的容量就会为指定的2倍。

  4.3.HashMap中hash函数是怎么实现的?还有那些hash函数的实现方式?

   答:通过源码可知,他是通过元素的key的hashCode值进行无符号向右移16位。我们也可以直接通过对key的hashCode值进行取余(%)。key.hashCode()%16,只是该方式效率不如无符号向右移动高。
   static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

  4.4.何时会发生哈希碰撞?

   答:当通过元素key的hashCode值进行位计算后,如果hash值相等,就会产生哈希碰撞,在JDK8之前采用链表解决,JDK8之后采用链表+红黑树解决。

  4.5.引用红黑树的目的是什么?

   答:引用红黑树的目的就是为了提高查找效率,通过文档说明,红黑树的节点占用内存大小是普通节点的2倍,都有好有坏。

  4.6.当两个元素的hashCode值相等会怎样?

   答:如果hashCode值相等,那么就会获取key的equals方法,获取内容信息,对比两个元素内容信息是否相同,如果相同,那么就直接覆盖,赋值新的Value值,如果不同,那么直接添加在链表上。

  4.7.负载因子为什么是0.75呢,为什么不是0.3,0.9呢?

   答:数值大的话,数组元素存储较多,发生哈希冲撞的几率增大,查找元素效率低。数值小的话,数组元素存储的个数较少,就会产生扩容,造成空间的浪费。
    比如:如果为0.3,16 * 0.3 = 4.8,也就是说存储4个元素就会进行扩容,还有12个数组空间被浪费,太小了不好。如果是0.9,16 * 0.9 = 14.4,也就是说存储14个元素才进行扩容,基本块存储满了,这样会出现哈希冲突的几率非常大,所以说为我们选用了一个最优值0.75,不建议指定该值。

上一篇下一篇

猜你喜欢

热点阅读