属性
HashMap中有一些重要属性如下:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient int size;
int threshold;
final float loadFactor;
transient Node<K,V>[] table;
构造器
自定义了四个构造器(包括无参数的默认构造器):
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;
//tableSizeFor的作用是求得不小于initialCapacity的最小2的幂次方值,
//比如initialCapacity是15,求得16=2^4;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//暂时不讨论该构造器
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
一、HashMap(int initialCapacity, float loadFactor)
该构造器首先确保参数的有效性,且initialCapacity
不能大于MAXIMUM_CAPACITY
(2 ^ 30),然后初始化了loadFactor
和threshold
,注意到并没有去开辟table数组占用的内存,那什么时候开辟呢?答案是第一次put的时候。而此时的阈值为什么要是一个2的幂次方值呢?显然threashold
应该和capacity
和loadFactor
相关,但此处直接将tableSizeFor(initialCapacity)
的值赋值给threashold
,是不是不合理呢?下面会讲。
二、HashMap(int initialCapacity)
该构造器和调用上一个构造器,只是将loadFactor
赋值为DEFAULT_LOAD_FACTOR
(0.75),也没有开辟桶数组的空间。
三、HashMap( )
该构造器只是将loadFactor
赋值为DEFAULT_LOAD_FACTOR
(0.75),注意到调用这个构造器后,threshold
为0,该构造器也没有开辟桶数组的空间。
下面这个表对后面的分析会有用处:
构造器 | this.loadFactor | this. threshold | this.table | this.size |
---|---|---|---|---|
HashMap(int initialCapacity, float loadFactor) | loadFactor | tableSizeFor(initialCapacity) | null | 0 |
HashMap(int initialCapacity) | DEFAULT_LOAD_FACTOR | tableSizeFor(initialCapacity) | null | 0 |
HashMap( ) | DEFAULT_LOAD_FACTOR | 0 | null | 0 |
四、第一次put操作
创建了一个HashMap之后,紧接着就会向map中添加键值对,map.put(key, value)
。
第一次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;
if ((tab = table) == null || (n = tab.length) == 0)
//第一次调用put操作时,table都是null,所以必走这一步
n = (tab = resize()).length; //resize()的作用就是开辟数组空间,此时tab就是新开辟数组的引用
//开辟出新数组之后,计算当前key应该放在哪个slot中
if ((p = tab[i = (n - 1) & hash]) == null) //i = (n - 1) & hash,计算索引
tab[i] = newNode(hash, key, value, null);
else {
//第一次调用put的时候,下面代码都不会执行
//...
}
++modCount;
//put完之后,size值会加1,如果此时大于阈值,将会扩容。
if (++size > threshold)
resize();
return null;
}
可以看出put
操作的主要流程:
- 分配数组空间
- 插入键值对
- ++size,++modCount
下面主要看看开辟数组空间的逻辑:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // now the table is null
int oldCap = (oldTab == null) ? 0 : oldTab.length; // so oldCap == 0
/*
* 1、若调用的是带有参数的构造器,oldThr = tableSizeFor(initialCapacity) > 0,
* 2、若调用的是无参数的构造器,oldThr == 0
*/
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//下面代码不会调用,因为oldCap == 0
}
else if (oldThr > 0) // initial capacity was placed in threshold
//若调用的是带有参数的构造器,此时计算的桶数组大小就是 tableSizeFor(initialCapacity) > 0
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//若调用的是无参数的构造器,newCap == 16, newThr == (int) 16 * 0.75
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//到这里如果newThr还没有被赋值,就执行下面代码
//为什么到这里newThr依然为0呢?如果调用带有参数的构造器,那么threashold是大于0的,那么
//就执行上面的if(oldThr > 0),将oldThr值赋给newCap,此时newThr依然为0, 而原来的threshold也就没有了用处
//这说明之前将一个2的幂次方值赋值给threshold,只是为了赋值给桶数组的默认容量值(newCap = oldThr),赋值完之后,
//threashold就需要重新计算了,下面就是计算过程:
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//覆盖threshold
threshold = newThr;
//开辟空间在这里
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//这里面的代码不会调用,因为oldTab == null。
}
//返回桶数组的引用
return newTab;
}
总结
1、调用HashMap构造器的时候,并不会开辟桶数组的空间。ArrayList的构造器在指定大小的时候会分配空间,而无参数的构造器则会分配一个长度为0的数组。
2、调用initialCapacity参数的构造器后,第一次put时的数组空间大小是不小于initialCapacity的最小2的幂次方值。
3、调用无参的构造方法之后,第一次put
,table
数组的空间大小是16,threshold
是16*0.75=12。
3、先put,然后比较size和threshold,进行扩容,扩容到两倍。
4、参数含义:
参数 | 含义 |
---|---|
size | map中的key-value对的数量 |
table.length | 即HashMap的容量,capacity |
threshold | 当size>threshold的时候,进行扩容,一般情况下threshold = capacity * loadFactor |
loadFactor | 负载因子,表明容器的利用率,可以大于1,一般不需要修改,默认值即可。 |
参考
1、https://www.zhihu.com/question/47028049
2、https://zhuanlan.zhihu.com/p/30360734
网友评论