JDK1.8 HashMap学习
Posted dazhu123
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK1.8 HashMap学习相关的知识,希望对你有一定的参考价值。
1:源码分析
1.1:构造方法
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); } /** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
HashMap的源码中,含有两个参数的构造函数,其参数分别是initialCapacity和loadFactor,这两个参数和HashMap中原有的threshold的关系是什么尼?代码中有一行 this.threshold = tableSizeFor(initialCapacity);要将initialCapacity的值通过tableSizeFor的方法来返回一个值赋给HashMap的threshold,那这个方法有什么用处尼?
/** * Returns a power of two size for the given target capacity. */ 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; }
这个方法按照注释会返回一个大于输入的cap的2的幂数,详细介绍我们参考https://www.cnblogs.com/loading4/p/6239441.html。
这个位运算十分高效的,写出JDK的人真的太厉害了。
1.2:put方法
分析put方法之前,我们要分析一下hash方法的,
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
当key是null时,返回0,当不等于null时,将key的hashCode的值异或上其本身右移16位,这个操作有什么意义尼?简单来说,是为了对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。简单点说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。这是因为直接使用hashCode&length-1,得到数据只与hashCode的低位有关,为了避免出现两个key的hashcode的低位相同,高位不停而索引到相同的数组下标,我们将hashcode的高位数据也通过右移向异或的方式,将其不同的也影响到索引的位置,所以这样操作。
下面讲解put方法中使用的putVal的方法作用。以下图的注释讲解。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //定义一个节点Node的数组tab,和节点p, Node<K,V>[] tab; Node<K,V> p; int n, i; //判断table是不是null,或者长度为0,即再第一次put数据的时候,tab时空数组 if ((tab = table) == null || (n = tab.length) == 0) //如果为空则,进行扩容,其中resize()方法是用来扩容使用的,这里先跳过。n的值是扩容后的数据长度 n = (tab = resize()).length; //(n - 1) & hash这里是通过hash与上length-1取出hash的低位当作数组的下标 //判断该数组是不是null,这里p指向的是用过待输入节点hasd索引的位置的节点 if ((p = tab[i = (n - 1) & hash]) == null) //若为null,则newNode后,放在上面找到的位置。 tab[i] = newNode(hash, key, value, null); //若该位置不为null,进入else else { Node<K,V> e; K k; //由于该位置不为null,所以判断该位置的节点的hash值与待输入节点的hash比较,若hash相等,且 //该位置节点的key也等于带输入节点的key,这里用两个方法判断之间是或的关系,==或者equals方法 //对象相等或者内容相等,二者成立一个即可。 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //若该位置节点和带输入节点相等,则用e指向该位置节点。 e = p; //如果该位置节点与输入节点不相等,则判断该节点是不是TreeNode。 else if (p instanceof TreeNode) //若为树节点,则通过putTreeVal方法将该节点加入到red black tree中, e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //若不是树节点,则就是链表节点,按照链表进行操作 else { // for (int binCount = 0; ; ++binCount) { //e指向p的next节点,若e为null, if ((e = p.next) == null) { //则p.next指向输入节点即可,输入节点接在链表的尾部。 p.next = newNode(hash, key, value, null); //如果binCount大于等于8,即超过jdk1.8规定的链表长度, if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //则进行将红黑树的转换,将链表转为红黑树 treeifyBin(tab, hash); //然后跳出循环。 break; } //这一步操作与上面的代码功能类似,用来判断链表中是不是有与输入节点一样的节点存在, //若有,直接break if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; //前面有e = p.next,结合下面的操作实现向链表的后面移动的操作步骤,e始终指向下一个节点 p = e; } } //若e不等于null,意味着输入节点没有放在尾部,而是找到了相等的节点,进行替换, if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); //将替换节点的return oldValue return oldValue; } } //迭代器中实现fall fast的功能 //This field is used to make iterators on Collection-views of the HashMap fail-fast. ++modCount; //加入后的带大小如果大于负载,则扩容, if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
上面代码注释,说明了putValue方法,何时扩容?如何查询index?如何判断是链表还是红黑树?如果返回oldValue的机制,相信见注释。
扩容分为初始化空位,使用initialCpacity或者大于设置值的的the power of two!,和size超过了有效负载后的进行扩容的机制。下面详细介绍扩容方法的原理!同样以注释的方式进行讲解。
2:扩容流程
final Node<K,V>[] resize() { //用oldTab指向table Node<K,V>[] oldTab = table; //如果当前table为null,则oldCap至0,若不为null,则将table.length赋给oldCap //则oldCap始终为当前数组(table)的长度 int oldCap = (oldTab == null) ? 0 : oldTab.length; //保存原来的有效负载数, int oldThr = threshold; //创建新的Capacity和threshold为0 int newCap, newThr = 0; //如果原有容量大于0,第一次扩容是,原有的oldCap就是0. if (oldCap > 0) { //如果已经超过了最大值, if (oldCap >= MAXIMUM_CAPACITY) { //将有效负载至为整形最大值,和恐怖 threshold = Integer.MAX_VALUE; //并且返回原始的table,因为超过了最大值,所以不扩容了,直接返回原始的数组。 return oldTab; } //如果,将原来的oldCap扩到二倍赋给newCap,且小于最大容量,且oldCap是大于等于原始容量16的。 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //则将oldThr也扩大二倍后赋给newThr,这里得到的newCapu和newThr都是原来的二倍。 newThr = oldThr << 1; // double threshold } //假如,oldCap小于等于0,其实上面可以保证第一次扩容时,oldCap是等于0的。且oldThr大于0 else if (oldThr > 0) // initial capacity was placed in threshold //这将oldThr赋给newCap。 newCap = oldThr; //如果再oldCap为0,且oldThr也不大于0情况下,使用默认是来初始化newCap和newThr,分别为16,16*0.75 = 12 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //如果newThr==0,则通过newCap和loadFactor来计算得到newThr if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //将newThr赋给threshold,同时使用newcCap来创建新的newTab threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //table指向newTab table = newTab; //如果oldTab不等于null,这下面进行的就是最复炸的操作, //扩容后,将原数组,链表,红黑树的节点数据进行转移到新的数组中, //进行重新的找到位置即可! if (oldTab != null) { //以oldCap为上限来循环 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //遍历原数组,如果原数组位置不为null,则 if ((e = oldTab[j]) != null) { //将该位置至null oldTab[j] = null; //在上面的if中,e = oldTab[j],即e已经指向了该数组的节点了 //oldTab[j] = null;这操作只是将数组指向null,并不会改变e已经指向了原来的节点 //这意味着这里只有一个节点, if (e.next == null) //加入e的next指向null,则将通过e的hash与上新的length-1索引的新的坐标 //将e放在新的数组位置。 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) //如果该节点是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; //判断扩容后,原始链表需不需要移动改变位置,如果==0.则不需要改变,否则改变 //而改变位置为原下标+oldCap构成新的数组下标 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中 newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; //将需要改变位置的链表,按照原位置+olodCap放在newTab中 newTab[j + oldCap] = hiHead; } } } } } //返回扩容后的newTab return newTab; }
难点主要集中在扩容后,如何将原节点转移到新的数组中问题,扩容的判断和操作是很简单的。
主要分为链表和红黑树的转移问题。红黑树由于我不是很懂,暂时跳过。如上面注释的那样,现在原数组的节点链表中,分成两个链表:
需要index变化的链表,不需要index变化的链表。我们分析发现index变化的链表其变化后的index都是原始index+oldCap固定在,所以可以分成两个链表后,在统一在新的index位置直接将两个链表的head节点插入数组即可!
3:多线程问题
在JDK1.7中由于,是在扩容时,将链表会顺序反过来放在newTab中,所以多线程有形参循环链表的问题,而JDK1.8中都是在尾部放入新的节点,同时也是一次性的将一条链表移动到newTab而不是,循环的插入过程,所以不会再有多线程中的循环链表的问题出现。
以上是关于JDK1.8 HashMap学习的主要内容,如果未能解决你的问题,请参考以下文章
Java中HashMap底层实现原理(JDK1.8)源码分析
Java中HashMap底层实现原理(JDK1.8)源码分析