HashMap源码中的一些重要常量
- DEFAULT_INITIAL_CAPACITY: HashMap的默认容量16
- MAXIMUM_CAPACITY: HashMap的最大支持容量,2^30
- DEFAULT_LOAD_FAC TOR: HashMap的默认加载因子:0.75
- TREEIFY_THRESHOLD: Bucket中链表长度大于该默认值,转化为红黑树:8
- UNTREEIFY_THRESHOLD: Bucket中红黑树存储的Node小于该默认值,转化为链表
- MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64.(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
- table:存储元素的数组,总是2的n次幂entrySet:存储具体元素的集
- size: HashMap中存储的键值对的数量
- modCount: HashMap扩容和结构改变的次数。threshold:扩容的临界值,=容量*填充因子
- loadFactor:填充因子 jdk7
在实例化之后,创建了一个长度为16的一维数组Entry[] table
当有一个数据要添加时(key-value),调用key所在类的HashCode()方法计算出key的哈希值,经过某种算法后算出此哈希值所在数组中的位置。
如果没有数据,则添加成功
若有数据(意味着此位置有一个或多个数据,这些数据以链表形式存在),则依次比较链表中的key值与要添加的key值的哈希值是否一样。
若不一样,则添加成功
若一样,则比较两个的内容是否相同,若相同,则覆盖原有数据。若不相同,则添加成功
注意:在不断的添加过程中会涉及到扩容问题,大小为扩容为原来的两倍,并将原有数据复制到扩容后的数组中 jdk8
相对于jdk7在底层方面的不同
- new HashMap() 底层没有创建一个长度为16的数组
- jdk8底层数组是Node[],而非Entry[]
- 首次调用put()方法时,底层创建长度为16的数组
- jdk7底层结构只有数组+链表。jdk8中底层结构:数组+链表+红黑树。当数组的某一个索引位置上的元素以链表形式存在的数据个数大于8且当前数组的长度大于64。此时此索引位置上的所有数据改为使用红黑树存储
jdk8中HashMap源碼解析
当首次创建一个HashMap实例时:HashMap map = new HashMap(); 并不会创建数组
public HashMap() {
// 只会初始化一个负载因子,默认大小为: static final float DEFAULT_LOAD_FACTOR = 0.75f;
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
当调用put方法时会传入如下参数
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
其中hash(key) 便是根据传入的key值计算出哈希值,计算方法如下
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
当进入putVal() 方法时,首先会先声明四个变量:Node<K,V>[] tab; Node<K,V> p; int n, i;接着会进行判断是否需要初始化。代码如下
if ((tab = table) == null || (n = tab.length) == 0){
n = (tab = resize()).length;
}
当条件中两个条件中的任何一个条件满足,就会进行初始化。调用resize()方法进行初始化
table意思:该表在首次使用时初始化,并根据需要调整大小。分配时,长度始终是 2 的幂。我们还在某些操作中允许长度为零,以允许当前不需要的引导机制。
初始化过程在resize()方法中,首先会执行如下过程
newCap = DEFAULT_INITIAL_CAPACITY; // static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 0.75*16=12
然后会把newThr赋值给临界值threshold,接着创建一个长度为16的数组。并把创建好的数组赋值给table,这样在下次执行put方法时,判断不为空,就不会执行初始化部分。
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
最后返回创建出来的数组
return newTab;
返回出去后,会把数组给tab,数组长度赋给n
n = (tab = resize()).length;
然后计算根据哈希值计算数据要存放的位置,若为空,则执行newNode方法添加数据。
if ((p = tab[i = (n - 1) & hash]) == null){
tab[i] = newNode(hash, key, value, null);
}
若计算出来的位置上有数据,则比较此位置上的元素的哈希值与要存入数据的哈希值是否相等。若相等,则进一步比较它们的数据是否相等,若相等,则进行覆盖操作
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))){
e = p;
}
// 执行覆盖操作
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null){
e.value = value;
}
afterNodeAccess(e);
return oldValue;
}
若判断为false,接下来会判断是否为红黑树结构,是的话则执行红黑树的添加操作
else if (p instanceof TreeNode){
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
}
若不是红黑树结构,则会进入如下代码执行
for (int binCount = 0; ; ++binCount) {
// 如果下一个数据为空,则执行添加操作
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
/*
判断是否达到转换为红黑树的条件 static final int TREEIFY_THRESHOLD = 8,。
在 treeifyBin(tab, hash);中还判断了一下数组长度是否大于64.
只有上述两个条件同时达成,才会转换为红黑树。
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); 此方法进行扩容
*/
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)))){
break; //跳出循环
}
p = e; // 把当前链表的指针赋值给p
}
// 若相同数据,则进行替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}