目录
3.1、DEFAULT_INITIAL_CAPACITY(默认桶数组大小)
3.2、DEFAULT_LOAD_FACTOR(默认负载因子大小)
3.4、MIN_TREEIFY_CAPACITY(哈希表的最小树形化容量)
3.5、TREEIFY_THRESHOLD(一个桶的树化阈值)
3.6、UNTREEIFY_THRESHOLD(一个树的链表还原阈值)
5.1、hash() 和【(n-1)& hash】 - hash相关
5.7、modCount 和 ConcurrentModificationException
6.2、HashMapSpliterator(内部类分割迭代器 )
8.2、ConcurrentHashMap CAS 和 分段锁
9.2、JDK8 ConcurrentHashMap CPU 100%
1、前言
在开始之前我们需要思考一件事情,大部分人在关注HashMap会把核心重点放在里面的红黑树,或者树相关的算法上面,红黑树固然很高效也很有效果,但是是否有人真正思考过,红黑树是否位HashMap的核心呢?HashMap高效是否真的体现在红黑树上面呢?在JDK1.8之前,HashMap没有使用红黑树,那么它又是怎么实现高效的能?还有红黑树真的是用来提高查询效率的吗?换句话说,红黑树的插入查询效率真的比Hash的走类似于索引的方式更加快吗?本次文章将会针对HashMap中的各种常量,各种方法来说明HashMap为什么高效,以及为什么HashMap要使用红黑树,解读负载因子等内容,同时来满足大家的好奇心。同时本次阅读源码的时候,希望大家能多阅读代码上方的注释,很多时候精华就在注释上面,在开始之前希望大家能先去了解一下位运算,比如左移和右移,逻辑右移和算术右移等等。
在开始前我们首先介绍一下HashMap的几个作者:
Doug Lea (Java并发之父)、Josh Bloch 、Arthur van Hoff、Neal Gafter
我们可以简单看一下HashMap类的依赖关系
从图中可以看出ConcurrentHashMap 和 HashMap都是单独实现的,并不存在继承光系,或者可以这么说ConcurrentHashMap相当于用并发手段重新实现了一次HashMap
基本上整个HashMap的结构是这样的
HashMap核心数据结构-Node数组(大小一定是2的幂)
/**
* 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.)
*/
transient Node<K,V>[] table;
时间复杂度
红黑树查询:O(log(N))
链表查询:O(n)
2、什么是Hash
Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数(来源百度百科)
换句话说,就是把一堆数据,变成一段定长的映射,这段定长由程序来控制决定
3、常量
3.1、DEFAULT_INITIAL_CAPACITY(默认桶数组大小)
3.2、DEFAULT_LOAD_FACTOR(默认负载因子大小)
3.3、MAXIMUM_CAPACITY(最大桶容量)
3.4、MIN_TREEIFY_CAPACITY(哈希表的最小树形化容量)
当哈希表中的容量大于这个值时,表中的桶才能进行树形化,否则桶内元素过多时只会扩容,而不是树形化,为了避免由于,扩容、树形化,选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD(32)
(当HashMap还很小的时候,这时候table占用的空间也不多,扩容的方式比树化更加节约时间,当容量大于64的时候,扩容的成本会变的越来越高,这个时候树化比扩容更加合适个人推断)
3.5、TREEIFY_THRESHOLD(一个桶的树化阈值)
遵从泊松分布里面的结果,当正常hash分布(满足泊松分布),一个桶里面有8个节点的概率很低0.00000006,所以达到这个值进行树化,避免高概率多个table进行树化导致的大开销 (在后面的泊松分布会详细说明)
3.6、UNTREEIFY_THRESHOLD(一个树的链表还原阈值)
3.7、常量总结
常量名称 |
常量值 |
描述 |
DEFAULT_INITIAL_CAPACITY |
1<<4 (16) |
默认初始容量-必须是2的幂,如果在构造的时候没有传入,默认就是16。后续会通过hash桶的负载情况进行扩容 |
DEFAULT_LOAD_FACTOR |
0.75f(负载因子值的范围 0.25f ~ 4.0f)
|
默认负载因子,这个数值是通过线下大量数据计算得来的,可以说和泊松分布有关,也可以说无关,后续的自动扩容和这个有关 |
MAXIMUM_CAPACITY |
1 << 30 (1073741824) |
Hash的最大值,这个数值基本上是java里面 int的最大值,当大于这个值的时候,HashMap的桶不再扩容,新进来的数据将会发生hash碰撞 |
MIN_TREEIFY_CAPACITY |
64 (4*16) |
可以将箱子树状化的最小表容量(否则,如果bin中的节点太多,就会调整表的大小。)应至少为4 * TREEIFY_THRESHOLD以避免冲突调整大小和树化阈值之间(因为默认初始值大小是16)。 |
TREEIFY_THRESHOLD |
8 |
每个桶中,节点转换位树节点的最小值(链表转化为树节点的阈值) |
UNTREEIFY_THRESHOLD |
6 |
由树转换位链表的阈值resize()中触发 |
4、内部类,构造方法
4.1、Node
Node的基本结构
Node本身是HashMap中bucket(桶的基本结构),本质是一个单向链表,但是HashMap的桶不本身是以数组的形式存在的
1、hash(final修饰) 通过HashMap hash 方法计算出来的结果
2、key (final修饰) 键
3、val 值(volatile修饰)
4、next 下一个节点引用(volatile修饰)
4.2、TreeNode
HashMap在JDK7的时候是单向链表,在JDK8的时候由于引入了TreeNode变相的使一部分的节点变成了双向链表(树节点)
1、继承于LinkedHashMap的Entry(内有before,after节点)
2、继承于Node(所以树节点本身也算是Node)
3、parent 父亲节点
4、left 左节点
5、right 右节点
6、prev Node节点前一个节点(双向链表)
7、red 是否为红色
4.3、部分成员属性
成员名称 | 属性描述 |
loadFactor(final修饰) | 当前HashMap对象的负载因子,HashMap一旦创建,负载因子就不能再变了 |
modCount(transient 修饰) | 当前HashMap的修改次数,官方叫做快速失败机制(我习惯称为乐观锁) |
size(transient 修饰) | 当前HashMap的容量 |
table(transient 修饰) | 当前HashMap的table数组 |
threshold | 当前HashMap自动扩容的阈值 |
4.4、构造方法
HashMap()
// 给默认值负载因子是0.75f
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
HashMap(int initialCapacity)
//指定容量大小,并使用默认的负载因子0.75f
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
HashMap(int initialCapacity, float loadFactor)
//指定HashMap的容量和负载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//保证table容量不会超过int最大正整数
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);
}
HashMap(Map<? extends K, ? extends V> m)
//将另一个Map放入当前map,使用默认负载因子
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
//这个方法一开始看也觉得奇怪,但是后来发现HashMap是可以被继承,并且修改的,也就想明白了
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // 如果当前table没有初始化,通过目标HashMap/0.75f 计算默认table大小
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//为了保证table数量是2的幂
if (t > threshold)
threshold = tableSizeFor(t);
}
//当原HashMap不为空的时候采取自动扩容的方式
else if (s > threshold)
resize();
//通过遍历将目标Map的数据转入当前Map
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
5、内部实现优选
5.1、hash() 和【(n-1)& hash】 - hash相关
右移16位,让高位和低位进行异或运算
因为,table的长度都是2的幂,因此index仅与hash值的低n位有关,hash值的高位都被与操作置为0了。
//hash 和 table 定位的计算方式
n = table.length;
index = (n-1) & hash;
文章参考:https://www.cnblogs.com/liujinhong/p/6576543.html(图片来源)
5.2、tableSizeFor() 计算最小2的幂
/**
* Returns a power of two size for the given target capacity.
* 返回给定目标容量的两个大小的幂。
* 这个方法巧妙的使用了位运算,返回当前 int的大于本身的最小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;
}
如下假入输入的是7,7减1后是6
右移一位做或运算
右移两位做或运算
...............
右移16位做或运算结果
结果做 + 1 操作
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
得值是:8 (大于7最小的2的幂)
为什么右移16位后就结束呢?按照java中对int 的定义是32位bit,1+2+4+6+8+16 = 31 ,int 31位刚好是2147483647 刚好是java里面 int正数的最大值,从上面这个操作也看出来,HashMap table最大的大小也就是 int正整数的最大值了
那么为什么一开始要减一,最后在加一呢?
int n = cap - 1;
.........
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
这里是一个技巧,从位运算可以看出来,如果不减一,那么上面运算的结果就是16(显然不是结果),这么做的目的就是保证结果是大于 cap的最小2次幂
5.3、treeifyBin()-HashMap树化
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();
//获取当前hash 所在的table的index
else if ((e = tab[index = (n - 1) & hash]) != null) {
// hd 头节点 ,tl temp 遍历使用
TreeNode<K,V> hd = null, tl = null;
do {
//将table节点替换位treenode
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);
}
}
对table树化
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
//遍历将这个节点下的Node全部转换为红黑树节点
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//红黑树的方式插入
root = balanceInsertion(root, x);
break;
}
}
}
}
//返回到根节点
moveRootToFront(tab, root);
}
红黑树方式插入
//这里建议可以直接去参照红黑树的实现,基本上是一致的
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
//先设置节点为红色
x.red = true;
//xp (父亲) xpp(爷爷) xppl(爷爷左) xppr(爷爷右)
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//如果是父亲节点,为黑色
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//爷爷节点操作
else if (!xp.red || (xpp = xp.parent) == null)
return root;
if (xp == (xppl = xpp.left)) {
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.right) {
//左旋
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
//右旋
root = rotateRight(root, xpp);
}
}
}
}
else {
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
//右旋
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
//左旋
root = rotateLeft(root, xpp);
}
}
}
}
}
}
5.4、万恶之源resize()-自动扩容
开始之前我们先来了解一下resize()方法的执行方式:
这里说明一点,在JDK7的时候,执行resize()会对每个Node进行rehash操作,但是到JDK8使用异或和红黑树后,resize()是不需要重新计算hash值,除此之外,JDK7会在构造的时候初始化table数组,但是在JDK8中,是通过放入数据的时候,调用resize()根据当前真实的数据量进行table初始化的
final Node<K,V>[] resize() {
//获得当前HashMap的table
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取自动扩容阈值
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//判断旧的HashMap的table容量是否达到最大值(如果是下一次扩容只能拓展为int的最大值)
if (oldCap >= MAXIMUM_CAPACITY) {
//设置当前threshold临界值为 int最大值
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果没有达到最大值,默认扩大2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//如果老的数组容量为0,而且老的阈值大于0,则新的容量=老的阈值
else if (oldThr > 0) // initial capacity was placed in threshold
//旧的阈值就是新的table的大小(例如从 16 扩容后到 32 ,32就是16那时候的阈值)
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//计算阈值,使用默认负载因子0.75f
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新的阈值为0,为新的阈值赋值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//初始化新table
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//循环遍历,将旧的数据重新放到新的桶里面
if (oldTab != null) {
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 { // preserve order
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;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
//设置当前table
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
//设置当前table
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
从这里可以看出resize()是一个开销很大的方法,所以开发的时候要进可能避免触发自动扩容,阿里巴巴开发手册里面也有对此说明:
调用HashMap#resize()并且每次遍历的数据量(无论是内存空间,还是性能上都是一种消耗)
第一次 16
第二次 32
第三次 64
第四次 128
第五次 256
第六次 512
第七次 1024
第八次 2048
第九次 4096
5.5、putVal()-存放
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//获取当前table
if ((tab = table) == null || (n = tab.length) == 0)
//如果table为空,初始化table
n = (tab = resize()).length;
//计算下表,找到相应的table位置
if ((p = tab[i = (n - 1) & hash]) == null)
//如果找到的table为空,对那个table初始化
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))))
e = p;
//table 是树节点,树化
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//计算当前table的list大小,如果超过8就进行树化(里面有判断,如果当前HashMap小于64,只是扩容)
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
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;
//LinkedHashMap用来控制顺序的
afterNodeAccess(e);
return oldValue;
}
}
//增加修改次数
++modCount;
if (++size > threshold)
//触发自动扩容机制
resize();
afterNodeInsertion(evict);
return null;
}
5.6、getNode()获取
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
//定位节点所在的table位置
(first = tab[(n - 1) & hash]) != null) {
//快速获取(如果当前table 就是目标,直接返回)
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//如果当前table是树(通过树的方式查找)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
5.7、modCount 和 ConcurrentModificationException
了解Doug Lea 的人都知道他喜欢做一个快速失败机制的东西,比如AbstractList里面就有modCount的东西,当然HashMap也使用上了。modCount意思就是,当前对象被修改的次数
当集合或者容器被迭代器遍历的时候Iterator,如果发现集合本身被修改了,那么就会快速终止遍历,从而保证数据一致性,比如HashIterator中,从创建Iterator 的时候就会获取一次modCount
在后面方法的时候每次操作都会去判断这个值,比如Iterator中的remove()方法,和nextNode()方法
6、迭代器
6.1、HashIterator(内部迭代器)
HashIterator是HashMap的内部迭代器,HashMap中的各个迭代器都是继承于它内部的结构是一个单向链表
1、next 下一个节点
2、current当前节点
3、expectedModCount HashMap修改次数(别人都叫快速失败机制,我更喜欢叫乐观锁,文章后面会说明)
4、index当前位置
HashIterator构造
HashIterator() {
//记录当前HashMap的修改次数
expectedModCount = modCount;
//获取当前HashMap的所有桶
Node<K,V>[] t = table;
current = next = null;
index = 0;
//遍历把table 放入链表(注意这里是table不是Node)
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
从这句可以看出来
expectedModCount = modCount;
在生成迭代器的时候,modCount 是赋值给了expectedModCount ,在迭代器中的操作是对expectedModCount 修改的,所以想必后续如果在HashMap的Iterator中对元素进行操作,很有可能会导致modCount和expectedModCount不一致的情况(ArrayList也有这种情况)
nextNode()方法
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
//乐观锁,传说中的快速失败机制
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//如果当前桶没有遍历结束,会使用next继续返回下一个Nodex
if ((next = (current = e).next) == null && (t = table) != null) {
//上一个table遍历结束,换下一个table(桶)
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
6.2、HashMapSpliterator(内部类分割迭代器 )
6.2.1、EntrySpliterator
6.2.2、KeySpliterator
6.2.3、ValueSpliterator
7、树和泊松分布
很多时候在阅读HashMap源码的时候,大家都忽略了这个点,认为每个table就应该是8的时候树化,负载因子就应该是0.75f,但是有没有想过为什么是这些值呢?下面就和大家聊聊HashMap里面和泊松分布有关的内容(切记这些常量是测量出来的值,但并不是和泊松分布有关)
这句话的意思是:
由于树节点的空间大小是普通节点的两倍,当一个桶中的元素个数满足阈值的时候才会使用树节点(TREEIFY_THRESHOLD)。当桶中的元素变化的时候(比如元素删除,或者大小调整),会从树变成普通模式,在使用分布良好的hash码的时候,树节点将会很少被使用。
7.1、引入红黑树的传说
在设计HashMap的时候,要尽可能的避免使用TreeNode 也就是除非万不得已的时候,不会使用红黑树,为什么呢?这里有一个传说。相传低版本的HashMap某次被黑客DDOS攻击导致服务器瘫痪就是由于HashMap分布不均导致的
从图中可以看出,被攻击的HashMap中的某个链表是可以无限长的,如果这个时候有代码访问使用这个HashMap,会使DDOS攻击的效果倍增,瞬间让整个服务器进入瘫痪状态,所以传说后来在JDK8中引入了红黑树
7.2、破坏HashMap的分布
秉承着,不怕事情搞大的原则,为了保证更好的使用HashMap,我们必然会去想什么情况下或者怎么样才能破坏HashMap的分布结构呢?首先我们回过头看看Hash的在计算数组下标时候使用的方式:
n = (tab = resize()).length
........
tab[i = (n - 1) & hash])
从中可以很明细看出,是使用Hash和table大小做并(&)运算,假设我们都知道并运算的特点,那么只要我们能保证构造的一批Object的Hash值通过&都能分配到同一个index的table里面就行了,然后我们在看看当初hash()的计算方式
static final int hash(Object key) {
int h;
//高位和低位 16位进行异或运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我们知道当计算下标的时候是使用低位进行并运算(& )
所以只要保证低位相同,高位不同(相同的hash会被认为是同一个对象),就能构造出HashMap碰撞100%的程序了,最终结果:
//随便给一个默认值
private static int defInt = 124253;
public int hashCode() {
//将当前对象的hash合并(使用异或更容易打乱)
int hash = this.b.hashCode() ^ this.a.hashCode() ^ this.c.hashCode();
//左移8位,这里的位数可以自己决定
hash = hash << 16;
//和默认值做或运算(让低位运算)
return defInt | hash;
}
例如hash值:
固定的低位值
这样就能保证每次Hash都是同一个index了
7.3、树化的优劣
虽然这样做很好的避免的恶意破坏HashMap分布的情况,但是树化就真的一定很好吗?
从图中可以看出来,树节点是很沉重的(树节点本身就需要满足Node节点的基本属性,并继承于它),其次分布良好的HashMap是可以通过位运算,通过index的方式快速定位,这个效率是远远胜过于红黑树的遍历 ,在新value进入HashMap的时候,走hash 的 index 数度也算远远快于红黑树(红黑树为了保证平衡,会进行左旋和右旋,而且红黑树本身是链表的形式存在,性能自然不足使用数组的Node table)
那么就得到一个原则:尽量不要树化(如果期望树化了也就没必要使用HashMap了可以直接使用TreeMap)
那么HashMap就要让正常分布的数据尽可能少使用TreeNode,而异常的数据使用TreeNode,那么8我们能理解了,基本上按照泊松分布HashMap不会达到8这个量级的极限上认为满足泊松分布中8个Node就等同于不存在的(0.00000006 )
7.4、HashMap中的泊松分布
首先一上来我们又贴出了公式:
泊松分布公式的推导:https://blog.csdn.net/ccnt_2012/article/details/81114920
首先说结论,这个公式是泊松分布的概率密度函数,那么HashMap用这个概率密度算了什么内容呢?
通过泊松分布的公式,我们计算出每个table中出现Node原始的概率。其中有这么一句话
可以看出来了,图中这个公式就是泊松分布的公式,其中 = 0.5 , n=k(table list 的大小) 。从中可以看出,完美的Hash分布的table里面的元素是遵循泊松分布的
7.5、负载因子
说到这里,貌似我还是一直没有解释,负载因子是怎么来的,和泊松分布有关吗?老实说负载因子和泊松分布还真没多大关系,但是说明泊松分布之后,大家才能更加了,树在HashMap中的低位,还有负载因子和HashMap之间的关系。传说:这个概率是通过实验得来的,我无意间在StackOverFlow看到一个文章
可以得出,负载因子和HashMap的性能一定程度上是负相关的
再者,我们发现在HashMap自动扩容计算阈值的时候,如果负载因子越小,那么table空间的利于率越低,扩容的越频繁,那么我可以这样认为,负载因子越小,HashMap的性能越高,但是同时会浪费内存空间,那么负载因子0.75f是在空间和时间上做的一个折衷,但是也有人说是根据牛顿二项定理得来的,当然到这里的时候我就没在往下探究了,有兴趣不嫌事情搞大的盆友们可以继续深究(也有可能这个值是 Doug Lea 一时拍脑袋给出来的)
8、HashMap 衍生Map
8.1、LinkedHashMap
8.1.1 LRU算法
8.2、ConcurrentHashMap CAS 和 分段锁
在防止并发的情况下ConcurrentHashMap和Hashtable实现的方式不太一样,ConcurrentHashMap使用的是分段锁的方式,只对当前table进行上锁,Hashtable则是大粒度锁,根据阿姆达尔定律,ConcurrentHashMap的通过硬件加速的效果将远远大于Hashtable,而且在分布良好的ConcurrentHashMap中,性能上基本上是和HashMap保持一致的
Hashtable
//在方法上面加入 synchronized
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
ConcurrentHashMap
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//寻找确定当前节点对应的table位置,请注意这个f变量
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
//当添加空节点的时候不上锁
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//这里是对f上锁(很明细使用的是分段锁)
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
CAS
在ConcurrentHashMap中自然也少不了CAS的使用,从中也能看出当对一个table操作的时候,使用了CAS的方式
CAS主要是放在putVal的时候由于不知道table index 使用不了分段锁的场景,对已有的val操作都是使用分段锁的方式
9、历史HashMap的BUG
9.1、JDK7 HashMap CPU 100%(死链)
问题的核心点在于多线程进行扩容的时候每个线程会生成一个新的table对象,线程A生成新的table以后,线程B在线程A新生成的table上进行操作会造成死循环。
https://www.jianshu.com/p/61a829fa4e49
https://www.jianshu.com/p/ab0111c0a34b(这篇比较详细)
9.2、JDK8 ConcurrentHashMap CPU 100%
computeIfAbsent导致CPU打满,在new ReservationNode 的时候 hash的默认值是-3
试了一下,虽然在本地没有100%,但是CPU确实飙高了
Map<String,String> map = new ConcurrentHashMap<>();
//第一种
//map.computeIfAbsent("AaAa", key -> map.computeIfAbsent("BBBB", key2 -> "value"));
//第二种
map.computeIfAbsent("AaAa",(String key) -> {map.put("BBBB","value"); return "value";});
官方给出的BUG描述
由于代码的第1670行不满足判断,导致代码一直死循环
这里会初始化一个预留占位的Node
BUG发生的位置(由于这里的fh值等于-3导致无法退出循环)
这个预留Node的hash值默认是-3
https://bugs.openjdk.java.net/browse/JDK-8062841(openJDK bug 提交的地址)
10、JDK 7 HashMap源码
看完了JDK8的源码,我们这边回顾一下JDK7HashMap的源码(方便大家对比JDK8)
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable
{
/**
* Default number of buckets. This is the value the JDK 1.3 uses. Some
* early documentation specified this value as 101. That is incorrect.
* Package visible for use by HashSet.
*/
static final int DEFAULT_CAPACITY = 11;
/**
* The default load factor; this is explicitly specified by the spec.
* Package visible for use by HashSet.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* Compatible with JDK 1.2.
*/
private static final long serialVersionUID = 362498820763181265L;
/**
* The rounded product of the capacity and the load factor; when the number
* of elements exceeds the threshold, the HashMap calls
* <code>rehash()</code>.
* @serial the threshold for rehashing
*/
private int threshold;
/**
* Load factor of this HashMap: used in computing the threshold.
* Package visible for use by HashSet.
* @serial the load factor
*/
final float loadFactor;
/**
* Array containing the actual key-value mappings.
* Package visible for use by nested and subclasses.
*/
transient HashEntry[] buckets;
/**
* Counts the number of modifications this HashMap has undergone, used
* by Iterators to know when to throw ConcurrentModificationExceptions.
* Package visible for use by nested and subclasses.
*/
transient int modCount;
/**
* The size of this HashMap: denotes the number of key-value pairs.
* Package visible for use by nested and subclasses.
*/
transient int size;
/**
* The cache for {@link #entrySet()}.
*/
private transient Set entries;
/**
* Class to represent an entry in the hash table. Holds a single key-value
* pair. Package visible for use by subclass.
*
* @author Eric Blake <[email protected]>
*/
static class HashEntry extends AbstractMap.BasicMapEntry
{
/**
* The next entry in the linked list. Package visible for use by subclass.
*/
HashEntry next;
/**
* Simple constructor.
* @param key the key
* @param value the value
*/
HashEntry(Object key, Object value)
{
super(key, value);
}
/**
* Called when this entry is accessed via {@link #put(Object, Object)}.
* This version does nothing, but in LinkedHashMap, it must do some
* bookkeeping for access-traversal mode.
*/
void access()
{
}
/**
* Called when this entry is removed from the map. This version simply
* returns the value, but in LinkedHashMap, it must also do bookkeeping.
*
* @return the value of this key as it is removed
*/
Object cleanup()
{
return value;
}
}
/**
* Construct a new HashMap with the default capacity (11) and the default
* load factor (0.75).
*/
public HashMap()
{
this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* Construct a new HashMap from the given Map, with initial capacity
* the greater of the size of <code>m</code> or the default of 11.
* <p>
*
* Every element in Map m will be put into this new HashMap.
*
* @param m a Map whose key / value pairs will be put into the new HashMap.
* <b>NOTE: key / value pairs are not cloned in this constructor.</b>
* @throws NullPointerException if m is null
*/
public HashMap(Map m)
{
this(Math.max(m.size() * 2, DEFAULT_CAPACITY), DEFAULT_LOAD_FACTOR);
putAllInternal(m);
}
/**
* Construct a new HashMap with a specific inital capacity and
* default load factor of 0.75.
*
* @param initialCapacity the initial capacity of this HashMap (>=0)
* @throws IllegalArgumentException if (initialCapacity < 0)
*/
public HashMap(int initialCapacity)
{
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Construct a new HashMap with a specific inital capacity and load factor.
*
* @param initialCapacity the initial capacity (>=0)
* @param loadFactor the load factor (> 0, not NaN)
* @throws IllegalArgumentException if (initialCapacity < 0) ||
* ! (loadFactor > 0.0)
*/
public HashMap(int initialCapacity, float loadFactor)
{
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "
+ initialCapacity);
if (! (loadFactor > 0)) // check for NaN too
loadFactor = 0.75f;
if (initialCapacity == 0)
initialCapacity = 1;
buckets = new HashEntry[initialCapacity];
this.loadFactor = loadFactor;
threshold = (int) (initialCapacity * loadFactor);
}
/**
* Returns the number of kay-value mappings currently in this Map.
*
* @return the size
*/
public int size()
{
return size;
}
/**
* Returns true if there are no key-value mappings currently in this Map.
*
* @return <code>size() == 0</code>
*/
public boolean isEmpty()
{
return size == 0;
}
/**
* Return the value in this HashMap associated with the supplied key,
* or <code>null</code> if the key maps to nothing. NOTE: Since the value
* could also be null, you must use containsKey to see if this key
* actually maps to something.
*
* @param key the key for which to fetch an associated value
* @return what the key maps to, if present
* @see #put(Object, Object)
* @see #containsKey(Object)
*/
public Object get(Object key)
{
int idx = hash(key);
HashEntry e = buckets[idx];
while (e != null)
{
if (equals(key, e.key))
return e.value;
e = e.next;
}
return null;
}
/**
* Returns true if the supplied object <code>equals()</code> a key
* in this HashMap.
*
* @param key the key to search for in this HashMap
* @return true if the key is in the table
* @see #containsValue(Object)
*/
public boolean containsKey(Object key)
{
int idx = hash(key);
HashEntry e = buckets[idx];
while (e != null)
{
if (equals(key, e.key))
return true;
e = e.next;
}
return false;
}
/**
* Puts the supplied value into the Map, mapped by the supplied key.
* The value may be retrieved by any object which <code>equals()</code>
* this key. NOTE: Since the prior value could also be null, you must
* first use containsKey if you want to see if you are replacing the
* key's mapping.
*
* @param key the key used to locate the value
* @param value the value to be stored in the HashMap
* @return the prior mapping of the key, or null if there was none
* @see #get(Object)
* @see Object#equals(Object)
*/
public Object put(Object key, Object value)
{
int idx = hash(key);
HashEntry e = buckets[idx];
while (e != null)
{
if (equals(key, e.key))
{
e.access(); // Must call this for bookkeeping in LinkedHashMap.
Object r = e.value;
e.value = value;
return r;
}
else
e = e.next;
}
// At this point, we know we need to add a new entry.
modCount++;
if (++size > threshold)
{
rehash();
// Need a new hash value to suit the bigger table.
idx = hash(key);
}
// LinkedHashMap cannot override put(), hence this call.
addEntry(key, value, idx, true);
return null;
}
/**
* Copies all elements of the given map into this hashtable. If this table
* already has a mapping for a key, the new mapping replaces the current
* one.
*
* @param m the map to be hashed into this
*/
public void putAll(Map m)
{
Iterator itr = m.entrySet().iterator();
int msize = m.size();
while (msize-- > 0)
{
Map.Entry e = (Map.Entry) itr.next();
// Optimize in case the Entry is one of our own.
if (e instanceof AbstractMap.BasicMapEntry)
{
AbstractMap.BasicMapEntry entry = (AbstractMap.BasicMapEntry) e;
put(entry.key, entry.value);
}
else
put(e.getKey(), e.getValue());
}
}
/**
* Removes from the HashMap and returns the value which is mapped by the
* supplied key. If the key maps to nothing, then the HashMap remains
* unchanged, and <code>null</code> is returned. NOTE: Since the value
* could also be null, you must use containsKey to see if you are
* actually removing a mapping.
*
* @param key the key used to locate the value to remove
* @return whatever the key mapped to, if present
*/
public Object remove(Object key)
{
int idx = hash(key);
HashEntry e = buckets[idx];
HashEntry last = null;
while (e != null)
{
if (equals(key, e.key))
{
modCount++;
if (last == null)
buckets[idx] = e.next;
else
last.next = e.next;
size--;
// Method call necessary for LinkedHashMap to work correctly.
return e.cleanup();
}
last = e;
e = e.next;
}
return null;
}
/**
* Clears the Map so it has no keys. This is O(1).
*/
public void clear()
{
if (size != 0)
{
modCount++;
Arrays.fill(buckets, null);
size = 0;
}
}
/**
* Returns true if this HashMap contains a value <code>o</code>, such that
* <code>o.equals(value)</code>.
*
* @param value the value to search for in this HashMap
* @return true if at least one key maps to the value
* @see containsKey(Object)
*/
public boolean containsValue(Object value)
{
for (int i = buckets.length - 1; i >= 0; i--)
{
HashEntry e = buckets[i];
while (e != null)
{
if (equals(value, e.value))
return true;
e = e.next;
}
}
return false;
}
/**
* Returns a shallow clone of this HashMap. The Map itself is cloned,
* but its contents are not. This is O(n).
*
* @return the clone
*/
public Object clone()
{
HashMap copy = null;
try
{
copy = (HashMap) super.clone();
}
catch (CloneNotSupportedException x)
{
// This is impossible.
}
copy.buckets = new HashEntry[buckets.length];
copy.putAllInternal(this);
// Clear the entry cache. AbstractMap.clone() does the others.
copy.entries = null;
return copy;
}
/**
* Returns a "set view" of this HashMap's keys. The set is backed by the
* HashMap, so changes in one show up in the other. The set supports
* element removal, but not element addition.
*
* @return a set view of the keys
* @see #values()
* @see #entrySet()
*/
public Set keySet()
{
if (keys == null)
// Create an AbstractSet with custom implementations of those methods
// that can be overridden easily and efficiently.
keys = new AbstractSet()
{
public int size()
{
return size;
}
public Iterator iterator()
{
// Cannot create the iterator directly, because of LinkedHashMap.
return HashMap.this.iterator(KEYS);
}
public void clear()
{
HashMap.this.clear();
}
public boolean contains(Object o)
{
return containsKey(o);
}
public boolean remove(Object o)
{
// Test against the size of the HashMap to determine if anything
// really got removed. This is necessary because the return value
// of HashMap.remove() is ambiguous in the null case.
int oldsize = size;
HashMap.this.remove(o);
return oldsize != size;
}
};
return keys;
}
/**
* Returns a "collection view" (or "bag view") of this HashMap's values.
* The collection is backed by the HashMap, so changes in one show up
* in the other. The collection supports element removal, but not element
* addition.
*
* @return a bag view of the values
* @see #keySet()
* @see #entrySet()
*/
public Collection values()
{
if (values == null)
// We don't bother overriding many of the optional methods, as doing so
// wouldn't provide any significant performance advantage.
values = new AbstractCollection()
{
public int size()
{
return size;
}
public Iterator iterator()
{
// Cannot create the iterator directly, because of LinkedHashMap.
return HashMap.this.iterator(VALUES);
}
public void clear()
{
HashMap.this.clear();
}
};
return values;
}
/**
* Returns a "set view" of this HashMap's entries. The set is backed by
* the HashMap, so changes in one show up in the other. The set supports
* element removal, but not element addition.<p>
*
* Note that the iterators for all three views, from keySet(), entrySet(),
* and values(), traverse the HashMap in the same sequence.
*
* @return a set view of the entries
* @see #keySet()
* @see #values()
* @see Map.Entry
*/
public Set entrySet()
{
if (entries == null)
// Create an AbstractSet with custom implementations of those methods
// that can be overridden easily and efficiently.
entries = new AbstractSet()
{
public int size()
{
return size;
}
public Iterator iterator()
{
// Cannot create the iterator directly, because of LinkedHashMap.
return HashMap.this.iterator(ENTRIES);
}
public void clear()
{
HashMap.this.clear();
}
public boolean contains(Object o)
{
return getEntry(o) != null;
}
public boolean remove(Object o)
{
HashEntry e = getEntry(o);
if (e != null)
{
HashMap.this.remove(e.key);
return true;
}
return false;
}
};
return entries;
}
/**
* Helper method for put, that creates and adds a new Entry. This is
* overridden in LinkedHashMap for bookkeeping purposes.
*
* @param key the key of the new Entry
* @param value the value
* @param idx the index in buckets where the new Entry belongs
* @param callRemove whether to call the removeEldestEntry method
* @see #put(Object, Object)
*/
void addEntry(Object key, Object value, int idx, boolean callRemove)
{
HashEntry e = new HashEntry(key, value);
e.next = buckets[idx];
buckets[idx] = e;
}
/**
* Helper method for entrySet(), which matches both key and value
* simultaneously.
*
* @param o the entry to match
* @return the matching entry, if found, or null
* @see #entrySet()
*/
final HashEntry getEntry(Object o)
{
if (! (o instanceof Map.Entry))
return null;
Map.Entry me = (Map.Entry) o;
Object key = me.getKey();
int idx = hash(key);
HashEntry e = buckets[idx];
while (e != null)
{
if (equals(e.key, key))
return equals(e.value, me.getValue()) ? e : null;
e = e.next;
}
return null;
}
/**
* Helper method that returns an index in the buckets array for `key'
* based on its hashCode(). Package visible for use by subclasses.
*
* @param key the key
* @return the bucket number
*/
final int hash(Object key)
{
return key == null ? 0 : Math.abs(key.hashCode() % buckets.length);
}
/**
* Generates a parameterized iterator. Must be overrideable, since
* LinkedHashMap iterates in a different order.
*
* @param type {@link #KEYS}, {@link #VALUES}, or {@link #ENTRIES}
* @return the appropriate iterator
*/
Iterator iterator(int type)
{
return new HashIterator(type);
}
/**
* A simplified, more efficient internal implementation of putAll(). The
* Map constructor and clone() should not call putAll or put, in order to
* be compatible with the JDK implementation with respect to subclasses.
*
* @param m the map to initialize this from
*/
void putAllInternal(Map m)
{
Iterator itr = m.entrySet().iterator();
int msize = m.size();
size = msize;
while (msize-- > 0)
{
Map.Entry e = (Map.Entry) itr.next();
Object key = e.getKey();
int idx = hash(key);
addEntry(key, e.getValue(), idx, false);
}
}
/**
* Increases the size of the HashMap and rehashes all keys to new
* array indices; this is called when the addition of a new value
* would cause size() > threshold. Note that the existing Entry
* objects are reused in the new hash table.
*
* <p>This is not specified, but the new size is twice the current size
* plus one; this number is not always prime, unfortunately.
*/
private void rehash()
{
HashEntry[] oldBuckets = buckets;
int newcapacity = (buckets.length * 2) + 1;
threshold = (int) (newcapacity * loadFactor);
buckets = new HashEntry[newcapacity];
for (int i = oldBuckets.length - 1; i >= 0; i--)
{
HashEntry e = oldBuckets[i];
while (e != null)
{
int idx = hash(e.key);
HashEntry dest = buckets[idx];
if (dest != null)
{
while (dest.next != null)
dest = dest.next;
dest.next = e;
}
else
buckets[idx] = e;
HashEntry next = e.next;
e.next = null;
e = next;
}
}
}
/**
* Serializes this object to the given stream.
*
* @param s the stream to write to
* @throws IOException if the underlying stream fails
* @serialData the <i>capacity</i>(int) that is the length of the
* bucket array, the <i>size</i>(int) of the hash map
* are emitted first. They are followed by size entries,
* each consisting of a key (Object) and a value (Object).
*/
private void writeObject(ObjectOutputStream s) throws IOException
{
// Write the threshold and loadFactor fields.
s.defaultWriteObject();
s.writeInt(buckets.length);
s.writeInt(size);
// Avoid creating a wasted Set by creating the iterator directly.
Iterator it = iterator(ENTRIES);
while (it.hasNext())
{
HashEntry entry = (HashEntry) it.next();
s.writeObject(entry.key);
s.writeObject(entry.value);
}
}
/**
* Deserializes this object from the given stream.
*
* @param s the stream to read from
* @throws ClassNotFoundException if the underlying stream fails
* @throws IOException if the underlying stream fails
* @serialData the <i>capacity</i>(int) that is the length of the
* bucket array, the <i>size</i>(int) of the hash map
* are emitted first. They are followed by size entries,
* each consisting of a key (Object) and a value (Object).
*/
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read the threshold and loadFactor fields.
s.defaultReadObject();
// Read and use capacity, followed by key/value pairs.
buckets = new HashEntry[s.readInt()];
int len = s.readInt();
while (len-- > 0)
{
Object key = s.readObject();
addEntry(key, s.readObject(), hash(key), false);
}
}
/**
* Iterate over HashMap's entries.
* This implementation is parameterized to give a sequential view of
* keys, values, or entries.
*
* @author Jon Zeppieri
*/
private final class HashIterator implements Iterator
{
/**
* The type of this Iterator: {@link #KEYS}, {@link #VALUES},
* or {@link #ENTRIES}.
*/
private final int type;
/**
* The number of modifications to the backing HashMap that we know about.
*/
private int knownMod = modCount;
/** The number of elements remaining to be returned by next(). */
private int count = size;
/** Current index in the physical hash table. */
private int idx = buckets.length;
/** The last Entry returned by a next() call. */
private HashEntry last;
/**
* The next entry that should be returned by next(). It is set to something
* if we're iterating through a bucket that contains multiple linked
* entries. It is null if next() needs to find a new bucket.
*/
private HashEntry next;
/**
* Construct a new HashIterator with the supplied type.
* @param type {@link #KEYS}, {@link #VALUES}, or {@link #ENTRIES}
*/
HashIterator(int type)
{
this.type = type;
}
/**
* Returns true if the Iterator has more elements.
* @return true if there are more elements
* @throws ConcurrentModificationException if the HashMap was modified
*/
public boolean hasNext()
{
if (knownMod != modCount)
throw new ConcurrentModificationException();
return count > 0;
}
/**
* Returns the next element in the Iterator's sequential view.
* @return the next element
* @throws ConcurrentModificationException if the HashMap was modified
* @throws NoSuchElementException if there is none
*/
public Object next()
{
if (knownMod != modCount)
throw new ConcurrentModificationException();
if (count == 0)
throw new NoSuchElementException();
count--;
HashEntry e = next;
while (e == null)
e = buckets[--idx];
next = e.next;
last = e;
if (type == VALUES)
return e.value;
if (type == KEYS)
return e.key;
return e;
}
public void remove()
{
if (knownMod != modCount)
throw new ConcurrentModificationException();
if (last == null)
throw new IllegalStateException();
HashMap.this.remove(last.key);
last = null;
knownMod++;
}
}
}
11、总结