底层结构:
hashMap的底层结构主要包括数组和链表,以及从jdk1.8开始引入红黑树。具体来说:
- JDK 1.7及之前:HashMap的基础结构是数组和链表,采用头插法进行元素的添加和删除操作。
- JDK 1.8及以后:由于数组和链表在某些情况下(如大量元素)可能效率较低,因此从JDK 1.8开始,HashMap引入了红黑树作为其底层结构的补充。当数组长度超过64字节且链表长度大于8时,链表会被转换为红黑树以提高查找效率。
核心内容(常量变量)
核心常量释义:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
- DEFAULT_INITIAL_CAPACITY:默认初始容量
- MAXIMUM_CAPACITY:table最大长度
- DEFAULT_LOAD_FACTOR:加载因子
- TREEIFY_THRESHOLD:树化阈值
- UNTREEIFY_THRESHOLD:树降级链表阈值
- MIN_TREEIFY_CAPACITY:树化的另一个参数,数组阈值
核心变量释义:
transient Node<K,V>[] table;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
- table:哈希表 (一个Node<K,V>类型的数组)
- size:当前哈希表中的元素个数
- modCount:当前哈希表结构修改次数(替换不算)
- threshold:扩容阈值(当哈希表里的元素超过阈值时,会触发扩容方法)
- loadFactor:加载因子
构造方法分析
构造方法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);
}
通过传入指定的table长度与指定的加载因子创建HashMap
前三个if为参数校验,capacity必须大于0,最大也就是常量MAXIMUM_CAPACITY(1<<30)
this.loadFactor = loadFactor,即将指定的加载因子参数赋值给loadFactor
this.threshold = tableSizeFor(iniitialCapacity)调用tableSizeFor方法将构造方法指定的通过计算得到2的幂次数的值赋值给threshold(table初始化的长度要求用2的幂次数)
tableSizeFor方法分析
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;
}
作用:返回一个大于等于当前capacity值的一个数组,且这个数字一定是一个2的幂次数
该算法让最高位的1后面的位全变为1。最后再让结果n+1,即得到了2的整数次幂的值了。
让cap-1
再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。通过一系列位运算提高效率。
构造方法2.
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
通过传入指定table长度和默认的加载因子(0.75f)调用构造方法1.
构造方法3.
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
无参构造,将加载因子设置为默认常量值0.75f
put方法分析
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
调用了hash方法,扰动hashCode值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
扰动函数hash()的作用:让key的hash值的高16位也参与路由运算
key如果为null,那么hash值为0(由于哈希值决定着元素在table数组中的位置,如果key是null的话,它会位于table的首位)
最终通过将key的hashCode值与右移十六位后的hashCode值进行异或运算扰动后得到的int类型值返回
至此,我们可以看到put方法的内部实际上是调用了putVal方法
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)
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 1st
treeifyBin(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 key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
逐个分析
Node<K,V>[] tab; Node<K,V> p; int n, i;
tab:引用当前hashMap的散列表
p:表示当前散列表的元素
n:表示散列表数组的长度
i:表示寻址结果
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中的table
(tab = table) == null 将table赋值给tab,如果是null的话,说明hashMap里的table没有初始化,tab.length = 0 也是说明table没有初始化(在我们使用无参创建hashMap时,table就是没有初始化的)
n = (tab = resize() . length) 在第一次往hashMap中插入元素时,便会调用resize方法并初始化table
情况1.
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
路由算法,假如目前的表长度为16,那么将16-1再&哈希值便会找到一个该值在table数组中要存放的位置
比如说计算出的值为 5 ,那么我们就会先判断tab[ 5 ] 的位置是否为null,如果为null那么就会直接添加进tab中
情况2.
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 1st
treeifyBin(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 key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
Node<K,V> e; K k;
e:不为null的话,找到一个与当前要插入的key一致的元素
k:一个临时的key
这时,要添加的tab数组的这个位置不为null,也就是已经有元素存在了
情况2.1
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
p.hash == hash 当前tab中的该位置的元素哈希值与要插入元素的key的哈希值相同,
并且,key做==判断和equals判断也为true,即说明要插入的元素与tab中的该元素是完全一致的,那么后续会进行一个替换操作(位于末尾的代码)
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
情况2.2
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
如果他们是不相同的元素,则会判断该位置是否已经树化为红黑树,如果已经树化了,则执行putTreeVal方法在红黑树中添加该元素
情况2.3
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 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
大致意思:此时,tab数组要插入的位置不为null,则在该位置的链表上开始比较,如果在链表上有元素是和要插入的元素相同的,则会发生替换操作,否则说明要插入的元素在整个hashMap中是唯一的,那么就会将它添加在链表的末尾处
for循环用于遍历这个链表,
情况2.3.1
if ((e = p.next) == null) 当这条判断语句为true时,说明已经遍历到了链表的尾部,并且没能在链表上找到与之相同的元素,则将要插入的元素新增在链表的尾部 p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1),当我们在链表尾部添加了元素后,这条语句会检查该链表是否已经达到了树化的阈值(8),如果达到了阈值,则会调用treeifyBin方法进行树化
情况2.3.2
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
这里的条件判断表示,在链表迭代时,找到了与要插入的元素相同的元素相同(哈希值相同,key值一样或equals方法得true) ,那么将链表上该位置的元素替换为要插入的元素
扩容方法(resize)分析
原因
为了解决哈希冲突导致的链化影响查询效率问题,扩容可以缓解这个问题
源码分析
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
oldTab:表示扩容前的哈希表
oldCap:表示扩容前table数组的长度
oldThr:表示扩容前的扩容阈值,触发本次扩容的阈值
newCap:扩容之后table数组的大小
newThr:扩容之后下次再触发扩容的条件
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
}
if (oldCap > 0)条件如果成立说明,hashMap中的散列表已经初始化过了,是一次正常的扩容
if oldCap >= MAXIMUM_CAPACITY:如果扩容前的table数组大小已经达到了最大阈值,则不再扩容,且设置下次扩容条件为int最大值
newCap通过oldCap左移一位获得,即oldCap乘以2,newCap小于数组最大值限制(1<<30)且扩容前的阈值>=16,这种情况下,则下次扩容的阈值等于当前阈值*2
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
这种情况一般用于传入指定长度构造方法时使用,初始化table,这里的oldThr即tableSizeFor方法计算出来的值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
此时oldCap ==0,oldThr ==0
也就是调用空参的构造方法时会使用这里的newCap和newThr作为table的长度与扩容阈值
此时newCap为16,newThr为16*0.75 = 12
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
newThr为0时,通过newCap与loadFactor去计算出一个newThr
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
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 order
Node<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;
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[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
if (oldTab != null):说明hashMap本次扩容前,table不为null,也就是table已经指向了一个数组了
在for循环里,首先定义了Node<K,V> e 用来表示当前node节点
if ((e = oldTab[j]) != null)
即说明当前的table数组位置中有元素,但具体是单个数据还是链表或者红黑树还并不知道
第一种情况:当前数组此下标下只有一个元素,没有发生过哈希冲突
if (e.next == null)
此时直接计算出当前元素应该存放在新数组的位置(哈希值&数组长度-1)然后放进去就可以了
第二种情况:当前数组此下标下已经树化
else if (e instanceof TreeNode)
第三种情况:该位置是链表结构
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;
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[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
扩容后,会将链表分向数组其他位置,低位链表:存在扩容后的数组下标的位置,与当前数组下标位置一直,高位链表:存放在扩容之后的数组下标位置为 当前数组下标位置 + 扩容前数组的长度