hashmap存储过程
HashMap数组容量
如果new HashMap时没有指定初始容量,则默认数组容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
HashMap 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)
//如果不存在就创建长度为16的数组
n = (tab = resize()).length;
//通过key的hash值与数组最大索引进行位运行,缺点在数组中的位置
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))))
//如果key完成相等,就把运来的位置的值赋值给一个变量
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);
//如果链表的长度大于等于8看是否转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果k完全相同的话就就行值替换
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;
}
------------本次只分享put方法,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;
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
....
....
}
--------------同上 treeifyBin()方法 --------
final void treeifyBin(Node<K, V>[] tab, int hash) {
int n, index;
Node<K, V> e;
//如果当前链表的长度大于8但是数组元素小于64就只进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
....
....
}
位置计算
hash = (h = key.hashCode()) ^ (h >>> 16)
最终位置 = (n - 1) & hash
验证:
假设初始容量为9,两个key的hash值分别为3和2
hash值 3 00000011
length-1 8 00000100 此处为按位异或
-------------------------------------------------------------
00000000 0 索引
经过计算得hash值为3的key在数组中的索引为0
hash 2 00000010
length-1 8 00000100
--------------------------------------------------------------
00000010 0 索引
经过计算得hash值为2的key在数组中的索引为0
这才两个元素就出现hash碰撞肯定是不可取的,接下来看如果数组长度为2的n次幂会怎么样
hash值 3 00000011
length-1 7 00000111 此处为进行按位异或运算:
--------------------------------------------------------------------
00000011 3 索引
hash 2 00000010
length-1 7 00000111
-------------------------------------------------------------------
00000010 2 索引
上例可说明如果数组长度为2的N次幂,和key的hash值经过按位与运算,可均匀的分配到数组上减少hash碰撞
所以若指定容量不是2的n次幂时,底层会换算成比cap大的2的n次幂的最小值
底层是如何实现将指定的数组容量变成2的n次幂的?
//通过按位与和按位异或运算将自定义容量运算成2的n次幂的最小值
//cap为上文中指定的初始容量
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;
}
为什么当链表长度达到8并且数组长度到达64时才会转成红黑树
以下为源码注释
简单描述:
因为树节点的大小大约是普通节点的两倍,所以我们只在箱子包含足够的节点时才使用树节点(参见TREEIFY_THRESHOLD)。当它们变得太小(由于删除或调整大小)时,就会被转换回普通的桶。在使用分布良好的用户hashcode时,很少使用树箱。理想情况下,在随机哈希码下,箱子中节点的频率服从泊松分布
关键点:泊松分布
即链表节点存储到数据的概率如上所述 ,换言之链表长度达到8的概率只有0.00000006,如此设计也是考虑到空间和时间的权衡
为什么链表长度小于6又要转成链表
红黑树的平均查找长度是1og(n),如果长度为8,平均查找长度为1g(8)=3,链表的平均查找长度为n/2,当长度为8时平均查找长度为8/2=4,这才有转换成树的必要,链表长度如果是小于等于6,6/2=3,而1og(6)=2.6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
loadFactor加载因子
是用来衡量 HashMap 满的程度,表示HashMap的疏密程度,影响hash操作到同一个数组位置的概率,计算HashMap的实时加载因子的方法为: size/apaity,而不是占用桶的数量去除以capacity。capacity 是桶的数量,也就是table 的长度length。
oadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。
当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能。,所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免。
ctor的默认值为0.75f是官方给出的一个比较好的临界值。
当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能。,所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免。
同时在HashMap的构造器中可以定制loadFactor。