常量
// 默认初始化容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链 转 tree 的 节点个数 下限阈值
static final int TREEIFY_THRESHOLD = 8;
// tree 转 链 的 节点个数 上限阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 链 转 tree 时 存储数组table的容量下限阈值. table.length小于此值时 resize()扩容。
static final int MIN_TREEIFY_CAPACITY = 64;
变量
// Node存储数组,在resize方法中初始化或扩容. 长度一定是 2的次方!
transient Node<K,V>[] table;
// 内部类 EntrySet,值对缓存
transient Set<Map.Entry<K,V>> entrySet;
// table中Node的数量
transient int size;
// 结构更改的次数。与AbstractList类似 (See ConcurrentModificationException)
transient int modCount;
// 下次需扩容size阈值: capacity * loadFactor, 或 外部指定initCap时tableSizeFor方法计算出的初始容量
int threshold;
// 加载因子 用于确定threshold
final float loadFactor;
loadFactor
加载因子表示hash表中元素的填满的程度, 默认是0.75。因子越大,填满的元素越多, 好处是:空间利用率高了, 但冲突的机会加大了;因子越小,则反之。
冲突的机会越大带来查找成本越大,所以需要在二者间寻求平衡。
threshold
构造函数指定initialCapacity时,通过tableSizeFor方法计算出的初始容量值;
其他时候表示下次需扩容时变量size的阈值threshold = capacity * loadFactor
构造函数
只是确定好几个成员变量的初值,并不实例化table。真正的实例化是在resize方法中
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);
}
这里有个重要的方法 tableSizeFor,保证table的初始容量是2的次方
n |= n >>> 1 : 先计算>>>, 按位或后赋值. 等价于 n = n | (n >>> 1)
// 返回 2 的 次方
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;
}
外部指定initialCapacity时,该方法返回 >= initialCapacity 最接近的 2的次方.
Node
链表结构下的值对存储对象,保存key在hash方法得到的hash值,并链接下一个Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
...
TreeNode
红黑树 结构下的存储对象,继承自LinkedHashMap.Entry 依然可以维护双向链表的结构。超类依然是 HashMap.Node
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
...
}
static class Entry<K, V> extends HashMap.Node<K, V> {
Entry<K, V> before, after;
Entry(int hash, K key, V value, Node<K, V> next) {
super(hash, key, value, next);
}
}
树化
链表操作O(n)随N的增长而性能愈差,jdk8将达到阈值的链转换为树。
putVal
调用入口 put 、merge、compute 等方法。
在链表末尾新增节点后,判定: 链长度 > TREEIFY_THRESHOLD 时,执行treeifyBin方法:
putVal -> treeifyBin
判定 table.length < MIN_TREEIFY_CAPACITY :
true 则 resize()扩容 ; false 则 转换为树结构 -> treeify方法。
resize
实例化table
-
putVal 方法判定table为空 则 执行resize()来初始实例化
-
putVal 方法执行结束前,判定 size > threshold 执行 resize()扩容
resize -> split
判定table[index]是TreeNode,执行split方法来处理重定位时TreeNode是否需要树转链。
区分好移动与否的节点集合后:因一定 由 index 移动到 index+ oldCap,
所以直接判定各自节点数量 <= UNTREEIFY_THRESHOLD: 转为链结构 -> untreeify方法。
重定位
put操作对于链表是 后插入。
在低版本中,resize()重定位操作移动到同一新index下的Node链是 前插入。并发下,原链 A->B->nil 对于错误线程可能演变为循环链 A->B->A。
在JDK8中优化了重定位方法来保证移动后节点在链表中的相对先后顺序不变。
(node.hash & oldCap) == 0 则index不变;否则在新table上移动:newIndex = oldIndex + oldCap。
推演
resize():若需要移动,一定是由 index 移到 index + oldCap;
换而言之:table[index+ oldCap] 上的节点一定是由index移动而来。
前提:
-
table.length 一定是 2 的次方。
(默认是 1 << 4 ; 指定initCap则经过 tableSizeFor处理,保证是2的次方) -
table扩容大小翻倍: newCap = oldCap << 1 左移1位
-
定位:index = node.hash & ( cap -1 )
oldCap = 16
newCap = oldCap << 1 = 32
旧下标位置: e.hash & (oldCap-1) :
eg1:hash 二进制值
e.hash = 10 0000 1010
oldCap-1 = 15 0000 1111
& = 10 0000 1010
eg2:hash 二进制值
e.hash = 17 0001 0001
oldCap-1 = 15 0000 1111
& = 1 0000 0001
比较判定Node在新table的位置是否需要移动: e.hash & oldCap
eg1:hash 二进制值
e.hash = 10 0000 1010
oldCap = 16 0001 0000
& = 0 0000 0000 为0
eg2:hash 二进制值
e.hash = 17 0001 0001
oldCap = 16 0001 0000
& = 1 0001 0000 不为0
新下标位置: e.hash & (newCap-1)
eg1: hash 二进制值
e.hash = 10 0000 1010
newCap-1 = 31 0001 1111
& = 10 0000 1010
结论:下标不变
eg1: hash 二进制值
e.hash = 17 0001 0001
newCap-1 = 31 0001 1111
& = 17 0001 0001 oldIndex + oldCap = 1 + 16
结论:元素位置在扩容后数组中的位置发生了改变,新的下标位置是原下标位置+原数组长度
在上例中:
oldCap = 16 0001 0000
newCap = 32 0010 0000 oldCap左移1位,末尾补0
(oldCap - 1) = 15 0000 1111
(newCap - 1) = 31 0001 1111 (oldCap - 1) 左移1位,末尾补1
(newCap - 1) 与 (oldCap - 1) 二者差别在最高位:(oldCap - 1)是 0 , (newCap -1) 是 1 。
所以:
hash & ( oldCap - 1) 与 hash & (newCap -1) 不同之处在最高位;余下位的值是相同的,正好对应oldIndex值 ;
加之:
(newCap -1) 与 oldCap 的 相同之处是 最高位都是1 。且 oldCap 除高位外余下位数固定是 0;
所以 :
(hash & oldCap)运算后只会在oldCap的最高位上结果不同,其余位(即使hash位数大于oldCap位数) "因oldCap除高位外余下位数都是0 " 而为0。
由此推出:newIndex = oldIndex + 最高位&运算结果值 !
oldCap 的最高位是 1 ,所以取决于hash值在oldCap最高位上的数值
最高位是 0 则 不变 -> newIndex = oldIndex;
最高位是 1 则 移动 -> newIndex = oldIndex + oldCap 。
(非2的次方, 除最高位后余下位不一定是0。所以 (hash & oldCap)运算后,不仅最高位的结果会不同,余下位的结果也可能不同。无法推出结论等式,不成立)
来源:oschina
链接:https://my.oschina.net/u/3434392/blog/3186685