1.成员变量、树化阈值
DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16(桶的个数)
DEFAULT_LOAD_FACTOR = 0.75f(负载因子)
TREEIFY_THRESHOLD = 8;(树化阈值)
MIN_TREEIFY_CAPACITY = 64(树化要求的最少哈希表元素数量)
UNTREEIFY_THRESHOLD = 6;(解除树化阈值,resize阶段)
树化总结:当桶中链表元素个数超过8,并且哈希表中所有元素个数超过64,此时会将桶中链表转为红黑树结构。否则(只是
链表元素个数超过8),只是简单地进行扩容操作而已。树化针对符合条件的桶,只有桶中链表的元素超过8,并且哈希表中
元素个数超过64,该哈希桶才会树化,并不是所有的桶都树化。
树化的好处:
1)便于查找 链表:T(n)=O(n) 二叉树T(n)=O(log(n))
2)安全问题 黑客服务:哈希碰撞拒绝服务。一直调用HashMap的put(),以前的HashMap会一直在桶后面+++,就会造成大量的
哈希都碰撞在同一个位置上,导致CPU占用率达到100%,整个服务器挂掉。面试回答查找快即可,不用答哈希碰撞(避免面试官误以为懂网络安全)。
为什么要树化?
当桶中链表长度太长会大大影响查找速度,因此将其树化来提高指定节点的速度。
为什么要使用红黑树?结合红黑树的特点回答
2.初始化策略–懒加载(同ArrayList)
HashMap采用lazy-load策略(当第一次使用put()时,才会将哈希表初始化。)
//无参构造
public HashMap() {
//仅仅将负载因子赋值,默认为0.75.
//哈希表并未初始化
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//有参构造一
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//有参构造二
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);
}
注意:要求初始化容量必须为2的n次方,若通过构造方法传入一个非2^n数值,HashMap会在内部调用tableSizeFor返回一个
距离最近的2^n数值。eg:传15,返回16;传31,返32;传100,返回128.
3.put方法
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)
//调用resize()进行哈希表的初始化操作 初始容量n=16
n = (tab = resize()).length;
//i = (n - 1) & hash计算桶的下标 若n恰好为2^n,此时上述运算刚好就是hash%(n-1)
//(n - 1) & hash 使用位运算代替取模运算提高分桶速度
if ((p = tab[i = (n - 1) & hash]) == null)//当前桶中元素为空
//将要保存的元素作为桶的头结点保存
tab[i] = newNode(hash, key, value, null);
//哈希表已经初始化,并且分得的桶不为空。
else {
Node<K,V> e; K k;
//当要保存的节点的key值与桶中节点的key值相同,直接替换首节点。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//情况:map.put(null,1) map.put(null,2)(此时就是该情况)
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;
}
//链表中存在相同key,替换其value。
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;
//添加节点后,整个HashMap的元素个数若要超过容量时,调用resize()进行扩容工作。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put方法流程:
1.若HashMap未初始化,调用resize()进行初始化操作。
2.对key值Hash取得要存储的桶下标
1)若桶中为空,将节点直接作为桶的头结点保存
2)若桶中不为空
a.若树化,使用树的方式添加新节点。
b.将新节点以链表形式尾插到最后。
-添加元素后,链表的个数binCount>=树化阈值-1,尝试进行树化操作。
3)若桶中存在相同key节点,替换value值。
3.添加元素后计算整个哈希表大小,若超过threshold(容量*负载因子),进行resize()扩容操作。
结论:16*0.75=12,故所有桶的数量超过12,桶就扩容了。
4.get方法
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//哈希表已经初始化并且分得桶的头节点不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//要查找的节点key值恰好等于桶中头节点的key值,直接返回头节点。
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//进行桶的遍历操作
if ((e = first.next) != null) {
//若树化,使用树的方式查找节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//还未树化,使用链表方式从前向后遍历节点
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
get方法流程:
1.若哈希表已经初始化,并且桶的首节点不为空。
1)查找节点的key值恰好等于首节点,直接返回首节点。
2)进行桶元素的遍历,查找指定节点
a.若树化,按照树的方式查找。
b.按照链表方式查找。
2.哈希表为空或桶的首节点为null,直接返回null。
5.resize方法
final Node<K,V>[] resize() {
//当前表是否初始化
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//哈希表不为空
if (oldCap > 0) {
//已达到最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//将哈希表double扩容
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 threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = 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"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将哈希表指向扩容后的新表
table = newTab;
//将原表中的元素移动到新表
//策略:原来的元素要么待在原桶中,要么移动到double size的桶下。
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)//如果树化,按照树的方式移动
//调用树的方式进行元素移动
//若在移动过程中,发现红黑树节点<=解树化阈值,会调用untreeif方法
//将红黑树解为链表
((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;
}
}
}
}
}
return newTab;
}
扩容resize流程:
1.判断哈希表是否初始化,若还未初始化,根据InitCapcity值进行初始化操作
2.若表已初始化,将原哈希表按照 2倍方式扩容
3.扩容后进行原表元素的移动
1)若桶中元素节点已经树化,调用树的方式移动元素
(若在移动过程中发现红黑树节点<=6,会将红黑树解除树化,还原为链表)
2)若未树化,调用链表的方式来移动元素。
6.性能问题及解决
1.多线程场景下,由于条件竞争,很容易造成死锁。(使用ConcurrentHashMap代替)
2.rehash是一个比较耗时的过程。(在能预估存储元素个数的前提下,尽量自定义初始化容量,尽量减少resize过程)。负载因子可以控制resize频率:负载因子较小,扩容频繁;负载因子较大,增加碰撞几率。因此负载因子尽量不要改。
7.哈希算法
//返回高低16位共同参与运算的哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
///h>>>16:将h无符号右移16位,实际上,就是将高16位移到低16位,然后和原来的所有数进行异或运算。
问题:为何不直接采用Object类的hashCode()返回桶下标:因为Object类的hashCode()几乎不会发生碰撞,需要的桶个数太多。
为什么容量必须为2的n次方?
用位运算替代数学取模运算,提高速度。
HashMap允许key和value为空,如果传进来的key为空,则放入第一个桶中,否则计算它的哈希码。