HashMap的几个构造方法源码解析(基于JDK1.8中的HashMap源码)
1、无参构造方法HashMap()
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() { //无参构造器
//负载因子为默认值 0.75f
//容量为默认初始值 16
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
2、有一个初始容量参数的构造方法HashMap(int initialCapacity)
参数: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) {
//此处通过把第二个参数负载因子使用默认值0.75f,然后调用有两个参数的构造方法
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
这个一个参数的构造方法,使用HashMap的默认负载因子,把该初始容量和默认负载因子作为入参,调用HashMap的两个参数的构造方法
3、有两个参数的构造方法HashMap(int initialCapacity, float loadFactor)
参数:initialCapacity 初始容量
参数:loadFactor 负载因子
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
* 通过指定的初始容量和负载因子初始化一个空的HashMap
*
* @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) {
if (initialCapacity < 0) //如果初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) //如果初始容量超过最大容量(1<<32)
initialCapacity = MAXIMUM_CAPACITY; //则使用最大容量作为初始容量
if (loadFactor <= 0 || Float.isNaN(loadFactor)) //如果负载因子小于等于0或者不是数字,则抛出异常
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor; //把负载因子赋值给成员变量loadFactor
//调用tableSizeFor方法计算出不小于initialCapacity的最小的2的幂的结果,并赋给成员变量threshold
this.threshold = tableSizeFor(initialCapacity);
}
我们下面看看tableSizeFor()这个方法是如何计算的,这个方法的实现原理很巧妙,源码如下:
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1; //容量减1,为了防止初始化容量已经是2的幂的情况,最后有+1运算。
n |= n >>> 1; //将n无符号右移一位再与n做或操作
n |= n >>> 2; //将n无符号右移两位再与n做或操作
n |= n >>> 4; //将n无符号右移四位再与n做或操作
n |= n >>> 8; //将n无符号右移八位再与n做或操作
n |= n >>> 16; //将n无符号右移十六位再与n做或操作
//如果入参cap为小于或等于0的数,那么经过cap-1之后n为负数,n经过无符号右移和或操作后仍未负
//数,所以如果n<0,则返回1;如果n大于或等于最大容量,则返回最大容量;否则返回n+1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
借用一位博主对tableSizeFor()方法的梳理:该算法分析原文地址(https://blog.csdn.net/fan2012huan/article/details/51097331)
首先,为什么要对cap做减1操作。int n = cap - 1;
这是为了防止,cap已经是2的幂。如果cap已经是2的幂, 又没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍。如果不懂,要看完后面的几个无符号右移之后再回来看看。
下面看看这几个无符号右移操作:
如果n这时为0了(经过了cap-1之后),则经过后面的几次无符号右移依然是0,最后返回的capacity是1(最后有个n+1的操作)。
这里只讨论n不等于0的情况。
第一次右移
n |= n >> 1;
由于n不等于0,则n的二进制表示中总会有一bit为1,这时考虑最高位的1。通过无符号右移1位,则将最高位的1右移了1位,再做或操作,使得n的二进制表示中与最高位的1紧邻的右边一位也为1,如000011xxxxxx。
第二次右移
n |= n >>> 2;
注意,这个n已经经过了n |= n >>> 1; 操作。假设此时n为000011xxxxxx ,则n无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1。如00001111xxxxxx 。
第三次右移
n |= n >>> 4;
这次把已经有的高位中的连续的4个1,右移4位,再做或操作,这样n的二进制表示的高位中会有8个连续的1。如00001111 1111xxxxxx 。
以此类推
注意,容量最大也就是32bit的正数,因此最后n |= n >>> 16; ,最多也就32个1,但是这时已经大于了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY 。
举一个例子说明下吧。
这个算法着实牛逼啊!
注意,得到的这个capacity却被赋值给了threshold。
this.threshold = tableSizeFor(initialCapacity);
开始以为这个是个Bug,感觉应该这么写:
this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
这样才符合threshold的意思(当HashMap的size到达threshold这个阈值时会扩容)。
但是,请注意,在构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算。
4、有一个Map类型的参数的构造方法
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
* 根据传入的指定的Map参数去初始化一个新的HashMap,该HashMap拥有着和原Map中相同的映射关系
* 以及默认的负载因子(0.75f)和一个大小充足的初始容量
* @param m the map whose mappings are to be placed in this map
* 参数 m 一个映射关系将会被新的HashMap所取代的Map
* @throws NullPointerException if the specified map is null
* 如果这个Map为空的话,将会抛出空指针异常
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR; //将默认的负载因子赋值给成员变量loadFactor
putMapEntries(m, false); //调用PutMapEntries()来完成HashMap的初始化赋值过程
}
我们看下putMapEntries()方法,这个方法调用了HashMap的resize()扩容方法和putVal()存入数据方法,源码如下:
/**
* Implements Map.putAll and Map constructor
*
* @param m the map
* @param evict false when initially constructing this map, else
* true (relayed to method afterNodeInsertion).
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size(); //定义一个s,大小等于map的大小,这个未做非空判断,可能抛出空指针异常
if (s > 0) { //如果map键值对个数大于0
if (table == null) { // pre-size 如果当前的HashMap的table为空
float ft = ((float)s / loadFactor) + 1.0F; //计算HashMap的最小需要的容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ? //如果该容量大于最大容量,则使用最大容量
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold) //如果容量大于threshold,则对对容量计算,取大于该容量的最小的2的幂的值
//并赋给threshold,作为HashMap的容量。tableSizeFor()方法讲解在上面
//已经有了说明,不明白的小伙伴可以往上翻翻看。
threshold = tableSizeFor(t);
}
else if (s > threshold) //如果table不为空,即HashMap中已经有了数据,判断Map的大小是否超过了HashMap的阈值
resize(); //如果超过阈值,则需要对HashMap进行扩容
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { //对Map的EntrySet进行遍历
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict); //调用HashMap的put方法的具体实现方法来对数据进行存放。
}
}
}
要看懂putMapEntries()方法,就必须弄懂resize()方法和putVal()方法,下面我们对这两个方法源码进行分析。
我们下面看看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.
* 初始化或把table容量翻倍。如果table是空,则根据threshold属性的值去初始化HashMap的容
* 量。如果不为空,则进行扩容,因为我们使用2的次幂来给HashMap进行扩容,所以每个箱子里的元素
* 必须保持在原来的位置或在新的table中以2的次幂作为偏移量进行移动
*
*
* @return the table
*/
final Node<K,V>[] resize() {
//定义一个oldTab存放当前的table
Node<K,V>[] oldTab = table;
//判断当前的table是否为空,如果为空,则把0赋值给新定义的oldCap,否则以table的长度作为oldCap的大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold; //把table的阈值赋值给oldThr变量
int newCap, newThr = 0; //定义变量newCap和newThr来存放新的table的容量和阈值
if (oldCap > 0) { //如果原来的table长度大于0
if (oldCap >= MAXIMUM_CAPACITY) { //判断长度是否大于HashMap的最大容量
threshold = Integer.MAX_VALUE; //以int的最大值作为原来HashMap的阈值,并把原来的table返回
return oldTab;
}
//如果原table容量不超过HashMap的最大容量,将 原容量*2 赋值给变量newCap,如果newCap不大于HashMap的最大容量,并且原容量大于HashMap的默认容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//将newThr的值设置为 原HashMap的阈值*2
newThr = oldThr << 1; // double threshold
}
//如果原容量不大于0,即原table为null,并且原阈值大于0
else if (oldThr > 0) // initial capacity was placed in threshold
//将原阈值作为容量赋值给newCap当做newCap的值
newCap = oldThr;
// 如果原容量不大于0,别切原阈值也不大于0
else { // zero initial threshold signifies using defaults
//则以默认容量作为newCap的值
newCap = DEFAULT_INITIAL_CAPACITY;
//以初始容量*默认负载因子的结果作为newThr值
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//经过上面的处理过程,如果newThr值为0,给newThr进行赋值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将新的阈值newThr赋值给threshold,为新初始化的HashMap来使用
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//初始化一个新的容量大小为newCap的Node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//给table重新赋值
table = newTab;
if (oldTab != null) { //如果原来的HashMap中有值,则遍历
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) { //如果原来的table数组中第j个位置不为空
oldTab[j] = null; //把e = oldTab[j],然后让oldTab[j]置空
if (e.next == null) //如果e.next = null,说明e.next不存在其他Node
newTab[e.hash & (newCap - 1)] = e; //此时以e.hash&(newCap-1)的结果作为e在newTab中的位置
else if (e instanceof TreeNode) //否则判断e的类型是TreeNode还是Node,即链表和红黑树判断
((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //如果时红黑树,则进行红黑树的处理
else { // preserve order //如果是链表
//定义了五个Node变量,我一直想知道lo和hi是是哪两个单词的缩写,
//根据代码来看应该是lower和higher吧,也就是高位和低位,
//因为我们知道HashMap扩容时,容量会扩到原容量的2倍,
//也就是放在链表中的Node的位置可能保持不变或位置变成 原位置+oldCap ,
//这里的高低应该就是这个意思吧,当然这只是个人理解。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do { //循环链表中的Node
next = e.next;
//如果e.hash & oldCap == 0,注意这里是oldCap,而不是oldCap-1。
//我们知道oldCap是2的次幂,也就是1、2、4、8、16...转化为二进制之后,
//都是高位为1,其它位为0。所以oldCap & e.hash 也是只有e.hash值在oldCap二进制不为0的位对应的位也不为0时,
//才会得到一个不为0的结果。举个例子,我们知道10010 和00010 与1111的&运算结果都是 0010 ,
//但是110010和010010与10000的运算结果是不一样的,所以HashMap就是利用这一点,
//来判断当前在链表中的数据,在扩容时位置时保持不变还是位置移动oldCap。
if ((e.hash & oldCap) == 0) { //如果结果为0,即位置保持不变
if (loTail == null) //如果是第一次遍历
loHead = e; //让loHead = e
else
loTail.next = e; //否则,让loTail的next = e
loTail = e; //最后让loTail = e
}
//其实if 和else 中做的事情是一样的,我们看到有loHead和loTail两个Node,
//我们其实可以把loHead当做头元素,然后loTail是用来维护loHead的,即每次循环,
//更新loHead的next。我们来举个例子,比如原来的链表是A->B->C->D->E。
//我们这里把->假设成next关系,这五个Node中,只有C的hash & oldCap != 0 ,
//然后这个代码执行过程就是:
//第一次循环: 先拿到A,把A赋给loHead,然后loTail也是A
//第二次循环: 此时e的为B,而且loTail != null,也就是进入上面的else分支,把loTail.next =
// B,此时loTail中即A->B,同样反应在loHead中也是A->B,然后把loTail = B
//第三次循环: 此时e = C,由于C不满足 (e.hash & oldCap) == 0,进入到了我们下面的else分支,其
// 实做的事情和当前分支的意思一样,只不过维护的是hiHead和hiTail。
//第四次循环: 此时e的为D,loTail != null,进入上面的else分支,把loTail.next =
// D,此时loTail中即B->D,同样反应在loHead中也是A->B->D,然后把loTail = D
//.
//.
//.
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//遍历结束,即把table[j]中所有的Node处理完
if (loTail != null) { //如果loTail不为空,此时保证了loHead不为空
loTail.next = null; //此时把loTail的next置空
newTab[j] = loHead; //把loHead放在newTab数组的第j个位置上
}
if (hiTail != null) { 同理,只不过hiHead放的位置是j+oldCap
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab; //最后返回newTab
}
我们上面讲了resize()方法,下面看看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;
if ((tab = table) == null || (n = tab.length) == 0) //如果hashMap为空,则使用resize方法去初始化HashMap
n = (tab = resize()).length; //把table数组的长度赋值给n
if ((p = tab[i = (n - 1) & hash]) == null) //如果tab的第(n-1) & hash为空,即此处没有Node
tab[i] = newNode(hash, key, value, null); //则初始化一个新的Node存放在此处
else {
Node<K,V> e; K k; //如果需要存放的位置已经存在了键值对
if (p.hash == hash && //判断此处键值对的key是否和我们要存入的键值对的key相同
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //如果相同,则把此处的Node赋给Node e
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) { //如果当前节点的next为空,即当前链表中不存在我们要存放的键值对
p.next = newNode(hash, key, value, null); //则把当前节点的next赋值为我们要存放的键值对
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和我们要存入的键值对的key相同的Node,则跳出循环,此时e = p.next
break;
p = e; //把p = p.next 进行遍历
}
}
if (e != null) { // existing mapping for key //如果e不为空,此处的e为我们要存放value的键值对
V oldValue = e.value; //把原来的值取出来
if (!onlyIfAbsent || oldValue == null) //如果onlyIfAbsent为false或者oldValue为null则进行覆盖,默认onlyIfAbsent为false
e.value = value;
afterNodeAccess(e); //这个为linkedHashMap中才有意义,HashMap为空方法
return oldValue;
}
}
++modCount; //让HashMap的修改次数+1
if (++size > threshold) //判断当前Hash的键值对数量是否超过扩容阈值
resize(); //如果超过扩容阈值则进行扩容
afterNodeInsertion(evict); //这个为linkedHashMap中才有意义,HashMap为空方法
return null;
}
终于分析完了,自己也从中获益很多,希望看到的同学也能从中获益。2018-12-20 今天英雄联盟德玛西亚杯哦~