1.HashMap类中的成员变量:
//默认初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16//最大容量1左移30位
static final int MAXIMUM_CAPACITY = 1 << 30;//默认扩容因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;//当一个桶的链表节点>8时,且数组容量>64时,该桶存储结构由链表变为红黑树
static final int TREEIFY_THRESHOLD = 8;
/*当哈希表中的容量大于这个值时,表中的桶才能进行树形化
否则桶内元素太多时会扩容,而不是树形化
为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD*/
static final int MIN_TREEIFY_CAPACITY = 64;//当一个桶的节点数为6时,该桶结构由红黑树变为链表
static final int UNTREEIFY_THRESHOLD = 6;//table用来初始化桶
transient Node<K,V>[] table;
//用来存放缓存
transient Set<Map.Entry<K,V>> entrySet;//存放的元素个数,每存放一个元素 size+1
transient int size;//修改次数
transient int modCount;//扩容边界值:当capacity * loadfactor = threshold
//当 size > threshold时,扩容
int threshold;//加载因子
final float loadFactor;
为什么变为红黑树的节点数量边界值MIN_TREEIFY_CAPACITY是8
因为树(红黑树)节点的大小是普通节点的两倍,所以我们只在一个桶包含足够的节点时才使用树节点(参见TREEIFY_THRESHOLD)。
当它们变得太小(由于删除或调整大小,当红黑树节点小于6则又变为链表)时,就会被转换回普通的桶。
理想情况下随机hashCode算法下所有桶中节点的分布频率会遵循泊松分布,我们可以看到,一个桶中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,选择8。
为什么默认加载因子DEFAULT_LOAD_FACTOR是0.75
兼顾数组(桶)利用率又考虑链表不要太多,经过大量测试0.75是最佳方案。
2.HashMap()无参构造默认初始容量capacity=16,默认加载因子loadfactor=0.75
3.HashMap(int initialCapacity) 初始容量用户定义,默认加载因子loadfactor=0.75
4. HashMap(int initialCapacity, float loadFactor)用户自定义初始容量和加载因子。
如果自己输入的capacity不是二的整数次幂会怎么样?
会将用户输入的capacity转化为二的整数次幂,详情见下面分析
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;
}
cap是自己手动输入的初始容量
| 是按位或操作
int n = cap - 1; //cap=10 n=9
第一次
n |= n >>> 1;
00000000 00000000 00000000 00001001 -->9 //int是4byte长度,32位
|
00000000 00000000 00000000 00000100 -->4右移之后变为4
-----------------------------------------------------
00000000 00000000 00000000 00001101 -->13
同理然后右移2、4、8 、16位并且按位进行或运算最终得到n=15,然后return n+1=16
即把传入的非二进制整数次幂的数转化为距离该数最近的二进制整数次幂,例如:10->16,20->32
为什么要对cap做减1操作,int n = cap -1?
这是为了防止,cap已经是2的幂。如果cap已经是2的幂,又没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍。
5.初始容量capacity为什么要被转化为二的整数次幂?
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
存值时,put函数要根据key.hashCode() ^ (key.hashCode()>>>16)得到hash值,然后hash&(capacity-1)得到index,根据index存入哈希表中的相应位置,这样求出index可以减小索引碰撞,使元素存储均匀。
其实hash&(capacity-1) => hash%capacity,但是考虑到位运算的性能高于取余操作,采取按位与的算法计算index。采用位运算的方式计算index,就需要capacity是二的整数次幂,这样才能达到使元素分配均匀的目的。
6.HashMap计算hash值的方法hash()
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
根据key计算出hash值
如果key==null,return 0;
如果key!=null,h=key.hashCode (), return h ^ h>>>16;
为什么采用这种方式计算hash值,不直接用hashCode结果作为hash值?
hash值的作用是计算索引,计算索引的算法是index=(hash & (capacity-1))
如果直接用hashCode方法的返回值作为hash值,计算完的index很容易冲突。
用hashCode() ^ hashCode()>>>16这种方法计算出的hash值计算出来的index可以减小冲突。
7.HashMap的put()
- 首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;
- 如果数组是空的,则调用 resize 进行初始化;
- 如果没有哈希冲突直接放在对应的数组下标里;
- 如果冲突了,且 key 已经存在,就覆盖掉 value;
- 如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;
- 如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。
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;
// 步骤1:tab为空则创建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步骤2:计算index,并对null做处理
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 步骤3:节点key存在,直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 步骤4:判断该链为红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 步骤5:该链为链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//链表长度大于8转换为红黑树进行处理
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;
// 步骤6:超过最大容量 就扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
// 第31行treeifyBin方法部分代码
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// static final int MIN_TREEIFY_CAPACITY = 64;
// 如果大于8但是数组容量小于64,就进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
}
8.HashMap的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;
}
// 没超过最大值,就扩充为原来的2倍
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);
}
// 计算新的resize上限
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;
if (oldTab != null) {
// 把每个bucket都移动到新的buckets中
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 { // 链表优化重hash的代码块
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;
}
// 原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
JDK1.8做了两处优化:
- resize 之后,元素的位置在原来的位置,或者原来的位置 +oldCap (原来哈希表的长度)。不需要像 JDK1.7 的实现那样重新计算hash ,只需要看看原来的 hash 值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引 + oldCap ”。这个设计非常的巧妙,省去了重新计算 hash 值的时间。
如下图所示,n 为 table 的长度,图(a)表示扩容前的 key1 和 key2 两种 key 确定索引位置的示例,图(b)表示扩容后 key1 和key2 两种 key 确定索引位置的示例,其中 hash1 是 key1 对应的哈希与高位运算结果。
2.当桶的链表插入元素时,采用尾插法
9.HashMap的put()中的treeifyBin()方法
将一个桶的链表转化为红黑树的方法
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
10.问题集锦
为什么在解决 hash(索引) 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?
在桶的元素个数小于8时,链表可以支撑起查询速度。
红黑树进行插入、删除操作时需要左旋、右旋、变色等操作来保持平衡,耗费性能,而且树节点耗费内存空间。
当桶的元素个数大于8时,需要红黑树来支撑查询速度。
不用红黑树,用二叉查找树可以吗?
二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。
11.死链问题
参考链接:https://zhuanlan.zhihu.com/p/362214327