HashMap源码分析(重要)

1.成员变量、树化阈值

DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16(桶的个数)
DEFAULT_LOAD_FACTOR = 0.75f(负载因子)
TREEIFY_THRESHOLD = 8;(树化阈值)
MIN_TREEIFY_CAPACITY = 64(树化要求的最少哈希表元素数量)
UNTREEIFY_THRESHOLD = 6;(解除树化阈值,resize阶段)

树化总结:当桶中链表元素个数超过8,并且哈希表中所有元素个数超过64,此时会将桶中链表转为红黑树结构。否则(只是
链表元素个数超过8),只是简单地进行扩容操作而已。树化针对符合条件的桶,只有桶中链表的元素超过8,并且哈希表中
元素个数超过64,该哈希桶才会树化,并不是所有的桶都树化。

树化的好处
1)便于查找  链表:T(n)=O(n)   二叉树T(n)=O(log(n))
2)安全问题  黑客服务:哈希碰撞拒绝服务。一直调用HashMap的put(),以前的HashMap会一直在桶后面+++,就会造成大量的
哈希都碰撞在同一个位置上,导致CPU占用率达到100%,整个服务器挂掉。面试回答查找快即可,不用答哈希碰撞(避免面试官误以为懂网络安全)。

为什么要树化?
当桶中链表长度太长会大大影响查找速度,因此将其树化来提高指定节点的速度。

为什么要使用红黑树?结合红黑树的特点回答

2.初始化策略–懒加载(同ArrayList)

HashMap采用lazy-load策略(当第一次使用put()时,才会将哈希表初始化。)

//无参构造
  public HashMap() {
       //仅仅将负载因子赋值,默认为0.75.
       //哈希表并未初始化
	 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

//有参构造一
  public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

//有参构造二
 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);
    }

注意:要求初始化容量必须为2的n次方,若通过构造方法传入一个非2^n数值,HashMap会在内部调用tableSizeFor返回一个
距离最近的2^n数值
。eg:传15,返回16;传31,返32;传100,返回128.

3.put方法

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)
			//调用resize()进行哈希表的初始化操作   初始容量n=16
            n = (tab = resize()).length;
			//i = (n - 1) & hash计算桶的下标  若n恰好为2^n,此时上述运算刚好就是hash%(n-1) 
			//(n - 1) & hash 使用位运算代替取模运算提高分桶速度
        if ((p = tab[i = (n - 1) & hash]) == null)//当前桶中元素为空
		//将要保存的元素作为桶的头结点保存
            tab[i] = newNode(hash, key, value, null);
			//哈希表已经初始化,并且分得的桶不为空。
        else {
            Node<K,V> e; K k;
			//当要保存的节点的key值与桶中节点的key值相同,直接替换首节点。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//情况:map.put(null,1)  map.put(null,2)(此时就是该情况)
                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;
                    }
					//链表中存在相同key,替换其value。
                    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;
		//添加节点后,整个HashMap的元素个数若要超过容量时,调用resize()进行扩容工作。
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

put方法流程:
    1.若HashMap未初始化,调用resize()进行初始化操作。
    2.对key值Hash取得要存储的桶下标
        1)若桶中为空,将节点直接作为桶的头结点保存
        2)若桶中不为空
            a.若树化,使用树的方式添加新节点。
            b.将新节点以链表形式尾插到最后。
                 -添加元素后,链表的个数binCount>=树化阈值-1,尝试进行树化操作。
        3)若桶中存在相同key节点,替换value值。
    3.添加元素后计算整个哈希表大小,若超过threshold(容量*负载因子),进行resize()扩容操作。

结论:16*0.75=12,故所有桶的数量超过12,桶就扩容了。

4.get方法

	    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
		//哈希表已经初始化并且分得桶的头节点不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
				//要查找的节点key值恰好等于桶中头节点的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);
            }
        }
        return null;
    }

get方法流程:
    1.若哈希表已经初始化,并且桶的首节点不为空。
        1)查找节点的key值恰好等于首节点,直接返回首节点。
        2)进行桶元素的遍历,查找指定节点
            a.若树化,按照树的方式查找。
            b.按照链表方式查找。
    2.哈希表为空或桶的首节点为null,直接返回null。

5.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;
            }
			//将哈希表double扩容
            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;
		//将原表中的元素移动到新表
		//策略:原来的元素要么待在原桶中,要么移动到double size的桶下。
        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)//如果树化,按照树的方式移动
					//调用树的方式进行元素移动
					//若在移动过程中,发现红黑树节点<=解树化阈值,会调用untreeif方法
					//将红黑树解为链表
                        ((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;
    }

扩容resize流程:
    1.判断哈希表是否初始化,若还未初始化,根据InitCapcity值进行初始化操作
    2.若表已初始化,将原哈希表按照 2倍方式扩容
    3.扩容后进行原表元素的移动
        1)若桶中元素节点已经树化,调用树的方式移动元素
              (若在移动过程中发现红黑树节点<=6,会将红黑树解除树化,还原为链表)
        2)若未树化,调用链表的方式来移动元素。

6.性能问题及解决

1.多线程场景下,由于条件竞争,很容易造成死锁。(使用ConcurrentHashMap代替)
2.rehash是一个比较耗时的过程。(在能预估存储元素个数的前提下,尽量自定义初始化容量,尽量减少resize过程)。负载因子可以控制resize频率:负载因子较小,扩容频繁;负载因子较大,增加碰撞几率。因此负载因子尽量不要改。

7.哈希算法

//返回高低16位共同参与运算的哈希值
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
///h>>>16:将h无符号右移16位,实际上,就是将高16位移到低16位,然后和原来的所有数进行异或运算。

问题:为何不直接采用Object类的hashCode()返回桶下标:因为Object类的hashCode()几乎不会发生碰撞,需要的桶个数太多。
为什么容量必须为2的n次方?

用位运算替代数学取模运算,提高速度。

HashMap允许key和value为空,如果传进来的key为空,则放入第一个桶中,否则计算它的哈希码。

    原文作者:寻瀑
    原文地址: https://blog.csdn.net/smell201611010513/article/details/89459148
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞