Java HashMap底层实现和原理分析(四)
文章目录
- JDK1.8 HashMap介绍
- JDK1.8 HashMap源码分析
- 成员变量
- 构造方法
- `put(K key, V value)`方法
- `hash(Object key)`方法
- `putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)`方法
- `treeifyBin(Node
JDK1.8 HashMap介绍
- 在JDK1.8 之前 HashMap 由 数组+链表 数据结构组成的。
- 在JDK1.8 之后 HashMap 由 数组+链表 +红黑树数据结构组成的。
JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突 (两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引值相同) 而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。
补充:将链表转换成红黑树前会判断,即使阈值大于8,但是数组长度小于64,此时并不会将链表变为红黑树。而是选择进行数组扩容。
这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡 。同时数组长度小于64时,搜索时间相对要快些。所以综上所述为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树。具体可以参考
treeifyBin方法。
当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于8并且数组长度大于64时,链表转换为红黑树时,效率也变的更高效。
JDK1.8 HashMap源码分析
前面的博文中我们已经分析了JDK1.7中HashMap的源码,这篇博文我们来分析JDK1.8中HashMap的源码,看看JDK1.8中对HashMap做了哪些改变。
JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。 当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。
JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度(阀值)超过 8 时且当前数组的长度 > 64时,将链表转换为红黑树,这样大大减少了查找时间。jdk8在哈希表中引入红黑树的原因只是为了查找效率更高。

成员变量
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {private static final long serialVersionUID = 362498820763181265L;/*** The default initial capacity - MUST be a power of two.*///定义默认的初始容量为16,必须是2的幂次方(面试常问,之前的博文已经分析过为啥一定是2的幂次方)static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/*** The maximum capacity, used if a higher value is implicitly specified* by either of the constructors with arguments.* MUST be a power of two <= 1<<30.*///最大的容量static final int MAXIMUM_CAPACITY = 1 << 30;/*** The load factor used when none specified in constructor.*/// 定义默认的负载因子是0.75static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** The bin count threshold for using a tree rather than list for a* bin. Bins are converted to trees when adding an element to a* bin with at least this many nodes. The value must be greater* than 2 and should be at least 8 to mesh with assumptions in* tree removal about conversion back to plain bins upon* shrinkage.*/// 当链表中元素个数大于8时,HashMap将会把链表转换为红黑树的结构。static final int TREEIFY_THRESHOLD = 8;/*** The bin count threshold for untreeifying a (split) bin during a* resize operation. Should be less than TREEIFY_THRESHOLD, and at* most 6 to mesh with shrinkage detection under removal.*///当树中的元素个数小于6个时,HashMap会将红黑树转换为链表结构。static final int UNTREEIFY_THRESHOLD = 6;/*** The smallest table capacity for which bins may be treeified.* (Otherwise the table is resized if too many nodes in a bin.)* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts* between resizing and treeification thresholds.*///当链表中的元素等于8个进行创建树的时候,如果当前桶的数量小于64,则进行扩容重新分配 hash 值,而不是将节点变为红黑树。static final int MIN_TREEIFY_CAPACITY = 64;/*** The table, initialized on first use, and resized as* necessary. When allocated, length is always a power of two.* (We also tolerate length zero in some operations to allow* bootstrapping mechanics that are currently not needed.)*///JDK1.8中改为了Node, JDK1.7中则是Entrytransient Node<K,V>[] table;/*** Holds cached entrySet(). Note that AbstractMap fields are used* for keySet() and values().*/transient Set<Map.Entry<K,V>> entrySet;/*** The number of key-value mappings contained in this map.*/transient int size;/*** The number of times this HashMap has been structurally modified* Structural modifications are those that change the number of mappings in* the HashMap or otherwise modify its internal structure (e.g.,* rehash). This field is used to make iterators on Collection-views of* the HashMap fail-fast. (See ConcurrentModificationException).*/transient int modCount;/*** The next size value at which to resize (capacity * load factor).** @serial*/// (The javadoc description is true upon serialization.// Additionally, if the table array has not been allocated, this// field holds the initial array capacity, or zero signifying// DEFAULT_INITIAL_CAPACITY.)int threshold;/*** The load factor for the hash table.** @serial*/final float loadFactor;//....
}
构造方法
首先我们来看看构造方法,其实大体上跟JDK1.7是一样的。我这里重点介绍其中两个构造方法,这个构造方法跟JDK1.7是有些不同的,如下:
第一个就是无参构造方法
/*** Constructs an empty HashMap with the default initial capacity* (16) and the default load factor (0.75).*/public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}
跟JDK1.7不同的是,这里的无参构造只是对成员变量loadFactor进行了赋值,并没有调用其他的构造方法,而在JDK1.7中则是调用了HashMap(int initialCapacity, float loadFactor)的构造方法
第二个就是我们两个参数的构造方法:
/*** Constructs an empty HashMap with the specified initial* capacity and load factor.** @param initialCapacity the initial capacity* @param loadFactor the load factor* @throws IllegalArgumentException if the initial capacity is negative* or the load factor is nonpositive*/public HashMap(int initialCapacity, float loadFactor) {//判断初始容量是否小于0,初始容量不能小于0if (initialCapacity < 0)//如果小于0,则抛出非法的参数异常IllegalArgumentExceptionthrow new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);//判断初始化容量initialCapacity是否大于集合的最大容量MAXIMUM_CAPACITY->2的30次幂 if (initialCapacity > MAXIMUM_CAPACITY)//如果超过MAXIMUM_CAPACITY,会将MAXIMUM_CAPACITY赋值给initialCapacityinitialCapacity = MAXIMUM_CAPACITY;//判断负载因子loadFactor是否小于等于0或者是否是一个非数值if (loadFactor <= 0 || Float.isNaN(loadFactor))//如果满足上述其中之一,则抛出非法的参数异常IllegalArgumentExceptionthrow new IllegalArgumentException("Illegal load factor: " +loadFactor);//将指定的加载因子赋值给HashMap成员变量的负载因子loadFactorthis.loadFactor = loadFactor;//前面部分跟JDK1.7都是一样的,区别就在下面this.threshold = tableSizeFor(initialCapacity);}
我们来看看这个tableSizeFor(initialCapacity)方法
/*** 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;}
这个方法我们就不多介绍了,前面的博文也介绍过,这个方法的作用就是得到大于给定initialCapacity的最小2的n次幂的整数。然后注意我们是把这个最小2的n次幂的整数赋值给了threshold。这个threshold的值应该是等于(capacity * load factor),而这里我们却赋值为HashMap的容量,是有问题吗?我们后面继续往后看,后面又对threshold进行了赋值的。
tableSizeFor(initialCapacity) 判断指定的初始化容量是否是2的n次幂,如果不是那么会变为比指 定初始化容量大的最小的2的n次幂。这点上述已经讲解过。
但是注意,在tableSizeFor方法体内部将计算后的数据返回给调用这里了,并且直接赋值给threshold边 界值了。有些人会觉得这里是一个bug,应该这样书写:
this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
这样才符合threshold的意思(当HashMap的size到达threshold这个阈值时会扩容)。
但是,请注意,在jdk8以后的构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推 迟到了put方法中,在put方法中会对threshold重新计算,put方法的具体实现我们下面会进行讲解
put(K key, V value)方法
接下来我们来介绍一下put()方法。
/*** Associates the specified value with the specified key in this map.* If the map previously contained a mapping for the key, the old* value is replaced.** @param key key with which the specified value is to be associated* @param value value to be associated with the specified key* @return the previous value associated with key, or* null if there was no mapping for key.* (A null return can also indicate that the map* previously associated null with key.)*/public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}
我们看到里面调用了putVal方法,然后传入的第一个参数是hash(key),我们来看看这个hash方法做了什么?
hash(Object key)方法
/*** Computes key.hashCode() and spreads (XORs) higher bits of hash* to lower. Because the table uses power-of-two masking, sets of* hashes that vary only in bits above the current mask will* always collide. (Among known examples are sets of Float keys* holding consecutive whole numbers in small tables.) So we* apply a transform that spreads the impact of higher bits* downward. There is a tradeoff between speed, utility, and* quality of bit-spreading. Because many common sets of hashes* are already reasonably distributed (so don't benefit from* spreading), and because we use trees to handle large sets of* collisions in bins, we just XOR some shifted bits in the* cheapest possible way to reduce systematic lossage, as well as* to incorporate impact of the highest bits that would otherwise* never be used in index calculations because of table bounds.*/static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
这个hash方法其实就是拿到key对象的hashCode,然后进行扰乱函数的处理,减少hash碰撞。这个我们前面的博文也有介绍过。
下面我们继续看这个putVal()方法
putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)方法
主要参数:
- hash key的hash值
- key 原始Key
- value 要存放的值
- onlyIfAbsent 如果true代表不更改现有的值
- evict 如果为false表示table为创建状态
我们现在开始分析这个方法,加上我们HashMap,在HashMap的无参构造方法中并没有做其他事情,只是对成员变量loadFactor进行了赋值。然后我们调用put方法往map对象中添加元素,接下来我们来分析这个put方法中做了些什么事情。
/*** Implements Map.put and related methods** @param hash hash for key* @param key the key* @param value the value to put* @param onlyIfAbsent if true, don't change existing value* @param evict if false, the table is in creation mode.* @return previous value, or null if none*/final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;/*1)transient Node[] table; 表示存储Map集合中元素的数组。2)(tab = table) == null 表示将空的table赋值给tab,然后判断tab是否等于null,第一次肯定是null3)(n = tab.length) == 0 表示将数组的长度0赋值给n,然后判断n是否等于0,n等于0由于if判断使用双或,满足一个即可,则执行代码 n = (tab = resize()).length; 进行数组初始化。并将初始化好的数组长度赋值给n.4)执行完n = (tab = resize()).length,数组tab每个空间都是null*/ 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 1sttreeifyBin(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 keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}
当第一次的时候HashMap是没有初始化的,所以条件if ((tab = table) == null || (n = tab.length) == 0)是为true的,所以会调用n = (tab = resize()).length;来进行数组的初始化,并把初始化好的数组长度赋值给n。
那么我们接下来就来看看这个resize()方法中做了什么?
这个方法简单的来说就是初始化或者是扩容的操作。
/*** Initializes or doubles table size. If null, allocates in* accord with initial capacity target held in field threshold.* Otherwise, because we are using power-of-two expansion, the* elements from each bin must either stay at same index, or move* with a power of two offset in the new table.** @return the table*/final Node<K,V>[] resize() {//得到当前数组Node<K,V>[] oldTab = table;//如果当前数组等于null长度返回0,否则返回当前数组的长度,所以当前的table是空,所以oldCap为0int oldCap = (oldTab == null) ? 0 : oldTab.length;//因为new HashMap()的时候并没有给threshold赋值,所以oldThr的值也为0int 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 thresholdnewCap = oldThr;//所以初始化的时候走到else的逻辑else { // zero initial threshold signifies using defaults//设置一个默认的容量,也就是16newCap = DEFAULT_INITIAL_CAPACITY;//设置阈值为(DEFAULT_LOAD_FACTOR * 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"})//初始化table数组Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//赋值到成员变量的table中table = newTab;//因为oldTab为空,所以不会走下面的逻辑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 orderNode<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;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;}}}}}// 返回新的tablereturn newTab;}
通过上面的代码我们可以看出来,经过resize()方法之后获得一个初始容量为16的table,然后threshold为
(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)
接下来我们继续看这个putVal的方法。
/*** Implements Map.put and related methods.** @param hash hash for key* @param key the key* @param value the value to put* @param onlyIfAbsent if true, don't change existing value* @param evict if false, the table is in creation mode.* @return previous value, or null if none*/final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;/*1)transient Node[] table; 表示存储Map集合中元素的数组。2)(tab = table) == null 表示将空的table赋值给tab,然后判断tab是否等于null,第一次肯定是null3)(n = tab.length) == 0 表示将数组的长度0赋值给n,然后判断n是否等于0,n等于0由于if判断使用双或,满足一个即可,则执行代码 n = (tab = resize()).length; 进行数组初始化。并将初始化好的数组长度赋值给n.4)执行完n = (tab = resize()).length,数组tab每个空间都是null*/ if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;/*1)i = (n - 1) & hash 表示计算数组的索引赋值给i,即确定元素存放在哪个桶中2)p = tab[i = (n - 1) & hash]表示获取计算出的位置的数据赋值给节点p3) (p = tab[i = (n - 1) & hash]) == null 判断节点位置是否等于null,如果为null,则执行代码:tab[i] = newNode(hash, key, value, null);根据键值对创建新的节点放入该位置的桶中小结:如果当前桶没有哈希碰撞冲突,则直接把键值对插入空间位置 */ if ((p = tab[i = (n - 1) & hash]) == null)//创建一个新的节点存入到桶中tab[i] = newNode(hash, key, value, null);else {// 执行else说明tab[i]不等于null,表示这个位置已经有值了。Node<K,V> e; K k;/*比较桶中第一个元素(数组中的结点)的hash值和key是否相等1)p.hash == hash :p.hash表示原来存在数据的hash值 hash表示后添加数据的hash值 比较两个hash值是否相等说明:p表示tab[i],即 newNode(hash, key, value, null)方法返回的Node对象。Node newNode(int hash, K key, V value, Node next) {return new Node<>(hash, key, value, next);}而在Node类中具有成员变量hash用来记录着之前数据的hash值的2)(k = p.key) == key :p.key获取原来数据的key赋值给k key表示后添加数据的key 比较两个key的地址值是否相等3)key != null && key.equals(k):能够执行到这里说明两个key的地址值不相等,那么先判断后添加的key是否等于null,如果不等于null再调用equals方法判断两个key的内容是否相等*/ if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))/*说明:两个元素哈希值相等,并且key的值也相等将旧的元素整体对象赋值给e,用e来记录*/ e = p;// hash值不相等或者key不相等;判断p是否为红黑树结点else if (p instanceof TreeNode)// 放入树中e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 说明是链表节点 else {/*1)如果是链表的话需要遍历到最后节点然后插入2)采用循环遍历的方式,判断链表中是否有重复的key*/for (int binCount = 0; ; ++binCount) {/*1)e = p.next 获取p的下一个元素赋值给e2)(e = p.next) == null 判断p.next是否等于null,等于null,说明p没有下一个元素,那么此时到达了链表的尾部,还没有找到重复的key,则说明HashMap没有包含该键将该键值对插入链表中*/if ((e = p.next) == null) {/*1)创建一个新的节点插入到尾部(尾插法)p.next = newNode(hash, key, value, null);Node newNode(int hash, K key, V value, Node next) {return new Node<>(hash, key, value, next);}注意第四个参数next是null,因为当前元素插入到链表末尾了,那么下一个节点肯定是null2)这种添加方式也满足链表数据结构的特点,每次向后添加新的元素*/ p.next = newNode(hash, key, value, null);/*1)节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树2)int binCount = 0:表示for循环的初始化值。从0开始计数。记录着遍历节点的个数。值是0表示第一个节点,1表示第二个节点。。。。7表示第八个节点,加上数组中的第一个元素,元素个数是9TREEIFY_THRESHOLD - 1 --> 8 - 1 ---> 7如果binCount的值是7(加上数组中的的一个元素,元素个数是9)TREEIFY_THRESHOLD - 1也是7,此时转换红黑树*/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))))// 相等,跳出循环/*要添加的元素和链表中的存在的元素的key相等了,则跳出for循环。不用再继续比较了直接执行下面的if语句去替换去 if (e != null) */break;/*说明新添加的元素和当前节点不相等,继续查找下一个节点。用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表*/p = e;}}/*表示在桶中找到key值、hash值与插入元素相等的结点也就是说通过上面的操作找到了重复的键,所以这里就是把该键的值变为新的值,并返回旧值这里完成了put方法的修改功能*/if (e != null) { // existing mapping for key// 记录e的valueV oldValue = e.value;// onlyIfAbsent为false或者旧值为nullif (!onlyIfAbsent || oldValue == null)//用新值替换旧值//e.value 表示旧值 value表示新值 e.value = value;// 访问后回调afterNodeAccess(e);// 返回旧值return oldValue;}}//修改记录次数++modCount;// 判断实际大小是否大于threshold阈值,如果超过则扩容if (++size > threshold)resize();// 插入后回调afterNodeInsertion(evict);return null;}
treeifyBin(Node[] tab, int hash) 方法
当节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树,然后就会调用转换红黑树的方法treeifyBin。接下来我们来看看这个转换红黑树的treeifyBin方法。
/*** Replaces all linked nodes in bin at index for given hash unless* table is too small, in which case resizes instead.* 替换指定哈希表的索引处桶中的所有链接节点,除非表太小,在这种情况下会调整大小。* Node[] tab = tab 数组名* int hash = hash表示哈希值*/ final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;/*如果当前数组为空或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY = 64),就去扩容。而不是将节点变为红黑树。目的:如果数组很小,那么转换红黑树,然后遍历效率要低一些。这时进行扩容,那么重新计算哈希值,链表长度有可能就变短了,数据会放到数组中,这样相对来说效率高一些。*/if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)//扩容方法resize();else if ((e = tab[index = (n - 1) & hash]) != null) {/*1)执行到这里说明哈希表中的数组长度大于阈值64,开始进行树形化2)e = tab[index = (n - 1) & hash]表示将数组中的元素取出赋值给e,e是哈希表中指定位置桶里的链表节点,从第一个开始*///hd:红黑树的头结点 tl :红黑树的尾结点TreeNode<K,V> hd = null, tl = null;do {//新创建一个树的节点,内容和当前链表节点e一致TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null)//将新创键的p节点赋值给红黑树的头结点hd = p;else {/*p.prev = tl:将上一个节点p赋值给现在的p的前一个节点tl.next = p;将现在节点p作为树的尾结点的下一个节点*/p.prev = tl;tl.next = p;}tl = p;/*e = e.next 将当前节点的下一个节点赋值给e,如果下一个节点不等于null则回到上面继续取出链表中节点转换为红黑树*/} while ((e = e.next) != null);/*让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树而不是链表数据结构了*/if ((tab[index] = hd) != null)hd.treeify(tab);}}
小结:上述操作一共做了如下几件事:
- 根据哈希表中元素个数确定是扩容还是树形化
- 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系
- 然后让桶中的第一个元素指向新创建的树根节点,替换桶的链表内容为树形化内容
resize()方法
接下来我们来分析一下resize()方法。
想要了解HashMap的扩容机制你要有这两个问题
- 什么时候才需要扩容
- HashMap的扩容是什么
什么时候才需要扩容
当HashMap中的元素个数超过
数组大小(数组长度)*loadFactor(负载因子)时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)是0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中的元素个数超过16×0.75=12(这个值就是阈值或者边界值threshold值)的时候,就把数组的大小扩展为2×16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预知元素的个数能够有效的提高HashMap的性能。
补充:
当HashMap中的其中一个链表的对象个数如果达到了8个,此时如果数组长度没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链表会变成红黑树,节点类型由Node变成TreeNode类型。当然,如果映射关系被移除后,下次执行resize方法时判断树的节点个数低于6,也会再把树转换为链表。
HashMap的扩容是什么
进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。
HashMap在进行扩容时,使用的rehash方式非常巧妙,因为每次扩容都是翻倍,与原来计算的
(n-1)&hash的结果相比,只是多了一个bit位,所以节点要么就在原来的位置,要么就被分配到"原位置+旧容量"这个位置。
怎么理解呢?例如我们从16扩展为32时,具体的变化如下所示:

因此元素在重新计算hash之后,因为n变为2倍,那么n-1的标记范围在高位多1bit(红色),因此新的index就会发生这样的变化:

说明:5是假设计算出来的原来的索引。这样就验证了上述所描述的:扩容之后所以节点要么就在原来的位置,要么就被分配到"原位置+旧容量"这个位置。
因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就可以了,是0的话索引没变,是1的话索引变成“原索引+oldCap(原位置+旧容量)”。可以看看下图为16扩充为32的resize示意图:

正是因为这样巧妙的rehash方式,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,在resize的过程中保证了rehash之后每个桶上的节点数一定小于等于原来桶上的节点数,保证了rehash之后不会出现更严重的hash冲突,均匀的把之前的冲突的节点分散到新的桶中了。
下面是代码的具体实现:
/*** Initializes or doubles table size. If null, allocates in* accord with initial capacity target held in field threshold.* Otherwise, because we are using power-of-two expansion, the* elements from each bin must either stay at same index, or move* with a power of two offset in the new table.** @return the table*/final Node<K,V>[] resize() {//得到当前数组Node<K,V>[] oldTab = table;//如果当前数组等于null长度返回0,否则返回当前数组的长度int oldCap = (oldTab == null) ? 0 : oldTab.length;//当前阀值点 默认是12(16*0.75)int oldThr = threshold;int newCap, newThr = 0;//如果老的数组长度大于0//开始计算扩容后的大小if (oldCap > 0) {// 超过最大值就不再扩充了,就只好随你碰撞去吧if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}/*没超过最大值,就扩充为原来的2倍1)(newCap = oldCap << 1) < MAXIMUM_CAPACITY 扩大到2倍之后容量要小于最大容量2)oldCap >= DEFAULT_INITIAL_CAPACITY 原数组长度大于等于数组初始化长度16*/else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)//阈值扩大一倍newThr = oldThr << 1; // double threshold}//老阈值点大于0 直接赋值else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr; // 老阈值赋值给新的数组长度else { // zero initial threshold signifies using defaults// 直接使用默认值newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 计算新的resize最大上限if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}//新的阀值 默认原来是12 乘以2之后变为24threshold = newThr;//创建新的哈希表@SuppressWarnings({"rawtypes","unchecked"})//newCap是新的数组长度--> 32Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;//判断旧数组是否等于空if (oldTab != null) {// 把每个bucket都移动到新的buckets中//遍历旧的哈希表的每个桶,重新计算桶里元素的新位置for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {//原来的数据赋值为null 便于GC回收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;//这里来判断如果等于true e这个节点在resize之后不需要移动位置if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}// 原索引+oldCapelse {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 原索引放到bucket里if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 原索引+oldCap放到bucket里if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}
remove(Object key)方法
/*** Removes the mapping for the specified key from this map if present.** @param key key whose mapping is to be removed from the map* @return the previous value associated with key, or* null if there was no mapping for key.* (A null return can also indicate that the map* previously associated null with key.)*/public V remove(Object key) {Node<K,V> e;return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;}
remove方法的具体实现在removeNode方法中,所以我们重点看下removeNode方法
removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)方法
/*** Implements Map.remove and related methods** @param hash hash for key* @param key the key* @param value the value to match if matchValue, else ignored* @param matchValue if true only remove if value is equal* @param movable if false do not move other nodes while removing* @return the node, or null if none*/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;//根据hash找到位置 //如果当前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;//如果桶上的节点就是要找的key,则将node指向该节点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 {//判断是否以链表方式处理hash冲突,是的话则通过遍历链表来寻找要删除的节点do {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}//比较找到的key的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;elsep.next = node.next;//记录修改次数++modCount;//变动的数量--size;afterNodeRemoval(node);return node;}}return null;}
参考
Java 8系列之重新认识HashMap
HashMap 工作原理
JDK1.8中的HashMap
HashMap学习笔记
https://gitee.com/cckevincyh/hashmap-learning
Java 7/8 HashMap源码详解与面试题分析
【黑马程序员】HashMap集合介绍
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
