一、HashMap概述
1、HashMap概述
HashMap以key-value形式存储数据,key允许为null,value也允许为null。
HashMap线程不安全,需要线程安全的HashMap可以使用ConcurrentHashMap。
HashMap底层是数组,又被称为hash桶,每个桶中存放的是链表,链表中的节点存储着集合中的元素。
JDK 1.8中当hash桶中链表长度达到 8 的时候会转成红黑树。
当HashMap中的元素数量达到threshold阈值的时候,会触发扩容,hash桶的长度翻倍。
HashMap中hash桶的长度一定是 2 ^ n。
HashMap中对于key落在哪个桶上,不仅仅会计算hashCode,还会经过扰动函数扰动使得key的分布更加均匀。
今天下面所有的分析都是基于JDK 1.8 的HashMap。
2、HashMap计算key在hash桶中落点的算法
HashMap的get方法会根据key的hashCode值计算出这个key的落点在hash桶的那个位置上。
通常这种计算hash值落点算法可以使用取余法,根据余数选择落点位置。
但是HashMap考虑到新能问题,利用了位运算的一个特性,将Hash桶的长度规定为 2 ^ n,然后利用tab[(tableSize - 1) & hash来得出不同hash值在hash桶中的落点位置。
3、hash()方法的扰动函数
static final int hash(Object key) {
int h;
return (key == null) ? : (h = key.hashCode()) ^ (h >>> );
}
hash()方法使用来获取key的hash值的,我们知道是哈希函数映射的比较均匀的话,碰撞的概率会小一些。
我们可以看到hash()方法中对key的hashCode值还进行了异或运算,下面来分析一下这样做的原因。
通过上面的分析我们知道计算key再hash桶中的落点采用了tab[(tableSize - 1) & hash算法,也就是取hash值的后几位来作为判断hash桶落点的依据。其实也就是对tableSize进行取余。
而hashCode的取值范围是Integer.MAX_VALUE,如果对tableSize进行取余的话,会忽略掉高位的数,只有低位的几个数会参与运算。
所以采用了异或运算,将 hashCode ^ (hashCode >>> 16) 将会使得hashCode的每一位都参与到取余运算中,这样就会使得哈希函数映射更加的均匀,降低hash碰撞的概率。
JDK 1.7中的扰动函数:
final int hash(Object k) {
int h = hashSeed;
if ( != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> ) ^ (h >>> );
return h ^ (h >>> ) ^ (h >>> );
}
JDK 1.7中的扰动函数会对key进行四次扰动,可能考虑到过多次的扰动意义其实不大,所以JDK 1.8中将扰动函数修改为一次就好。
4、HashMap的构造方法
HashMap一共有四个构造方法:
public HashMap(){...}
public HashMap(int initialCapacity){...}
public HashMap(int initialCapacity, float loadFactor) {...}
public HashMap(Map<? extends K, ? extends V> m) {...}
其中前三个构造方法都是初始化一个空的HashMap,第四个构造方法会构造一个HashMap然后将参数中的 m 中的元素一一put到构造出来的HashMap中。
下面先来了解一下前三个构造方法:
要理解HashMap的构造方法需要先了解几个常量个成员变量
//hash桶的默认长度
static final int DEFAULT_INITIAL_CAPACITY = << ; // aka 16
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = f;
//HashMap中元素数量
transient int size;
//hash桶,用来存放链表和红黑树
transient Node<K,V>[] table;
//HashMap中元素的阈值,当size > threshold时,将发生扩容
int threshold;
//加载因子
final float loadFactor;
其中table被transient修饰,所以HashMap在序列化的过程中不会序列化size属性。
其中size被transient修饰,所以HashMap在序列化的过程中不会序列化size属性。
其中loadFactor被final修饰,所以一个HashMap对象的loadFactor一旦在对象初始化的时候被设置,就不能再进行修改。
第一个构造方法其实就是HashMap(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR)
第二个方法其实就是HashMap(initialCapacity, DEFAULT_LOAD_FACTOR)
第三个构造方法这两个变量都由HashMap对象创建的时候传入。
我们可以发现在初始化构造HashMap的时候并没有给table数组赋值,所以初始化HashMap的时候hash桶的size是 0,需要在第一次put操作的时候才会给hash桶赋值。这段赋值代码在resize方法中,下面讲到resize方法的时候再一起讲。
这里我们针对第三个构造方法拿出来讲一下:
public HashMap(int initialCapacity, float loadFactor) {
//initialCapacity合法性校验
if (initialCapacity < )
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//initialCapacity最大值校验
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//loadFactor合法性校验
if (loadFactor <= || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//设置HaspMap对象的loafFactor
this.loadFactor = loadFactor;
//设置HaspMap对象的threshold
this.threshold = tableSizeFor(initialCapacity);
}
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - ;
n |= n >>> ;
n |= n >>> ;
n |= n >>> ;
n |= n >>> ;
n |= n >>> ;
return (n < ) ? : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + ;
}
tableSizeFor这个方法使用了位运算的技巧,将返回第一个大于cap并且满足2 ^ n的数。
调用这个方法是为了将threshold设置为满足 2 ^ n 的数,之后在第一个put操作的时候会将这个threshold作为hash桶的size。
这样我们就成功的控制了hash桶的size满足 2 ^ n。
5、链表节点Node
下面我们来看一下链表中的节点Node,至于红黑树中的节点TreeNode这里不做介绍:
static class Node<K,V> implements Map.Entry<K,V> {
//存放元素key的hash值
final int hash;
//存放元素的key
final K key;
//存放元素的value
V value;
//指向链表中下一个Node
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//覆盖链表中元素的时候返回旧值
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//重写equals方法,只有当key和value都相等时返回true
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
其中Node节点中存放一个hash字段来记录hash值而不是每次使用的时候再计算是因为每个Node的hash值都需要经过扰动函数的扰动,并且扩容的时候需要计算每个Node的hash值,所以出于空间换时间的想法,在Node节点中加入了hash字段。
6、扩容方法
当table没有初始化时进行put操作或者size > threshold的时候会进行扩容
final Node<K,V>[] resize() {
//当前hash桶
Node<K,V>[] oldTab = table;
//当前hash桶的大小
int oldCap = (oldTab == null) ? : oldTab.length;
//当前hashMap的阈值
int oldThr = threshold;
//初始化新的hash桶的大小和hashMap阈值
int newCap, newThr = ;
//如果当前hash桶的大小大于0
if (oldCap > ) {
//如果当前hash桶的大小到达最大值,不再进行扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果当前hash桶的大小未达到最大值,将新的hash桶大小翻倍
else if ((newCap = oldCap << ) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//如果旧的hash桶大小 >= 16,那么新的hashMap阈值也翻倍
newThr = oldThr << ; // double threshold
}
//如果当前hash桶大小为 0,但是阈值不为 0
//那么表示初始化的HashMap对对象进行扩容
else if (oldThr > )
//将当前hashMap的阈值赋值给新hash桶的大小
//此时阈值和hash桶大小一致
newCap = oldThr;
//如果当前hash桶的大小和hashMap阈值都是0
else {
//设置新hash桶大小为默认值 16
newCap = DEFAULT_INITIAL_CAPACITY;
//设置新hashMap阈值为 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果当前hashMap的阈值为 0
if (newThr == ) {
//根据当前hash桶的大小和加载因子计算新的阈值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//更新阈值
threshold = newThr;
//根据新hash桶的大小构建新的Node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将新构建的Node数组赋值给table
table = newTab;
//省略部分是将当前hash桶中所有的节点转移到新的hash桶中
...
return newTab;
}
上面这段代码分析了扩容过程中关于hash桶的大小以及hashMap阈值的变化,看完这段可以比较清晰的了解扩容时Node数组的变化。
二、HashMap扩容详细分析
其中关于扩容过程集合中的元素从原hash桶中向新hash桶的转移过程没有介绍,下面来介绍一下扩容时元素的转移过程。
1、JDK 1.7中HashMap扩容过程
所以这里先简单分析一下JDK 1.7中的扩容过程:
void resize(int newCapacity) {
//当前hash桶
Entry[] oldTable = table;
//当前hash桶大小
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
//如果当前hash桶大小达到上限,将阈值设置为Integer.MAX_VALUE
threshold = Integer.MAX_VALUE;
return;
}
//根据传入的newCapacity创建新的hash桶数组
Entry[] newTable = new Entry[newCapacity];
//专业元素
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//将新构建的Node数组赋值给table
table = newTable;
//更新阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + );
}
//转移元素方法
void transfer(Entry[] newTable, boolean rehash) {
//新hash桶大小
int newCapacity = newTable.length;
//遍历当前hash桶
for (Entry<K,V> e : table) {
//如果桶中元素不是空,进入循环
while(null != e) {
//使用next指针指向链表下一个节点
//防止下一个节点丢失
Entry<K,V> next = e.next;
//根据hashseed的值来判断是否需要使用rehash来减少Hash碰撞
if (rehash) {
e.hash = null == e.key ? : hash(e.key);
}
//找出key需要存放在新hash桶中的位置
int i = indexFor(e.hash, newCapacity);
//将当前转移的元素插入到新hash桶指定位置链表表头
e.next = newTable[i];
newTable[i] = e;
//转移当前转移元素在旧hash桶中的下一个元素
e = next;
}
}
}
//使用 & 运算代替取余算法计算需要分配的has桶位置
static int indexFor(int h, int length) {
return h & (length-);
}
上面这一段是JDK 1.7中对HashMap扩容使用的算法,简单概括一下扩容流程:
- 计算新hash桶的大小和阈值
- 根据新hash桶的大小生成新的hash桶数组
- 对当前hash桶中的元素进行转移
- 遍历hash桶
- 遍历制定下标hash桶中的待转移节点
- 根据节点hash值算出在新hash桶中的下标
- 使用头插法将待转移的节点插入到新hash桶中的单链表上
- 将新hash桶以及新hash桶的大小以及阈值设置到当前HashMap对象
2、JDK 1.7中HashMap扩容可以优化的地方
下面讲一下JDK 1.7中HashMap扩容过程中可以优化的地方,也就是JDK 1.8中优化的地方。
头插法
下关于待转移元素插入新hash桶时采用了头插法,在之前的文章待转移元素位置的选择
我们假设一个key的hash值是 x,当前hash桶的大小是 n,这个key在当前hash桶中的位置 i = x / n(其实HashMap采用的是 x & (n-1),这里为了好理解采用取余算法);
扩容的时候新hash桶的大小将变成 2n,那么这个key在新hash桶中的位置 j = x / 2n。
我们可以很轻易的得出 j = i 或者 j = i + n。
所以原hash桶中某个下标为 i 的链表中的节点在扩容后一定会落在新hash桶中下标为 i 或 i + n 的位置上。
那么如果 x & n == 0 表示这个key会落在新hash桶中下标为 i 的位置,如果 x & n != 0 表示这个key会落在新hash桶中下标为 i + n 的位置。
链表太长,查询效率会低
如果链表长度太长,那么HashMap的查询效率会降低,可以考虑树结构。
3、JDK 1.8 HashMap扩容过程
final Node<K,V>[] resize() {
//当前hash桶
Node<K,V>[] oldTab = table;
//当前hash桶的大小
int oldCap = (oldTab == null) ? : oldTab.length;
//当前hashMap的阈值
int oldThr = threshold;
//初始化新的hash桶的大小和hashMap阈值
int newCap, newThr = ;
//如果当前hash桶的大小大于0
if (oldCap > ) {
//如果当前hash桶的大小到达最大值,不再进行扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果当前hash桶的大小未达到最大值,将新的hash桶大小翻倍
else if ((newCap = oldCap << ) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//如果旧的hash桶大小 >= 16,那么新的hashMap阈值也翻倍
newThr = oldThr << ; // double threshold
}
//如果当前hash桶大小为 0,但是阈值不为 0
//那么表示初始化的HashMap对对象进行扩容
else if (oldThr > )
//将当前hashMap的阈值赋值给新hash桶的大小
//此时阈值和hash桶大小一致
newCap = oldThr;
//如果当前hash桶的大小和hashMap阈值都是0
else {
//设置新hash桶大小为默认值 16
newCap = DEFAULT_INITIAL_CAPACITY;
//设置新hashMap阈值为 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果当前hashMap的阈值为 0
if (newThr == ) {
//根据当前hash桶的大小和加载因子计算新的阈值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//更新阈值
threshold = newThr;
//根据新hash桶的大小构建新的Node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将新构建的Node数组赋值给table
table = newTab;
if (oldTab != null) {
//遍历当前hash桶
for (int j = ; j < oldCap; ++j) {
//定义节点e用来指向待转移的节点
Node<K,V> e;
//如果当前下标为 j 的桶中没有元素,直接结束
if ((e = oldTab[j]) != null) {
//将当前下标为 j 的桶释放
oldTab[j] = null;
//如果当前桶的中只有一个节点
//直接计算出该key在新hash桶中对应的位置
if (e.next == null)
//这里又一次使用了 & 代替取余算法
newTab[e.hash & (newCap - )] = e;
//如果当前节点存在于树中而不是链表中
else if (e instanceof TreeNode)
//采用红黑树的转移逻辑,这里暂不做介绍
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果当前节点存在于链表中而不是树中
else {
//定义低位链表的头尾节点
Node<K,V> loHead = null, loTail = null;
//定义高位链表的头尾节点
//其中高位链表在hash桶中的下标比低位节点大oldCap
Node<K,V> hiHead = null, hiTail = null;
//定义临时节点
Node<K,V> next;
do {
//使用next指针指向链表下一个节点
//防止下一个节点丢失
next = e.next;
//利用 & 运算得出key落在低位链表中
if ((e.hash & oldCap) == ) {
//如果低位链表为空
//直接将待转移元素置为表头
if (loTail == null)
loHead = e;
//低位链表不为空
//采用尾插法将待转移元素插入到低位链表中
else
loTail.next = e;
//更新低位链表尾指针
loTail = e;
}
//利用 & 运算得出key落在高位链表中
else {
//执行逻辑和低位链表一致,不再赘述
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
//循环直到当前hash桶对应下标中没有元素为止
} while ((e = next) != null);
//如果低位链表不为空
//将低位链表赋值给低位hash桶中
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//如果高位链表不为空
//将低位链表赋值给高位hash桶中
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
上面这一段是JDK 1.8中对HashMap扩容使用的算法,简单概括一下扩容流程:
- 计算新hash桶的大小和阈值
- 根据新hash桶的大小生成新的hash桶数组,如果当前hash桶为空,构造一个长度默认的hash桶(JDK 1.7中在扩容前会先检查当前hash桶是否为空)
- 将新hash桶以及新hash桶的大小以及阈值设置到当前HashMap对象
- 对当前hash桶中的元素进行转移
- 遍历hash桶
- 遍历指定下标hash桶中的待转移节点
- 如果指定下标hash桶中待转移节点只有一个,直接计算在新hash桶中的落点并转移到新hash桶中
- 如果指定下标hash桶中存储的是树,按照树的结构来转移(暂不做介绍)
- 如果指定洗标hash桶中存的是链表
- 创建低位链表头尾指针和高位链表头尾指针
- 将待转移元素按照尾插法插入到低位链表和高位链表中
- 将低位hash桶和高位hash桶分别指向低位链表和高位链表
- 返回新的hash桶
4、JDK 1.7 和 JDK 1.8关于HashMap扩容的区别
- JDK 1.7在扩容前会判断hash桶是否为空,如果为空会提前创建;JDK 1.8会在扩容时检查hash桶是否为空
- JDK 1.7采用头插法将待转移元素插入到新hash桶链表中,多线程场景下会导致死循环;JDK 1.8采用尾插法,规避了死循环的情况
- JDK 1.7扩容后链表中的元素会倒置;JDK 1.8扩容后链表中的元素不会倒置
- 由于JDK 1.8对于链表长度达到 8 时会转成红黑树,所以扩容过程中考虑到了红黑树的情况
- JDK 1.8在链表转移的时候引入了低位链表和高位链表进行转移,效率更高
虽然JDK 1.8中规避了HashMap扩容过程中遇到的死循环问题,但是HashMap仍然是线程不安全的,JDK 1.8只是对HashMap做了一些优化,但是多线程场景下还是不能使用HashMap的。
三、HashMap常用方法源码
下面再来分析一下HashMap中常用的方法。
下面部分文章基于JDK 1.8,就不再介绍JDK 1.7中的方法了。
1、get()方法
public V get(Object key) {
//定义临时节点用于返回结果
Node<K,V> e;
//先调用hash(key)方法获得key的hash值
//调用getNode方法找到Node节点
//如果Node节点不存在返回null,存在的话返回Node节点的value
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//使用扰动函数使得hash值分布更加均匀
static final int hash(Object key) {
int h;
return (key == null) ? : (h = key.hashCode()) ^ (h >>> );
}
//根据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[(n - 1) & hash]可以找到key对应在hash桶中的位置
//如果hash桶不为空并且key对应hash桶的位置上存在节点,进入遍历
if ((tab = table) != null && (n = tab.length) > &&
(first = tab[(n - ) & hash]) != null) {
//如果第一个节点的hash值和key值和当前查询的key一致,直接返回第一个节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//第一个节点的hash值或者key值和查询的key不一致
//并且存在下一个节点,执行下面方法
if ((e = first.next) != null) {
//如果当前节点在红黑树中
if (first instanceof TreeNode)
//调用getTreeNode方法获得红黑树中对应的节点(暂时不做介绍)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//如果当前节点不在红黑树中,也就是链表中,进入循环
do {
//如果循环中某个节点的hash值和key值一致,返回这个节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
//当不存在下一个节点的时候结束循环
} while ((e = e.next) != null);
}
}
//如果hash桶为空或者key对应hash桶的位置上不存在节点,返回null
return null;
}
上面这一段是HashMap中get()方法的源码,简单概括一下扩容流程:
- 计算需要查询的key的hash值
- 判断hash桶是不是空以及该hash值对应在hash桶上是否存在节点,不存在直接返回null
- 对比头节点的hash值和key是否和需要查询的key一致,如果一致直接返回头节点
- 判断头节点在红黑树中还是链表中
- 如果在红黑树中,则在红黑树中查找该节点
- 如果在链表中,则遍历链表查询该节点
下面来分析几个小细节:
细节一、先比较hash再比较key
我们发现判断HashMap中节点是否是需要查询的key的时候使用的判断条件是
e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))
会先比较一下hash值是否一致再比较key是否相等,这么做是也是为了查询效率。
因为通常key都会重写equals方法,通过equals方法比较的效率明显是比hash值比较要慢的。
所以再equals判断之前先比较一次hash值可以提前规避很多不符合的节点,效率上会高一些。
细节二、先比较头节点,再遍历链表(红黑树)
我们发现当取到key对应hash桶中的节点的时候,会先比较一下头节点是否是我们需要查询的节点,如果不符,再去遍历链表(红黑树)。
因为在hash算法足够分散的情况下,很多hash桶的每一个桶中都只会存在一个节点,所以先比较第一个节点再去遍历也能带来效率上的提升。
2、put()方法
public V put(K key, V value) {
//先调用hash(key)方法获得key的hash值
//调用putVal方法插入数据,并返回结果
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//定义临时变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果hash桶为空
if ((tab = table) == null || (n = tab.length) == )
//扩容并将扩容后hash桶的大小赋值给n
n = (tab = resize()).length;
//如果key对应hash桶的位置上没有节点
if ((p = tab[i = (n - ) & hash]) == null)
//将put进来的key,value生成一个Node节点
//并将该节点赋值给hash桶指定下标上
tab[i] = newNode(hash, key, value, null);
//如果key对应hash桶的位置上有节点
else {
//定义临时变量
Node<K,V> e; K k;
//如果头节点的hash值和key和需要put的key一致
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//将头节点赋值给临时变量p,用于下面返回
e = p;
//如果头节点的hash值和key和需要put的key不一致
//并且头节点在红黑树中
else if (p instanceof TreeNode)
//调用红黑树的putTreeVal方法将key,value设置到红黑树中
//并且如果红黑树中存在该key,将对应的value赋值给e
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果头节点的hash值和key和需要put的key不一致
//并且头节点在链表中
else {
//累计便利次数,用于在下面链表长度达到 8 的时候转成红黑树
for (int binCount = ; ; ++binCount) {
//如果链表中不存在下一个节点
if ((e = p.next) == null) {
//将put进来的key,value生成一个Node节点
//并将该节点按照尾插法插入到链表尾部
p.next = newNode(hash, key, value, null);
//如果便利次数达到 7 次,那么插入新节点后链表长度将达到 8
if (binCount >= TREEIFY_THRESHOLD - )
//将链表转成红黑树
treeifyBin(tab, hash);
//跳出循环
break;
}
//如果链表中节点的hash值和key和需要put的key一致
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//跳出循环
break;
//链表中对应节点赋值给临时节点p
p = e;
}
}
//如果e != null,表示链表(红黑树)中存在相同的key
if (e != null) { // existing mapping for key
//记录旧值,用于返回
V oldValue = e.value;
//如果onlyIfAbsent == false或者旧值为空(这里下面再讲)
if (!onlyIfAbsent || oldValue == null)
//使用调用put方法传递进来的value覆盖链表中节点中的value
e.value = value;
//这个方法是为LinkedHashMap留的后路
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
//能走到这一步,在上面if (e != null)判断中 e == null
//表示新增了元素而不是替换了元素
//modCount记录了HashMap在节点数量上变化的次数,在这里加一
++modCount;
//当前HashMap中元素加一,并且和阈值比较
if (++size > threshold)
//如果当前HashMap中元素数量大于阈值,进行扩容
resize();
//这个方法是为LinkedHashMap留的后路
afterNodeInsertion(evict);
//由于是新增节点,没有覆盖旧节点的值,返回null
return null;
}
上面这一段是HashMap中put()方法的源码,简单概括一下扩容流程:
- 计算需要put的key的hash值
- 判断hash桶是不是空,为空先进行扩容
- 判断该key值对应在hash桶上是否存在节点,不存在则直接在hash桶中创建节点
- 对比头节点的hash值和key是否和需要查询的key一致,如果一致直接覆盖头节点
- 判断头节点在红黑树中还是链表中
- 如果在红黑树中,则在红黑树中查找该节点,如果在链表中,则遍历链表查询该节点
- 如果在链表(红黑树)中存在节点的hash值和key和需要put的key一致,进行覆盖操作
- 如果不存在,创建新的节点,添加到链表(红黑树)中
- 如果当前HashMap中元素数量超过阈值,进行扩容
- 如果put()方法覆盖了某个节点,则返回这个节点的value,否则返回null
下面我们来分析一下这段代码:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
if (!onlyIfAbsent || oldValue == null)
//使用调用put方法传递进来的value覆盖链表中节点中的value
e.value = value
...
}
通过这段代码我们发现,如果put方法的key在HashMap中存在,put方法一定会使用新的值去覆盖旧的值;如果putVal方法的key在HashMap中存在,只有在旧值为空的情况下putVal方法才会使用新的值去覆盖旧的值。
3、clear()方法
public void clear() {
//定义临时变量
Node<K,V>[] tab;
//将HashMap节点变化次数加一(如果当前hash桶是空也会增加)
modCount++;
//如果当前hash桶不是空,则遍历hash桶,hash桶中所有的桶都置空
if ((tab = table) != null && size > ) {
size = ;
for (int i = ; i < tab.length; ++i)
tab[i] = null;
}
}
clear()方法很简单,就不做介绍了。
4、总结
到这里,关于HashMap的get()、put()、clear()方法的源码也都看完了。
这篇文章把HashMap主要的源码和实现方式介绍了一下,关于红黑树方面的内容都略过了,没有做过多介绍,以后有时间再分享一下红黑树相关的源码。
喜欢这篇文章的朋友,欢迎扫描下图关注公众号lebronchen,第一时间收到更新内容。
