JDK1.7中的HashMap源码分析

我们以jdk1.7.0_80为例,对其中的HashMap源码进行分析。

一、源码分析

本篇文章不会对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,这样做最保险。

发布了6 篇原创文章 · 获赞 5 · 访问量 1000

猜你喜欢

转载自blog.csdn.net/qq_28203555/article/details/104047729