面试必备1:HashMap(JDK1.8)增、删、改、查以及扩容源码分析
首先附上我的几篇其它文章链接感兴趣的可以看看,如果文章有异议的地方欢迎指出,共同进步,顺便点赞谢谢!!!
Android framework 源码分析之Activity启动流程(android 8.0)
Android studio编写第一个NDK工程的过程详解(附Demo下载地址)
面试必备2:JDK1.8LinkedHashMap实现原理及源码分析
Android事件分发机制原理及源码分析
View事件的滑动冲突以及解决方案
Handler机制一篇文章深入分析Handler、Message、MessageQueue、Looper流程和源码
Android三级缓存原理及用LruCache、DiskLruCache实现一个三级缓存的ImageLoader
HashMap概述:
在分析HashMap的源码前,需要了解一下几个问题。
面试必备2LinkedHashMap的源码分析(基于JDK1.8)
1:HashMap的数据结构
我们需要先知道HashMap是基于什么样的数据结构来进行数据存储的,知道了这些我们再去看源码就容易的多。
散列表(哈希表) 我们常用的数据结构就是数组和链表,数组具有增删慢查找快的特点,而链表具有增删快查找慢 的特点;基于上述特点,HashMap 即想要查询效率快,又想增删效率高,基于这样的特点HashMap的数据结构就是通过数组和链表组成的散列链表。
一直到JDK7为止,HashMap的结构都基于一个数组以及多个链表的实现,hash值冲突的时候,就将对应节点以链表的形式存储;JDK8中,HashMap采用的是数组(位桶)+链表/红黑树组成,当同一个hash值的节点数不小于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树,提高查询效率,这就是JDK7与JDK8中HashMap实现的最大区别。
2:存储节点 : Node 和TreeNode
HashMap存储数据时如何组织出这样一个散列表呢?HashMap中将我们存入的每一个数据封装成一个Node类(Node节点),Node应该有以下下属性 :key--------键、value-----值 组成我们put的键值对,既然要组织起链表则必须有Node next属性、还有一个根据key算出的int 类型hash值,hash值用来确定该该节点所在数组的索引后面会进行详细描述。
/**
* 散列表的节点 Node 部分源码
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//用于确定该节点所在数组的位置, 下标
final K key;// 我们所操作的键
V value;// 值
Node<K,V> next;//存储下一个节点,通过next组织起链表
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;//位置
this.key = key;
this.value = value;
this.next = next;
}
/**
* 返回当前节点的根节点
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
当链表长度大于8时将链表转换成红黑树,红黑树节点TreeNode部分源码
static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left;//左子树
TreeNode<K,V> right;//右子树
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;//颜色属性
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
当我们放入或取出元素时大致流程是:
- 现根据hash函数去求出key的Hash值,至于求hash值得算法将在后面进行详细分析
- 根据上一步的hash值可以确定在数组的哪个位置,定下标(数组的索引)
- 根据索引从数组中获取数据,如果为NULL 则将该元素直接放入或取出(直接在数组中操作),如果该位置数据不为NuLL,则需要向该元素所在的链表进行数据的操作。
哈希冲突(哈希碰撞)
如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。
3:HashMap中散列表中数组(或位桶)如何表示?
HashMap源码中与数组相关的几个成员属性:
- 数组的表示:
transient Node<K,V>[] table;
- 数组的大小 : 数组的初始化需要一个指定的大小,在HashMap中 数组的大小永远是 2的幂次方(原因将在源码中分析)
- 数组的默认大小:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 , 1 << 4 二进制中1左移四位就是2^4=16, 不直接写16的原因是,效率高,计算机最终读取的二进制
; - 数组的最大容量:
static final int MAXIMUM_CAPACITY = 1 << 30//最大为2^30,使用位移操作符的原因也是效率高 ;
- 数组扩容因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f//用于确定数组扩容的临界值;
- 以存放数据大小:
transient int size//记录 hashMap 当前存储的元素的数量;
- 数组扩容的临界值:
int threshold;//数组已使用量size>threshold时需要扩容,threshold=数组大小*扩容因子提升效率,没有必要等到数组都用完了在进行扩容,每次扩大2倍
**注意:数组的扩容是在当已使用量大于数组容量*扩容因子(默认0.75)**时进行扩容
4:HashMap中散列表中链表的长度限制:长度越长存储查询效率低的问题
链表越长,期查询效率越低,所以链表的长度也有一个限制,我们称为阈值 ,JDK1.8的时候,链表的长度大于阈值,将其结构改成一个红黑二叉树,以提升差值效率,同时也伴随者性能的损耗增加
HashMap源码中与链表相关的几个成员属性:
- 转换红黑树的阈值:
static final int TREEIFY_THRESHOLD = 8;//阈值 链表越深查找存储效率都低 ,超过8以后将其转换成红黑二叉树 ,提升查找效率
- 转换成链表的阈值
static final int UNTREEIFY_THRESHOLD = 6; //当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
static final int MIN_TREEIFY_CAPACITY = 64;//当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。
6:新的数据封装成的Node对象如何去存储:
根据key计算hashCode ----》int类型的值,hash(key)然后就知道要存在数组的那个位置
5:HashMap中的属性的概述
这里将对HashMap属性进行描述,以便于理解源码
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
//序列号,序列化的时候使用。
private static final long serialVersionUID = 362498820763181265L;
/**默认容量,1向左移位4个,00000001变成00010000,也就是2的4次方为16,使用移位是因为移位是计算机基础运算,效率比加减乘除快。**/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,2的30次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
//加载因子,用于扩容使用。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当某个桶节点数量大于8时,会转换为红黑树。
static final int TREEIFY_THRESHOLD = 8;
//当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
static final int UNTREEIFY_THRESHOLD = 6;
//当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。
static final int MIN_TREEIFY_CAPACITY = 64;
//存储元素的数组,transient关键字表示该属性不能被序列化
transient Node<K,V>[] table;
//将数据转换成set的另一种存储形式,这个变量主要用于迭代功能。
transient Set<Map.Entry<K,V>> entrySet;
//元素数量
transient int size;
//统计该map修改的次数
transient int modCount;
//临界值,也就是元素数量达到临界值时,会进行扩容。
int threshold;
//也是加载因子,只不过这个是变量。
final float loadFactor;
....
}
HashMap源码分析:
了解了概述中的几个概念后,将从对HashMap的put、get、remove三个方法进行源码分析。
1:HashMap的put(key,value)方法源码
put方法的大致流程:
- 根据传入的key,计算hash值
- 判断键值对数组tab[]是否为空或为null,否则以默认大小resize()进行数组的初始化操作;
- 根据键值key计算hash值得到插入的数组索引i,如果tab[i]==null,直接新建节点添加,否则转入3
- 判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理
//返回值扔然为Value
public V put(K key, V value) {
//1:hash(key)先找存在哪里,先算key的hash值
//2: putVal(hash(key), key, value, false, true)方法进行存储
return putVal(hash(key), key, value, false, true);
}
//根据key求出Hash值,通过这去求散列表的下标
static final int hash(Object key) {
int h;//32位数,不足高位补0
// (h = key.hashCode()) ^ (h >>> 16)
// 1: key的HashCode h
//2: h先做了个位移运算,向右边位移16位即h >>> 16
/// 3:然后将两个值进行异或预算 充分利用hash的每一位数,将h的高16位异或h的低16位得到一个值, 使得高位也可以参与hash,更大程度上减少了碰撞率。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
putVal方法源码:
//,如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value。如果evict是false。那么表示是在初始化时调用的
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;// 数组 当前操作的节点
if ((tab = table) == null || (n = tab.length) == 0)//数组table为null
n = (tab = resize()).length;//resize() 作用1:初始化数组 n记录数组的大小
if ((p = tab[i = (n - 1) & hash]) == null) // tab[i = (n - 1) & hash]==null, 即数组tab[i]==null,没有元素直接新建节点添加
/**
* 根据hash和数组长度算出数组下标i table[i]取出对应元素
* i = (n - 1) & hash 算出数组下标的优点?
* 1:防止数组越界:这么写是为了解决hash有可能越界问题 ,即hash不能大于n 数组长度,
* 数组的大小n一定是2几次幂,只有这样n-1,n-1二进制表示时每位的值都为1,从而保证在(n-1)&hash值的值永远<=n-1 ,保证不会数组越界
* 例如:当n=16默认值时,n - 1=15 的二进制 01111 和前面的hash值进行与运算 的值永远小于等于01111(15),不会越界
* 2:提高效率:(n - 1) & hash等价于 模/n取余 这样写是因为与运算比模运算效率高
* 3: 数组的大小n一定是2几次幂,n-1 的二进制形式每位都是1,在&hash时导致数组的散列性变大,降低了hash碰撞的概率
*/
tab[i] = newNode(hash, key, value, null);
else {//tab[i]!=null需要向链表或红黑树中存放
Node<K,V> e; K k;
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
// 如果这个元素的key与要插入的一样,那么就替换
e = p;
else if (p instanceof TreeNode)
//1.如果当前节点是TreeNode类型的数据,执行putTreeVal方法, 向红黑树中存放
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//遍历这条链子上的数据,跟jdk7没什么区别
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//即table[i].next==null,table[i]就是尾节点
//向链表尾部添加数据
p.next = newNode(hash, key, value, null);
//项链表中添加完元素后判断,链表长度是否超过8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//长度超过8,执行treeifyBin方法,将链表转换成红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
++modCount;
//每次添加完成后,判断阈值,决定是否扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);//这个为子类HashMap服务,进行lru排序,在这里无需研究
return null;
}
treeifyBin方法源码:将链表转换成红黑二叉树
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();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);//将Node转成TreeNode
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);
}
}
2:HashMap的扩容机制:resize()源码
resize的作用:1:初始化数组table 2: 扩容
构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比*Node.length)重新调整HashMap大小 变为原来2倍大小,扩容很耗时
- 每次扩展的时候,都是扩展2倍;
- 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
//resize 作用1:初始化数组 2扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;//记录数组的长度
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {//数组大于0进行扩容操作
if (oldCap >= MAXIMUM_CAPACITY) {//如果容量已大于最大值
threshold = Integer.MAX_VALUE;//修改扩容临界值
return oldTab;
}
//如果oldCap << 1扩大两倍,满足扩容的条件
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 左移1位扩大两倍容量
}
// 如果旧表的长度的是0,就是说第一次初始化表
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//数组的初始化
newCap = DEFAULT_INITIAL_CAPACITY;//默认大小
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//
}
if (newThr == 0) {//新表长度乘以加载因子
float ft = (float)newCap * loadFactor;//新的数组容量
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;//threshold 容量*负载因子 =临界值
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//初始化数组 一开始默认到校
table = newTab;//把新表赋值给table
if (oldTab != null) {//原表不是空要把原表中数据移动到新表中
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)//说明这个node没有链表直接放在新表的数组e.hash & (newCap - 1)位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果是红黑树则,转为链表
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//如果e后边有链表,到这里表示e后面带着个单链表,需要遍历单链表,将每个结点重新计算在新表的位置,并进行搬运
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),可以得到哈希值去模后,
//是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,
//否则存放在高位。这里又是一个利用位运算 代替常规运算的高效点
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) {//lo队不为null,放在新表原位置
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {//hi队不为null,放在新表j+oldCap位置
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;//返回初始化或者扩容后的数组
}
3:小结:
- 运算尽量都用位运算代替,更高效。
- 对于扩容导致需要新建数组存放更多元素时,除了要将老数组中的元素迁移过来,也记得将老数组中的引用置null,以便GC
- 取下标 是用 哈希值 与运算
(桶的长度-1) i = (n - 1) & hash
。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高 - 扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
- 因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,
即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量
- 利用哈希值 与运算 旧的容量 ,
if ((e.hash & oldCap) == 0)
,可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位
。这里又是一个利用位运算 代替常规运算的高效点 - 如果追加节点后,链表数量》=8,则转化为红黑树
- 插入节点操作时,有一些空实现的函数,用作LinkedHashMap重写使用。
4:HashMap的get(key)方法源码分析:
下面简单说下 get(key) 的过程:
get(key)方法时获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否与参数key相等,不等就遍历后面的链表找到相同的key值返回对应的Value值即可,即先根据hash值确定数组(哈希桶)位置,然后根据key相等取值 , hash相等&&key相等的节点
//传入键 key
public V get(Object key) {
Node<K,V> e;
//根据hash函数得到的hash值和key去 getNode(hash(key), key) 返回null或者Node.value
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode(hash, key)方法源码分析:
//根据 hash 和key获取node节点
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;//在tab数组中经过散列的第一个位置
// table已经初始化,长度大于0,根据hash寻找table中的项也不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash &&// 判断是不是第一个节点,是的话从数组中返回
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//否则
if ((e = first.next) != null) {
// 取出first的下一个节点,如果为为红黑树结点
if (first instanceof TreeNode)
// 在红黑树中查找
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:remove方法源码分析
public V remove(Object key) {
Node<K,V> e;
//这里传入了value 同时matchValue为true
return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
}
removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable)源码
/**
* Implements Map.remove and related methods
* @param hash 哈希值
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue 为true的话,则表示删除它key对应的value,不删除key
* @param movable 如果为false,则表示删除后,不移动节点,为true则移动节点
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//tab 哈希数组,p 数组下标的节点,n 长度,index 当前数组下标
Node<K,V>[] tab; Node<K,V> p; int n, index;
//哈希数组不为null,且长度大于0,然后获得到要删除key的节点所在是数组下标位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node 存储要删除的节点,e 临时变量,k 当前节点的key,v 当前节点的value
Node<K,V> node = null, e; K k; V v;
//如果数组下标的节点正好是要删除的节点,把值赋给临时变量node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//也就是要删除的节点,在链表或者红黑树上,先判断是否为红黑树的节点
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
//遍历红黑树,找到该节点并返回
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else { //表示为链表节点,一样的遍历找到该节点
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
/**注意,如果进入了链表中的遍历,那么此处的p不再是数组下标的节点,而是要删除结点的上一个结点**/
p = e;
} while ((e = e.next) != null);
}
}
//找到要删除的节点后,判断!matchValue,我们正常的remove删除,!matchValue都为true
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//如果删除的节点是红黑树结构,则去红黑树中删除
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//如果是链表结构,且删除的节点为数组下标节点,也就是头结点,直接让下一个作为头
else if (node == p)
tab[index] = node.next;
else /**为链表结构,删除的节点在链表中,把要删除的下一个结点设为上一个结点的下一个节点**/
p.next = node.next;
//修改计数器
++modCount;
//长度减一
--size;
/**此方法在hashMap中是为了让子类去实现,主要是对删除结点后的链表关系进行处理**/
afterNodeRemoval(node);
//返回删除的节点
return node;
}
}
//返回null则表示没有该节点,删除失败
return null;
}
HashMap的遍历entrySet()
entrySet()源码:
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
//直接返回成员变量entrySet==null时 new EntrySet()并返回,否则直接返回
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
集合的遍历主要是通过内部类EntrySet
的iterator()
方法实现,EntrySet的部分源码如下:
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
//返回EntryIterator类型的迭代器
return new EntryIterator();
}
....省略部分源码...
}
iterator方法返回EntryIterator类型的迭代器其源码如下
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
//Iterator的`next`方法就是调用父类`HashIterator`的`nextNode`方法获取下一个节点
public final Map.Entry<K,V> next() { return nextNode(); }
}
nextNode方法是父类HashIterator
提供的方法,即HaapMap的迭代的核心就是HashIterator
源码的源码如下,这里是重点!!!
abstract class HashIterator {
Node<K,V> next; // 下一个节点
Node<K,V> current; // 当前节点
int expectedModCount; // HashMap是线程不安全的,在迭代时通过expectedModCount去记录成员变量modCount,判断迭代过程中的并发修改异常
int index; // 当前数组的索引
/**
* 构造器中对属性进行了初始化,
* 并通过while循环得到数组中的第一个不为空的元素下标以及值,并将此元素值赋给next
*/
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
//while循环得到数组中的第一个不为空的元素下标以及值,并将此元素值赋给next
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
//判断是否有下一个节点
public final boolean hasNext() {
return next != null;
}
/**
* nextNode()的方法,就是Iterator中next方法中调用的,即Iterator中next方法就是nextNode方法
* 其遍历HashMap过程就是,就是依次遍历数组中的链表,取出下一个节点
*/
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;//e,为当前要返回的节点
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//HashMap遍历的核心思想
//e为当前要返回的节点,
//获取e的下一节点,并赋值给next,如果此时的next为空,则说明当前链表已经遍历完毕,
//那么进入判断体,开始通过while遍历下一个数组桶中的链表,并将此链表的头结点赋值给next;
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
/**
* Iterator的remove删除方法 本质上还是调用了HashMap的removeNode方法
* 只是在调用之前,通过modCount != expectedModCount时抛出并发修改异常,处理线程不安全问题,
* 如果相等则调用HashMap的removeNode方法移除节点
*
*/
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)//处理多线程并发问题
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
HashMap的遍历过程就是通过nextNode取出下一个节点的过程,它的核心是想就是依次遍历哈希桶中的链表实现。
总结
到此为止,HashMap原理分析完毕,阅读时需要从第一部分先看,然后再看第二部分的源码分析,具体的总结在概述和小结中都已经描述清楚,这里不做过多赘述。这是周末看的HashMap源码的理解,里面有不对的对方欢迎大家留言指处,共同进步,后续会陆续将LinkedHashMap、CurrentHashMap的源码分享出来以供参考。
趁热打铁接下来可以看一下我的另一片文章:面试必备2LinkedHashMap的源码分析(基于JDK1.8)