JDK1.7中的HashMap详细分析
前言
现在一般进入java开发这个行业的所有同行工作者基本可以说100%都接触过HashMap,可以说HashMap是我们在一般的应用开发中用的是最多的一种集合类框架,像ArrayList和Hashmap可能是用的最多的,这两种集合框架都是非线程安全的,也就是说这两种集合框架只能用于单线程的环境下,在多线程环境下会有线程安全问题,当然了针对这两种集合框架都有其对应的线程安全集合框架,ArrayList对应可以使用 Collections.synchronizedList,CopyOnWriteArrayList,现在针对ArrayList在多线程环境下常用的是CopyOnWriteArrayList,但是这种安全集合框架也只是针对于读多写少的场景,如果频繁读写的情况下,CopyOnWriteArrayList性能会有一定的影响,因为CopyOnWriteArrayList也是在修改集合元素的情况下,加锁,然后复制一份完成了相应的操作过后然后再写回去,那么这个时候就会出现读取前后不一致的情况,在会写成功之前,读取到的是之前的原来的数组的元素,所有CopyOnWriteArrayList还有一个特点就是前后读取有不一致的情况产生。当然了针对了Arraylist,还有一个线程安全的集合类Vector类,但是这个集合类现在基本上已经被废弃了,不推荐使用了。
而我们的HashMap对应的线程安全集合类是ConcurrentHashMap,这个采用了分段锁的机制,这个后面的笔记会专门来记录,我这篇笔记主要记录下Hashmap的实现原理。
集合存储原理
我们都知道如果我们申明一个数组,我一般添加元素都是默认尾部添加,有些集合类的比如像ArrayList底层的数据结构就是一个普通的Object数组,还有就是阻塞队列中的ArrayBlockingQueue,优先级队列PriorityBlockingQueue底层都是基于数据结构来存储元素的,虽然PriorityBlockingQueue底层是二叉树(我叫它二叉堆)来存储,但是二叉堆中的节点Node元素都是存储在Object数据中的。
所以数组存储有一个优点就是访问特别快,我们可以通过下标访问,并且支持随机下标访问;比如:
Object [] arr = new Object[2];
arr[0]= new Test();
arr[1] = new Test();
//访问第一个元素
Object o1 = arr[0];
Object o2 = arr[1];
但是我们平时在使用HashMap的时候,有没有想过,HashMap的存放和获取是不是和下标都没有什么关系呢?在使用HashMap的时候添加元素是通过put(key,value),获取元素是通过get(key)来进行操作的,那么HashMap底层的数据结构时什么样的呢?
HashMap这个集合类,就算没有看过它的底层源码,单单从名字就可以看出有Hash 有Map,那么什么意思呢?Hash一般指的就是我们的hashcode的意思,Map就是对应的意思,所有HashMap的底层数据结构就是在数据的基础上的一个hash单向链表结构,什么意思呢?就是hashmap的底层数据结构(JDK7)也是一个数组结构,当添加元素的时候通过计算添加的key的hash值,然后进行一些计算,得出我们的数组下标,然后将对应的值放入到这个数据的位置上就可以了,但是我们都知道不同的key可能会生成相同的hashcode的值,那么这个时候又不能覆盖我们之前添加的值,但是如果你添加一个相同key的时候会覆盖原有的值,将原有的值返回;我们来说如果添加一个key不一样,但是计算出的数据下标一样的时候是如何存放这个数据的,HashMap的设计者其实以及早就想到这种情况下,针对这种情况,设计者在原有的数据结构上增加了一个单向链表的数据结构来存储,就是如果当hash冲突的时候,那么在这个冲突的数据下标位置处,增加一个链表,这个链表是单向的,只有next,没有prev,所以添加链表也是一直next,所以当同一个数据下标位置处出现同个hash冲突的时候,默认添加到头结点,什么意思呢?我们来看个图就知道了:
图(1)
HashMap的底层数据结构是用一个数组来存储元素的,每一个元素都是一个Entry读写,Entry对象中封装了key和value以及key的hash值和元素指针,具体的后面看代码解读;
看图(1)所示,图(1)所示的表示我们存储的的元素都没有出现hash冲突就是这样存储的,但是每一个Entry是如何存储到指定的位置的呢?这个计算就比较复杂一点,后面代码会有解读,这边先把原理解读一下,比如我们put一个元素,那么会根据这个key生成一个hash值,但是这个hash值是一个很大的Integer数据,设计者不可能就把这个这大的值存到下标对应的位置上,会对这个值做一些处理,处理过程后面会源码会讲,反正处理的过程就是比如我们计算出的hash值是1409876,那么目前数据的长度是8,那么通过1409876和数据的长度8-1=7计算出位置是0-7的一个下标位置(这个0-7是要平均出现的)然后将这个值put到这个位置上,当下一次出现计算出的位置下标发现这个位置已经存储了元素的时候,那么这个时候就被称作为hash冲突,就需要采用单向链表的形式进行存储了,具体看下图:
上图只是我的个人理解,当出现第一次冲突的时候会在冲突的位置上设置一个指针next,指向下一个元素,如果出现第二次冲突会在头结点生成一个新的Entry元素,因为是单向链表,没有prev指针,所有需要移动,也就是演变成了头结点插入的方式,为什么要这么做?后面源码解读,现在来分析下HashMap的源码。
JDK7 HashMap源码分析
属性信息
/**
* The default initial capacity - MUST be a power of two.
*/
//Hashmap的初始化大小,初始化的值为16,1往右移4位为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
//HashMap是动态扩容的,就是容量大小不能大于 1<<30
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
//默认的扩容因子,这个值可以通过构造修改
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* An empty table instance to share when the table is not inflated.
*/
//空数据,默认构造的时候赋值为空的Entry数组,在添加元素的时候
//会判断table=EMPTY_TABLE ,然后就扩容
static final Entry<?,?>[] EMPTY_TABLE = {};
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
//表示一个空的Hashmap
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
/**
* The number of key-value mappings contained in this map.
*/
//Hashmap的大小
transient int size;
/**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
//初始容量,如果构造传入的值是12,那么这个值就是12,如果没有传入就是默认的容量
//DEFAULT_INITIAL_CAPACITY=16
//扩容的阈值
int threshold;
/**
* The load factor for the hash table.
*
* @serial
*/
//扩容因子,没有传入就使用默认的DEFAULT_LOAD_FACTOR = 0.75f
final float loadFactor;
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
//数据操作次数,用于迭代检查修改异常
transient int modCount;
/**
* The default threshold of map capacity above which alternative hashing is
* used for String keys. Alternative hashing reduces the incidence of
* collisions due to weak hash code calculation for String keys.
* <p/>
* This value may be overridden by defining the system property
* {@code jdk.map.althashing.threshold}. A property value of {@code 1}
* forces alternative hashing to be used at all times whereas
* {@code -1} value ensures that alternative hashing is never used.
*/
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
构造方法
//这个构造方法是传入初始容量和扩容因子
//当需要扩容的时候,根据扩容因子计算进行扩容
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//当初始容量太大,大于了允许的最大值时,使用最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//判断加载因子必须是大于0,而且必须是数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//初始化时将一个Map集合放入新创建的HashMap
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
put方法
这个方法需要逐行来解读,否则根本就理解不了,还有JDK的底层源码很多都用到了位运算,位运算如下表:
总结下左移右移:
左移<<:高位舍掉,低位补0
右移>>:低位舍掉,高位补0;
public V put(K key, V value) {
//我们在实例化HashMap的时候,只是将初始化容量值赋值了
//但是没有初始化我们的数组,所以第一次进来的话,
//那么table==EMPTY_TABLE肯定是成立的
//而且会初始化数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
//这里是得到通过key计算出来的hash值,这个hash值通过
//位移运算和hashseed进行位运算得到
int hash = hash(key);
int i = indexFor(hash, table.length);
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;
}
}
//和快速失败有关
modCount++;
addEntry(hash, key, value, i);
return null;
}
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//官方的解释:找到一个大于等于toSize
//的2的幂次方,什么意思呢?看解读1.1
int capacity = roundUpToPowerOf2(toSize);
//记录下一次扩容的阈值大小,
//这边计算是通过本次初始的容量* 扩容因子
//得到的值作为下一次扩容的阈值大小
//当添加元素的数组大小大于等于阈值了,就需要进行扩容
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
//解读1.2
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
解读1.1
int capacity = roundUpToPowerOf2(toSize):这句话在官方的解释就是找到一个数,这个数是2的幂次方并且大于等于toSize,什么意思呢?toSize是初始化的容量大小,
所以简单来理解就是比如我们传入传入的容量容量是11,那么最终初始化的容量就是16,如果是15,也是16,如果是17,则就是32,反正就是2的幂次方;
解读1.2
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
这行代码的使用了两个三元运算符,第一个运算符比较简单,就是看我们要设置的数组大小是否大于了允许的最大容量,如果大于则使用最大的容量,否则又一个三元运算符,这个是重点,假如我这边传入的是11:
(num -1) << 1等价于 10 << 1也就是10左移一位,那么表示为
10 的二进制:00001010
10 << 1:00010100 也就是20了
然后看Integer.highestOneBit
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);
}
我们传入的是20,二进制是00010100
i |= (i >> 1);
i: 00010100
i >> 1:00001010
|: 00011110
执行了i |= (i >> 1)得到的结果是00011110
i |= (i >> 2):
i: 00011110
i >> 2:00000111
|: 00011111
执行了i |= (i >> 2)得到的结果是00011111
i |= (i >> 4):
i: 00011111
i >> 4:00000001
|: 00011111
执行了i |= (i >> 4)得到的结果是00011111
i |= (i >> 8):
i: 00011111
i >> 8:00000000
|: 00011111
下面的16我就不移了,因为移动8位已经全是0了,”与“出来的永远是00011111,也就是31
return i - (i >>> 1):
i: 00011111
i>>>1: 00001111
i-(i >>> 1):00010000
所有最终的结果就是00010000,十进制就是16 ,而传入的初始容量是11,而实际初始化的容量大小是16,也就验证了“解读1”和官方的解释: Find a power of 2 >= toSize,找到一个2的幂次方数并且大于等于toSize,toSize就是我们传入的11,你可以传入17,18,19等数字去演算,反正结果肯定是要大于等于你传入的数字并且是2的幂次方。
其实这个操作的规律就是最终的结果不是4个0就是4个1,它将低位的1尽量的移动到高位去,不管你移动多少位,反正取 ”与“ 后的结果肯定还是 ”与“ 之前的结果。
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
这个方法就是hashmap计算key的hash值方法,其实这里比较复杂,大体意思就是将key的hash值计算出来然后做位运算,看代码知道位移了20、12这些,为什么要移动这么多,其实核心思想就是避免出现太多了hash冲突,你想哈,如果不位移这么多,那么计算出来的hash值大多数都一样,因为高位都是0,所以这样就会导致一个问题就是hash冲突太多,链表太长,所以位移位数多了以后,尽量将避免hash冲突,我理解的思想是这样的,这个方法就是尽量的避免不同的key会产生太多了hash冲突。
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
上面这个方法当key==null的时候调用,HashMap允许传入一个null作为key,至于为什么这样做,可能为了支持更好,而且如果是null的key,那么默认放在第一位,也就是数组为0的位置,那么这里会出现一个疑问就是,当你放置null key的时候,第0个位置已经被占用了,那么怎么办,这个时候就会存在第0个位置的链表上。
上面代码可以看出for循环中是从数组的第一个位置开始循环的,也就是说key = null的数据是放在数组为0的位置上或者数组为0的链表上;上面的这个方法是要返回一个值得,如果说我们添加key = null的数据的时候,这个null = key已经有了,那么会替换这个新的值,然后返回之前的值,所以HashMap的put方法是有返回值的,如果返回值不为空,则原来的HashMap中已经存在了这个key,并且已经覆盖了新的值,并且将旧值返回,如果返回null,则Hashmap中没有这个值存在。
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容的大小为原来数组长度的2倍,比如当前长度16,扩容
//后就是32
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//创建一个Entry存放我们添加的元素
createEntry(hash, key, value, bucketIndex);
}
上面这个方法就是添加元素,也就是put方法的核心逻辑处理,首先判断当前的map的size是否大于了这个阈值,这个阈值初始化长度=16(构造hashmap为空的情况下),当第一次put的时候会计算数组的初始长度然后这个阈值=16 * 扩容因子;
所以当size大于等于阈值过后,并且要添加的这个鼠标下标位置已经有值了就开始扩容;
扩容
//这个newCapacity默认是扩容前的2倍,
void resize(int newCapacity) {
//首先声明一个Entry数组用来存放原来的数组信息
Entry[] oldTable = table;
//得到原来的数组长度,然后判断扩容的大小是否已经达到了最大的长度,
//如果大于了数组的最大长度,那么就设置阈值为最大数组的长度,则下次无法再扩容了
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//声明新的数组信息
Entry[] newTable = new Entry[newCapacity];
//数据的元素转移,就是讲oldTable转移到newTable中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//这个方法是干什么的呢?初始看这个方法我也不太懂用来做什么
//简单来说就是currentAltHashing 和useAltHashing异或如果为true
//那么hashSeed就会重新赋值,那么什么时候为true呢?
//比如说第一次初始化的时候hashSeed肯定是0,所以currentAltHashing false
//而useAltHashing 主要是由Holder.ALTERNATIVE_HASHING_THRESHOLD来控制的
//根据 图(hashSeed)来看,主要由jdk.map.althashing.threshold这个参数控制
//这个参数可以在运行参数中设置 -Djdk.map.althashing.threshold=3,比如为3
//那么useAltHashing 就很有可能为true,那么switching就会ture,那么
//hashseed就会重新计算值,就不是0了,那么这个值到底有什么用呢?
//这个值的用途在transfer方法中,其实它的用法其实就是为了使我们的hash算法
//变得更复杂一些,也就是更散列一些,其实就是这个作用,更散列一些,计算出来的
//的下标也就更好一些,我是这样认为的,看客的你是否这样认为?欢迎进行交流
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;
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;
}
//扩容的核心方法,就是讲原来的数组复制到新的数组中
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//这里使用了2层循环来进行数组的复制,为什么要使用2层循环呢?
//因为hashmap是一般的数组结构,在数组元素上的单向链表结构,所以如果发生了数组
//的扩容,需要两层循环来操作
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
//hashSedd的判断
if (rehash) {
//使hash更散列一些
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//通过hash计算出来的下标,
//在新的数组中赋值给原来数组的next
//其实就是新的数组下标引用了原来的下标数据的引用地址
e.next = newTable[i];
//将本次元素与原来解除关系过后,将引用变成原有的地址
//具体看图
newTable[i] = e;
e = next;
}
}
}
图(hashSeed)
扩容数组移动图
上图就是transfer方法的2层循环执行的过程
CreateEntry
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
如果有需要扩容,扩容完成过后就进行最后一步添加Entry元素,我们看第二行代码,第二行代码是
Entry<K,V> e = table[bucketIndex],这个e可能为空,也可能不为空,当e为空的时候,那么证明这个下标上还没有元素被添加,那么bucketIndex位置上直接存放我们添加的元素即可;如果不为空,则证明bucketIndex上已经存在元素了,那么这个时候就要添加到bucketIndex的链表上,因为hashmap的链表默认是单向链表,Hashmap的单向链表默认是头插法,根据扩容的流程图就可以知道,所以当e不为空的时候,那么这个元素默认在头部,其他元素往下移,也就有了 table[bucketIndex] = new Entry<>(hash, key, value, e)这句代码的含义。
Entry的基本结构
我们就根据createEntry方法来了解下Entry的数据结构,大概了解一下即可
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
这是HashMap在put的时候最后调用createEntry的方法,这个方法有4个参数
int h:可以对应的hash值;
K k:key的明文值;
V v:put的值;
Entry<k,v> n:这个值可空,可不为空,如果put的时候,n不为空,那么n就是这个位置上的(包括链表上)的所有节点元素,如果为空,则也就为空,什么意思呢?就是这个位置有添加的一个当前put的元素,它的下一个是null,比如我们添加的元素是abc,如图:
get获取元素
get获取元素比较简单,就算没有看过源码,也大概能够猜出来,不就是根据key循环HashMap的Entry数组嘛,找到key相等的就返回,这是我初探HashMap源码时,带着这种思维去看的,所以看源码先要理解它的原理,带着原理快速过一遍源码,刚开始不要纠结细节,否则你看源码的兴趣可能在半小时左右,你专到一个方法细节里面出不来,半小时过后可能就失去兴趣了,所以要根据自己所猜的原理去快速过一遍,然后后面再慢慢理细节。
public V get(Object key) {
//如果Key为空,那么就找到Entry数组中key==null的一个元素Entry
//在HashMap中是允许Null key和Null value的
if (key == null)
return getForNullKey();
//循环Entry数组,根据找到对应的value
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
//这个方法比较简单,就是在table中找到一个null key的Entry,然后返回
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;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
//根据key找到Entry,这里的循环很简单,就是根据传进来的的key
//计算出来一个hash,然后通过IndexFor找到指定的位置
//然后在这个位置上循环单向链表,找到对应的key,然后返回这个
//key对应的Entry对象
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
删除元素remove
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
//根据key删除元素和get差不不大,都是新通过key计算hash
//然后再通过indexFor得到要删除的元素所在的位置
//然后循环,找到要删除元素的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;
}
在remove或者put的时候都有代码,modCount++和size++ / size–
size其实很好理解,就是Hashmap的真实元素个数,而不是数组长度,这个元素个数在一定程度上可能大于数组长度,因为有链表结构;
而这个modCount是什么呢?这个涉及到一个异常,修改异常ConcurrentModificationException,也就是fast-fail(快速失败)是HashMap的一种自我保护模式,就是说在迭代的时候是不循环对map进行添加或者删除的,所以个modCount表示的是对map的操作次数,只是改变数组元素时候会记录这个modCount,所以在迭代的时候声明一个期望值=modCount,然后每次迭代判断这个期望值是否还是等于modCout,如果不等于,则抛出异常,是一种快速失败的模式。
Hashmap的其他的api就不做记录了,其实都差不多,都是围绕Entry这个来展开进行操作的,没有什么太多太难的地方,理解原理和思想即可。