涉及数据结构
- 红黑树
- 链表
- 哈希
从CRUD说起
预热知识:
DEFAULT_INITIAL_CAPACITY = 1 << 4
, HashMap默认容量为16(n << m意思是n*2^m)
MAXIMUM_CAPACITY = 1 << 30
最大容量,2^30即10,7374,1824
.
DEFAULT_LOAD_FACTOR = 0.75f
负载因子0.75
,扩容时需要用到
TREEIFY_THRESHOLD = 8
The table, initialized on first use, and resized as necessary. When allocated, length is always a power of two. (We also tolerate length zero in some operations to allow bootstrapping mechanics that are currently not needed.)
首次使用时初始化,必要时进行调整。调整之后的容量通常为2的次幂。(在一些操作中也允许长度为零,以允许当前不需要的自举机制)
Node
存储KV对[] table
The next size value at which to resize (capacity * load factor).
threshold
:因为牵涉到扩容,而map的扩容(即对table进行扩容操作)不是到了存满了才扩,是以容量*负载因子作为临界点进行扩容的。
简单讲下Node
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
看了之后发现什么,这就是个简单的实现了Map.Entry
接口的单链表。
new HashMap()
有的时候,大家写代码可能会不指定初始大小Map
运行map.put("test", "zhangsan");
的时候,首先运行一次resize()
,此时table = new Node[16]
(p = tab[i = (n - 1) & hash]) == null
经过hash与n-1的与运算,找到一个位置(比如4),如果这个位置值为null,那么tab[4]就放上一个Node(图中仅仅以value做区分,但请大家注意,这实际上是Node,有key,有value,有hash,有next,尽管刚开始next是null)
假如此时接着执行了map.put("test", "liss");
会走p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
这个if分支,然后将原来的value替换为liss
但正常情况下我们写业务逻辑,都不会这么写的对吧。
假如继续put("test2", "zhangsan2")
按照这样的方式put下去。插入7条数据之后如下
当执行map.put("test8", "zhangsan8");
时,由于test8的hash与15做运算,得到的位置为4,而4这个位置已经有值了。
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 新的值就挂在上一个的next指针上
p.next = newNode(hash, key, value, null);
// 大于等于7的时候,树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 在找key相等的节点,如果找到了,就替换掉
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
然后就这样很开心的继续put,直到遇见map.put("test13", "zhangsan13");
此时,size > threshold(16 * 0.75 = 12),因此进入resize()
,长度扩为16 * 2 = 32
进入如下逻辑。
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 对于单个Node节点,直接放到原来的位置+n的位置,比如原来在0,则扩容后在16
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node 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;
}
}
}
}
}
如果e.hash在16对应的那个位置是1,那么就放到hiHead那条链表上,位置处于[j + oldCap]处。
resize()方法
调整map的大小,实现map初始化或扩展为原有尺寸的二倍。
HashMap默认大小16,当存储了12个的时候,如果再put,会先将新的值存于原来的map中,然后发现++size > threshold(这里是12),就会进行调整resize。调整的时候,新的table会变成32,threshold变成24,并且需要将原有的值复制进新的table中。
int hash(Object key)
key == null,hash为0;否则,(h = key.hashCode()) ^ (h >>> 16)即计算hashCode,与hashCode右移16位(除以65536得到的商)做异或(同0异1)运算。
计算单个字符的hash结果就是对应的ASCII码,例如hash('a')=97
V put(K key, V value)
put是允许key为null的