标签:集合类 键值 迭代 treenode nal 过程 链表 except 使用数组
HashMap的底层使用数组+链表/红黑树实现。
transient Node<K,V>[] table;
这表示HashMap是Node数组构成,其中Node类的实现如下,可以看出这其实就是个链表,链表的每个结点是一个<K,V>映射。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
HashMap的每个下标都存放了一条链表。
/* 常量定义 */
// 初始容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 负载因子,当键值对个数达到DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR会触发resize扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当链表长度大于8,且数组长度大于MIN_TREEIFY_CAPACITY,就会转为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当resize时候发现链表长度小于6时,从红黑树退化为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 在要将链表转为红黑树之前,再进行一次判断,若数组容量小于该值,则用resize扩容,放弃转为红黑树
// 主要是为了在建立Map的初期,放置过多键值对进入同一个数组下标中,而导致不必要的链表->红黑树的转化,此时扩容即可,可有效减少冲突
static final int MIN_TREEIFY_CAPACITY = 64;
/* 变量定义 */
// 键值对的个数
transient int size;
// 键值对的个数大于该值时候,会触发扩容
int threshold;
// 非线程安全的集合类中几乎都有这个变量的影子,每次结构被修改都会更新该值,表示被修改的次数
transient int modCount;
关于modCount的作用见这篇blog
在一个迭代器初始的时候会赋予它调用这个迭代器的对象的modCount,如何在迭代器遍历的过程中,一旦发现这个对象的modCount和迭代器中存储的modCount不一样那就抛异常。
Fail-Fast机制:java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map。
注意初始容量和扩容后的容量都必须是2的次幂,为什么呢?
先看散列方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap的散列方法如上,其实就是将hash值的高16位和低16位异或,我们将马上看到hash在与n - 1相与的时候,高位的信息也被考虑了,能使碰撞的概率减小,散列得更均匀。
在JDK 8中,HashMap的putVal方法中有这么一句
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
关键就是这句(n - 1) & hash
,这行代码是把待插入的结点散列到数组中某个下标中,其中hash就是通过上面的方法的得到的,为待插入Node的key的hash值,n是table的容量即table.length
,2的次幂用二进制表示的话,只有最高位为1,其余为都是0。减去1,刚好就反了过来。比如16的二进制表示为10000,减去1后的二进制表示为01111,除了最高位其余各位都是1,保证了在相与时,可以使得散列值分布得更均匀(因为如果某位为0比如1011,那么结点永远不会被散列到1111这个位置),且当n为2的次幂时候有(n - 1) & hash == hash % n
, 举个例子,比如hash等于6时候,01111和00110相与就是00110,hash等于16时,相与就等于0了,多举几个例子便可以验证这一结论。最后来回答为什么HashMap的容量要始终保持2的次幂
注意table.length是数组的容量,而transient int size
表示存入Map中的键值对数。
int threshold
表示临界值,当键值对的个数大于临界值,就会扩容。threshold的更新是由下面的方法完成的。
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的最小的二次幂数值。比如cap为16,就返回16,cap为17就返回32。
put方法主要由putVal方法实现:
tab[i = (n - 1) & hash]
处新建一个结点;get方法由getNode方法实现:
remove方法的流程大致和get方法类似。
by @sunhaiyu
2018.7.26
标签:集合类 键值 迭代 treenode nal 过程 链表 except 使用数组
原文地址:https://www.cnblogs.com/sun-haiyu/p/9383251.html