我们以jdk1.7.0_80为例,对其中的HashMap源码进行分析。
目录
- 一、源码分析
- 二、问题总结
- 2.1 roundUpToPowerOf2(toSize)方法是怎样实现 找到一个大于等于toSize的2的幂次方数的?
- 2.2 为什么数组初始化容量或者扩容的容量一定是2的幂次方数呢?
- 2.3 initHashSeedAsNeeded(capacity)方法的作用是什么?
- 2.4 为什么key的哈希值要经过多次的右移和异或得出呢?
- 2.5 为什么数组的下标值是通过key的哈希值 &(数组长度-1)而不用 | 呢?
- 2.6 数组扩容的目的是什么?
- 2.7 多线程下jdk1.7的hashmap的扩容后,调用get或者put会出现死循环,为什么?
- 2.8 modCount是做什么用的?
- 2.9 如何实现要找到一个小于等于当前数字的2的幂次方数呢?
一、源码分析
本篇文章不会对HashMap中的所有方法进行解析,只会对其中几个重要的方法进行解析,比如PUT、GET等。
1.1 HashMap重要属性
我们先了解一下Hashmap类中的一些重要属性。
//默认的数组初始化容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量 2的30次幂
tatic final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//空的数组
static final Entry<?,?>[] EMPTY_TABLE = {};
//数组,根据需要调整大小。长度必须始终是2的幂。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//当前hashMap中存的元素的数量
transient int size;
//阈值 (capacity * load factor). 用于扩容 16 * 0.75
int threshold;
//哈希表的加载因子。
final float loadFactor;
//Hashmap修改次数
transient int modCount;
//映射容量的默认阈值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
//数组的元素,是个链表
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//……………………….省略
}
由上面的代码可知:HashMap是由数组加链表实现的
。
那么这里提出一个简单的问题:为什么要使用数组和链表相结合的方式而不是只用数组的方式呢?
我们都知道数组的特点是 查询快,而插入则比较慢。链表的特点是插入比较快,查询较慢。在使用HashMap的时候,put和get方法的使用频率都很高,如果只用数组结构的话,put的效率就会很差。只用链表结构的话,get的效率也会很差。我们要兼顾两者的效率,因此使用数组和链表相结合的方式。
1.2 解析PUT方法
我们先看一个简单的例子:
public class HashMapDemo {
public static void main(String[] args) {
HashMap<Object, Object> map = new HashMap<>();
map.put("1","1");
map.get("1");
}
}
首先解析 HashMap<Object, Object> map = new HashMap<>();
当然我们也可以使用 new HashMap(10);
自定义数组容量。跟进源码可知,他只是做了一些简单的初始化工作,就是 将传入的负载因子赋值给HashMap的loadFactor属性,以及将传入的初始容量赋值给HashMap的threshold属性。
public HashMap() {
//DEFAULT_INITIAL_CAPACITY = 16,DEFAULT_LOAD_FACTOR = 0.75f
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
进入this方法:
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);
//将传入的负载因子数值0.75赋值给hashMap的loadFactor属性
this.loadFactor = loadFactor;
//将传入的初始容量 16 赋值给hashMap的threshold属性
threshold = initialCapacity;
init();
}
接下来解析put方法,看一下put的源码:
public V put(K key, V value) {
//如果数组为空,则进行初始化
if (table == EMPTY_TABLE) {
//数组初始化
inflateTable(threshold);
}
//如果key==null,则进入下面这个方法,说明在hashmap中,key可以为空
if (key == null)
return putForNullKey(value);
//计算key的hash值
int hash = hash(key);
//计算key对应的数组下标
int i = indexFor(hash, table.length);
//循环数组当前元素的链表,不会每次都循环,或者循环到链表末尾,所以不必担心效率问题
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果传入的key在链表中已经存在,则用传入的key对应的value覆盖就的value
//并返回旧的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//HashMap修改标志加1
modCount++;
//添加Entry,放入数组
addEntry(hash, key, value, i);
return null;
}
每行注释解释了put的总体流程,下面我们对每行代码进行解析。
(1)首先进入判断如果数组为空,则进行初始化,我们进入inflateTable(threshold)
方法来看一下是如何进行初始化的,这个threshold(阈值)在默认情况下是16:
private void inflateTable(int toSize) {
//找到一个大于等于toSize的2的幂次方数作为数组初始化容量
//举个例子,就是假如toSize=10 那么capacity应该=16
//假如toSize=16 那么capacity应该=16
//在这里面如何做的呢?
int capacity = roundUpToPowerOf2(toSize);
//计算阈值,默认值下 为 16*0.75=12
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//数组初始化,这个容量就是计算后的值16
table = new Entry[capacity];
//初始化哈希种子
initHashSeedAsNeeded(capacity);
}
通过上面的数组初始化,我们知道,传入的参数 toSize=16
,由 roundUpToPowerOf2(toSize)
方法得出的capacity =16,因此初始化后的数组容量大小为16。
同时在这里提出两个问题:
- roundUpToPowerOf2(toSize)方法是怎样实现 找到一个大于等于toSize的2的幂次方数的?
- 为什么数组初始化容量一定是2的幂次方数呢?
- initHashSeedAsNeeded(capacity)方法的作用是什么?
(2)接下来我门来看HashMap是如何处理key=null的情况的,我们进入putForNullKey(value)
进行解析:
private V putForNullKey(V value) {
//遍历数组第0个位置的元素(链表)
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//遍历链表,如果key==null,则将新的value覆盖原来的value,返回原来的value
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//修改标志加1
modCount++;
//如果数组第0个位置为null,则将传入的value放在数组第0个位置
addEntry(0, null, value, 0);
return null;
}
由上述代码可知,在HashMap中是允许key为null的
,并且会将key=null对应的value封装成Entry放到数组下标为0的位置,并将原来存在的元素覆盖。
(3)接下来是根据传入的key计算它的哈希值 hash(key)
:
final int hash(Object k) {
//种子值,在这里默认为0 ,如果不为0,则禁用k.hashCode(),来减少哈希冲突
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
//计算key的哈希值
h ^= k.hashCode();
//hash值是32位,数组长度只有16位,哈希值不停的右移和异或,会使hash值的高位能够参与计算,保证散列性更好些,减少哈希冲突。
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
由上述代码可知,key的哈希值并不是hashCode()直接得出,而是经过多次的右移和异或得出。
我们在这里提出一个问题:
- 为什么key的哈希值要经过多次的右移和异或得出呢?
(4)接下来解析indexFor(hash, table.length)
来获取数组的下标:
static int indexFor(int h, int length) {
// key的哈希值 & (数组长度-1)
return h & (length-1);
}
由上述代码可知,数组的下标值是通过key的哈希值 &(数组长度-1)得出。
那我们在这里提出一个问题:
- 为什么数组的下标值是通过key的哈希值 &(数组长度-1)呢?
(4)计算完数组下标值,我们就开始循环遍历这个下标值对应的数组元素(链表):
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
由上述代码可知,遍历链表,如果传入的key在链表中已经存在,则用传入的key对应的value覆盖旧的value,并返回旧的value。
(5)最后解析 addEntry(hash, key, value, i)
,这是真正存放元素的方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果当前hashMap中存的元素的数量大于阈值并且 bucketIndex下标下的数组元素不为空,则进行扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//创建元素
createEntry(hash, key, value, bucketIndex);
}
加入我们是第一次put数据,那么不需要扩容,直接进入 createEntry(hash, key, value, bucketIndex)
方法:
void createEntry(int hash, K key, V value, int bucketIndex) {
//根据下标值bucketIndex找到对应的元素e
Entry<K,V> e = table[bucketIndex];
//创建一个元素Entry,将e赋值给Entry.next,将Entry放到数组的bucketIndex位置
table[bucketIndex] = new Entry<>(hash, key, value, e);
//长度加1
size++;
}
//*********new Entry*************
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
由上述代码可知,数组存放元素是采用头插法的方式。
假如数组的形式原本图1,现在put一个key=李四,value=李四的数据,将它封装为Entry,这个元素对应的数组下标恰好是bucketIndex,那么我们将这个Entry放到这个位置上,流程如图2 ,3 所示:
那么我们在这里提出一个问题:
- jdk1.7中的HashMap的put方法采用头插法呢?
从上面的图中很容易知道,采用头插法不需要遍历链表,效率较高,如果采用尾插法,需要遍历链表找到尾部,效率低。
1.3 解析resize(扩容)方法
在解析到createEntry(int hash, K key, V value, int bucketIndex)
方法的时候,里面有一段扩容的代码:
//如果当前hashMap中存的元素的数量大于阈值并且 bucketIndex下标下的数组元素不为空,则进行扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
假设当前put到table中的元素容量size已经超过threshold(值为12),并且bucketIndex下标下的元素不为null,那么进行扩容resize(2 * table.length)
,扩容参数为 2 * table.length=2*16=32
:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//以上代码是记录旧的数组table保存起来
//创建一个新的容量为32的数组newTable
Entry[] newTable = new Entry[newCapacity];
//将oldTable上的元素放到newTable上去,完成数组转移
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//转移结束后 将新数组赋给旧的数组
table = newTable;
//重新计算阈值 32*0.75
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
由以上代码可知,真正实现扩容的方法是 transfer(newTable, initHashSeedAsNeeded(newCapacity))
:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//双层循环,先循环旧的数组table
for (Entry<K,V> e : table) {
//再循环旧数组元素上面的链表元素
while(null != e) {
Entry<K,V> next = e.next;
//rehash默认为false,稍后解析
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//根据key的哈希值和新的数组容量32 重新计算数组下标值
//计算出来的i有可能跟以前一样,也有可能不同(跟key的哈希值高位有关)
int i = indexFor(e.hash, newCapacity);
//下面两行代码就是采用头插法的方式将元素e放到新的数组上
e.next = newTable[i];
newTable[i] = e;
//将next赋值给额,进行下次循环
e = next;
}
}
}
由上述代码可知,HashMap扩容就是创建一个新的数组,容量为旧数组容量的二倍,然后将旧数组上的元素根据重新计算的数组下标值。存放到新的数组上面。
我们用图形对上述代码进行描述:
首先旧的数组上面的某个位置有链表元素,我们对这个链表进行遍历,假设计算出的新下标值i都相同。新的数组容量为旧数组的2倍:
当执行 e.next = newTable[i];
时,变成如下图:
然后执行 newTable[i] = e;
得到如下图:
接下来执行e = next;
进入下次循环,最终得到结果如下:
有上面几个图可知,扩容后的链表是倒序的。
那么我们在这里提出两个问题:
- 数组扩容的目的是什么?
- 多线程下jdk1.7的hashmap的扩容会出现死循环,为什么?
- 多线程如何防止死循环?
1.4 解析GET方法
get方法源码如下:
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
当key为null时,我们进入getForNullKey()
方法获取对应的value:
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
前面我们说过,当key为null时,将对应的value封装成Entry放到数组table的第0个位置。那么上面这个方法就是从table[0]上,找key为null时对应的value 返回。
当key不为null时,通过getEntry(key)
获取值:
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//计算key的哈希值
int hash = (key == null) ? 0 : hash(key);
//根据哈希值和数组长度计算数组下标值,并对这个下标值对应的数组元素进行遍历
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//遍历链表,如果找到与传入的key相同的key值,则返回这个元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
二、问题总结
在解析源码的过程中,我们提出了一些问题,下面就对这些问题一一进行解答。
2.1 roundUpToPowerOf2(toSize)方法是怎样实现 找到一个大于等于toSize的2的幂次方数的?
首先 roundUpToPowerOf2(toSize)
这个方法是在put元素,初始化数组的时候调用的:
private static int roundUpToPowerOf2(int number) {
// number =16
//MAXIMUM_CAPACITY默认为2的三十次幂
//由于number > 1成立,所以进入Integer.highestOneBit((number - 1) << 1) 方法,(number - 1) << 1结果为15左移一位得到结果为15*2=30
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
那我们接下来进入Integer.highestOneBit(30)这个方法,理论上的结果应该为16:
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
计算过程如下:
highestOneBit(30)
30 0001 1110
>>1 0000 1111
| 0001 1111 -------32
>>2 0000 0111
| 0001 1111 ------32
>> 4 0000 0001
| 0001 1111 ----- 32
........
后面运算过程与前面一样,结果为0001 1111
0001 1111
>>>1 0000 1111
相减 0001 0000
结果 0001 0000 --------16
因此 roundUpToPowerOf2(int number)方法最终返回的值是16
2.2 为什么数组初始化容量或者扩容的容量一定是2的幂次方数呢?
先看一个例子,查看二进制数的规律:
2--------------0010
4--------------0100
8--------------1000
16-------------0001 0000 --------------- 减1 ----------- 0000 1111
从上面的例子可以看出2的次幂方数对应的二进制数,都只有一个1,那么我们在计算数组的下标的时候会执行h & (length-1)
,我们以length的默认值为16为例,length-1=15,对应的二进制数为0000 1111,这样 h & 0000 1111
的结果就满足数组的长度范围一定为0-15(因为高位 & 后都为0),并且结果的低四位为h的低四位,这样能保证得出的下标值均匀的分布在0-15上面。我们举个反例,假如length=15,15不是2的次幂方数,那么length-1=14,对应的二进制数为0000 1110,那么 h & 0000 1110
得出的结果最后一位一定是0,最后一位都为0,而 后四位为 0001, 0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。
2.3 initHashSeedAsNeeded(capacity)方法的作用是什么?
在前面的源码分析中,我们分别在在数组初始化以及数组扩容的方法中使用到了这个方法。
首先我们进入HashMap的静态代码块:
static final int ALTERNATIVE_HASHING_THRESHOLD;
static {
//如果在配置了jdk的参数jdk.map.althashing.threshold,那么altThreshold 就不为空,一般情况下用户不会配置这个阈值
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try {
//如果altThreshold不为null,threshold =altThreshold,否则 threshold =nteger.MAX_VALUE
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
// disable alternative hashing if -1
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
//赋值
ALTERNATIVE_HASHING_THRESHOLD = threshold;
}
进入initHashSeedAsNeeded
方法:
final boolean initHashSeedAsNeeded(int capacity) {
//默认情况下hashSeed=0 那么currentAltHashing=false
boolean currentAltHashing = hashSeed != 0;
// 如果配置了jdk的参数jdk.map.althashing.threshold,并且capacity >=这个配置值成立(一般情况下不成立 ,useAltHashing =false),那么useAltHashing =true
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
//
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
//计算哈希种子值
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
在数组扩容的时候有这样一段代码:
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
rehash就是上述返回的switching值,如果为true,就会重新计算key的哈希值。哈希种子存在的目的就是让key计算哈希值更加复杂,更加散列。
2.4 为什么key的哈希值要经过多次的右移和异或得出呢?
hash值是32位,数组不扩容的情况下长度只有16位,哈希值不停的右移和异或,会使hash值的高位能够参与计算,保证散列性更好些,减少哈希冲突。
2.5 为什么数组的下标值是通过key的哈希值 &(数组长度-1)而不用 | 呢?
因为 & 的效率比 I 高,并且散列性更好。
2.6 数组扩容的目的是什么?
目的就是减少哈希冲突,让链表变短。
2.7 多线程下jdk1.7的hashmap的扩容后,调用get或者put会出现死循环,为什么?
代码如下:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
假如有如下场景:有两个线程同时执行到扩容阶段,会新建两个不同的新数组,当执行到Entry<K,V> next = e.next;阶段时,线程1继续正常执行,线程2在这里卡住了。
线程1执行完,线程2还在Entry<K,V> next = e.next;阶段时阶段时的场景为:
当线程1执行完后,会出现newTable1的场景,此时原本的oldTable2的指针也会跟着转移到newTable1下。
假设这个时候线程2又开始 执行了,执行第一次循环:
执行第二次循环 这段代码时 Entry<K,V> next = e.next;
:
接着执行完一下代码时
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
场景如下:
接下来执行e = next;
那么此时e2就移动到了key=1的位置:
再次执行第三次循环 Entry<K,V> next = e.next
; :
然后执行e.next = newTable[i]
;
最后执行 e = next;
得出 e=null,进行第三次循环的时候退出代码。
此时出现了循环链表 ,当我们get元素或者put的时候,就会遍历链表,出现死循环。
2.8 modCount是做什么用的?
举个例子:
public class HashMapDemo {
public static void main(String[] args) {
HashMap<Object, Object> map = new HashMap();
map.put("1", "1");
map.put("2", "2");
map.put("3", "3");
for(Iterator i$ = map.keySet().iterator(); i$.hasNext(); ) {
Object key = i$.next();
if (key.equals("2")) {
map.remove(key);
//i$.remove();
}
}
}
}
这段代码执行结果为:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextEntry(HashMap.java:922)
at java.util.HashMap$KeyIterator.next(HashMap.java:956)
at HashMapDemo.main(HashMapDemo.java:12)
报错啦,造成报错的原因就和modCount有关。首先我们看一下异常的出处:
final Entry<K,V> nextEntry() {
//modCount != expectedModCount的情况下报错
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
因为例子代码中 map.put执行的三次,根据put的源码可知,此时modCount =3
,我们在遍历map的时候执行了map.keySet().iterator()
,进入其中源码可知又调用了newEntryIterator()
,然后调用 EntryIterator()
,然后调用其父类HashIterator构造方法:
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
由上述代码可知modCount赋值给expectedModCount ,已知modCount=3,那么expectedModCount =3。
接下来执行Object key = i$.next();
获取key:
public Map.Entry<K,V> next() {
return nextEntry();
}
在这个方法中调用了上述抛出异常的方法nextEntry()。
接下来执行 map.remove(key)
方法删除元素:
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
在 这个方法中 modCount++,得出 modCount=4。
然后举例中的代码进入了下一轮循环,再次执行 Object key = i$.next();
就会调用nextEntry()
,此时modCount =4 , expectedModCount=3,modCount != expectedModCount,所以 throw new ConcurrentModificationException();
解决这个异常的方式:
将map.remove(key); 改为 i$.remove();
HashMap这样做的意义就是,HashMap时非线程安全的,假设有两个线程,有遍历,有一个修改, 会出现并发问题,hashmap发现会有这个问题,就会抛出异常,快速失败。
2.9 如何实现要找到一个小于等于当前数字的2的幂次方数呢?
在2.1问题中,有一个方法highestOneBit(int i)
,这个方法就能获取一个小于等于当前数字i的2的幂次方数。
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
相当于把高位1以后的所有的位的数都改为1,最后,用这个数字减去 数字右移一位的后的数字,就会得到2的次幂数。
右移和或的方式,不停的右移1+2+4+8+16=31,正好是整数(int 4个字节,32位)的范围2的31次幂,直到把高位变成0,这样做最保险。