一、关于HashMap的底层机制具体机制
1.首先,从关系上来说,HashMap是Map接口的一个实现类,也是AbstractMap的子类。
2.HashMap底层维护一个Node<K,V>[]数组,Node<K,V>为Entry<K,V>接口的实现类,而Entry<K,V>为Map的一个接口,其主要用来存储key-value键值对。在HashMap中,通过静态类Node<K,V>来实现Entry<K,V>接口,即在HashMap的Entry[]数组中,存储的元素为Node<K,V>类型,我们称其为Hash桶。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; 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; } 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; } }
3.HashMap是如何知道加入进来的元素该存在哪个位置呢?
通过阅读代码,我们可以发现这么一行代码:p = tab[i = (n - 1) & hash]
据此分析,我们不难看出插入的位置是hash()方法返回的值与tab(也就是HashMap底层维护的Entry[]数组)的长度-1做与运算得到的,而hash()方法,则是将hashCode值与其无符号右移16位之后得到的数做异或运算,这样做可以理解为添加了一个扰动函数,增加结果的随机性。但这里面有一个问题要注意,那就是HashMap中tab的长度必须为2的幂次方,至于原因,简单来说其主要目的时为了加快hash计算及减小hash冲突。具体的可以看看下面这篇回答:
为什么HashMap的长度是2的整数次幂? - 知乎 (zhihu.com)
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
4.那么HashMap是如何保证tab的长度一定是2的幂次方的呢?
这是通过其内部的一个方法:tabSizeFor(int cap)
static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
代码可能有些复杂,如果看不懂我们不妨可以带一个数进去试一试,比方说,我们带一个15,也就是cap=15,那么n=14(也就是二进制1110)
由于高16为全部为0,以下仅展示低16位的运算过程
n|=n>>>1 :
0000 0000 0000 1110
0000 0000 0000 0111
得到0000 0000 0000 1111
n|=n>>>2 :
0000 0000 0000 1111
0000 0000 0000 0011
得到 0000 0000 0000 1111
n|=n>>>4 :
0000 0000 0000 1111
0000 0000 0000 0000
得到0000 0000 0000 1111
n无符号右移8位为0000 0000 0000 0000,即运算结果与n相同
无符号右移16位同理,则最后运算完后n位0000 0000 0000 1111即15
根据最后return语句我们可以判断出返回值应位n+1即16
那么这个方法的目的到此一目了然,即返回一个比我们输入的容量大的最小2的整数次幂。通俗地来说,就是因为这个数组的长度是可以人为设定的,如果不了解HashMap的原理而输入了不合理的参数时,HashMap可以对数组长度进行调整,保证其长度是合法的。
return语句中的另外两种情况分别对应输入的长度为负数或者超过最大容量(即230)
5.搞清楚了这些我们来看每个Hash桶到底是怎么存储元素的。
根据前面的分析,我们知道了,每当向HashMap中插入一个元素时,HashMap就会计算出一个数组上的一个索引index,即table[index]处放入待插入的元素。那么如果出现两个元素计算出的index相同的情况要怎么处理呢?此时,并不会重新计算一个Index,而是以链表的形式将新插入的Node与tab[index]处已经存在的Node进行连接。
6.但是链表的长度过长会影响查询效率,因此HashMap设置了树化机制,即链表长度达到TREEIFY_THRESHOLD(树化阈值,默认设为8)且tab中的元素个数达到MIN_TREEIFY_CAPACITY(默认设为64)时,链表会树化为红黑树,提高查询效率。当然当红黑树中的结点下降到UNTREEIFY_THRESHOLD(树退化阈值,默认设为6)后,红黑树会重新退化为链表。
也就是说HashMap的实现基于数组+链表+红黑树,其具体结构如下图所示:
7.基于以上6条,相信大家都已经对HashMap存储及维护元素的机理有了一定的了解,其实HashMap的增删改查方法也是基于上面的原理来进行的。除此之外,还有一点也需要注意,如果数组中元素的数量过多,那么在插入元素时,hash冲突的可能性将大大增加,会影响HashMap的执行效率。因此HashMap设置了一个扩容阈值用于数组扩容操作,扩容阈值为当前数组最大容量*加载因子
二、源码分析部分:
我们来看一下HashMap的源码:
2-1 成员变量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认数组长度为16
static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量为2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f;//调节因子默认0.75
static final int TREEIFY_THRESHOLD = 8;//树化阈值为8
static final int UNTREEIFY_THRESHOLD = 6;//树降级阈值为6
static final int MIN_TREEIFY_CAPACITY = 64;//最小树化容量为64
transient Node[] table;//HashMap底层实现结构,非常重要,也称作散列表
transient int size;//当前散列表中元素个数
transient int modCount;//散列表修改次数,注意只有增删改算修改次数,查询不算,主要用于多线程遍历HashMap时的安全问题而设计
int threshold;//扩容阈值(=最大容量*调节因子)
final float loadFactor;//加载因子
transient Set keySet; //用于遍历key值
transient Collection values;//用于遍历value值
2-2 构造方法:
1.HashMap在调用构造方法时,并不会初始化一个HashMap的对象,因为HashMap可能存在造出来不立刻存元素的情况,这样对内存是一种浪费,因此HashMap仅在第一次调用put方法才会进行初始化
2.HashMap的构造方法有以下四种:
第一种是最为重要的,具体代码如下:
//可自定义数组的初始长度及加载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) //初始容量不可以小于0
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) //初始化容量不可以超过最大允许容量
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor)) //加载因子不可以小于0或不为浮点数
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);//因为输入的容量是用户自定义的,而table的长度应为2的整数次幂,tableSizeFor是将initialCapacity转换为最小的满足2的整数次幂的数
}
第二种和第三种都是基于第一种构造方法,只是对其中的参数值进行了相应的调整。
//基于双参数构造方法,只是将加载因子设为默认值
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//基于第一种方法,数组长度和加载因子均设为默认值
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
第四种是根据形参种传入Map对象生成一个相同的HashMap
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {//根据传入的Map初始化HashMap
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)//若Map容量超过扩容阈值,则进行扩容操作
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {//遍历Map取出key和value插入HashMap中
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
2-3 插入方法 put(K key, V value)
我在有道云笔记里画了一张流程图,在看代码之前先看这张图,可能对下面对源码的理解有所帮助
https://note.youdao.com/s/AFNUpDjs
这副图里已经详细写出了插入的具体步骤,结合这张图,我们来看下具体的代码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
可以看到put方法实际上调用了putVal方法进行插入操作,那么我们来看以下putVal方法的代码:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//这里首先对参数进行以下简单的说明:
//hash为插入的元素由hash()返回的值,key和value为待插入的Node结点的key值和value值
// onlyIfAbsent如果为true,则表示只有在HashMap没有该key的时候才添加相应的Node结点,言外之意,若存在相同key值的结点,则不执行替换操作,但这里设为false,即相同key值时执行替换操作,key值不存在时,添加相应key-value的结点
- // evict如果为false,hashmap为创建模式;只有在使用Map集合作为构造器创建LinkedHashMap或HashMap时才会为false。
//插入方法的大体思路已在上面列出,此时我们只需要跟着这个思路取看代码就行
Node[] tab; // table散列表
Node p; // node pointer指向链表中某一位置的元素(也可能是链表结点或红黑树结点)
int n, i; // n 为length, i 为 node index
if ((tab = table) == null || (n = tab.length) == 0)//若table[]此时尚未初始化,在第一次调用Put方法时对table进行初始化
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // index处没有其他元素,则直接放入新节点
else {
- // index处有元素,有三种情况:1.tab[i]处仅一个结点 2.tab[i]为红黑树,且红黑树中的某一节点可能与待插入元素的key值匹配3.tab[i]处为链表,且链表中某一结点可能与待插入的key元素匹配。注意我说的是可能匹配,并不是一定匹配!
Node e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 假如key是相同的,将p的值赋给临时变量e,执行后续的替换操作
e = p;
else if (p instanceof TreeNode)
//执行红黑树的插入操作,若树中存在相同key的结点,则替换value,若没找到,那么将新节点放入红黑树
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
//从链表的第二个元素开始查(e=p.next),因为头节点key值匹配的情况已在第一个if代码块中做了处理,将查询结果与key值进行比较,若遍历找到相应结点,记录该结点,后续执行替换操作;若遍历到链表最后依然没找到对应结点,则将结点放入链表最后
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//此时已经遍历到了最后一个结点,由于前一步p为倒数第二个结点时,e为最后一个结点,能走到这一步,说明最后一个结点的key值依然不匹配。
p.next = newNode(hash, key, value, null);
- // 插入后如果发现已经链表长度已经适合转为红黑树了,则转换
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 链表中某元素key和key相同,则替换value即可
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//迭代条件p=p.next,上面已经写出了e=p.next
}
}
if (e != null) { // 存在与待插入元素key值相同的结点
V oldValue = e.value;//e点原来的value
if (!onlyIfAbsent || oldValue == null)
e.value = value;//e点新的value值
afterNodeAccess(e);//保证链表插入的有序性,hashmap中该方法为一个空方法,主要是为了给LinkedHashMap使用而创建的,此方法在LinkedHashMap中会重写。
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();//size+1超过阈值要进行扩容
afterNodeInsertion(evict); //删除链表中最先插入的结点以达到优化链表的效果,hashmap中该方法为一个空方法,主要是为了给LinkedHashMap使用而创建的,此方法在LinkedHashMap中会重写。
return null;
}
2-4 扩容方法resize()
扩容的具体思路如下:
1.如果tab为空,说明数组未进行初始化,此时要判断是否已经调用过了构造方法,若已调用过,则将扩容阈值设置为原来阈值的2倍即可,之后进行初始化操作
2.若未调用过构造方法,则先调用构造方法,再进行初始化操作
3.若tab已初始化,则扩容后的容量默认为原来容量的2倍且不可超过最大允许的容量
4. 特别注意的是,对于链表和红黑树,在扩容时要把结点分成高位和低位两部分,低位部分依然放在tab[index]中,高位放于tab[index+oldCap]中
具体的可以参考这个流程图:
https://note.youdao.com/s/B2pV3wLe
final Node<K,V>[] resize() {
//oldCap/newCap:指定新旧最大容量
//oldThr/newThr:指定新旧扩容阈值
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//数组不为null,此时有三种情况,一是正常扩容,二是已经使用了构造方法,但之后未调用put初始化,三是调用构造方法
if (oldCap > 0) {//此时为正常扩容,将容量扩大为原先的2倍,并将扩容阈值设为原来的2倍,若扩容前容量超过最大容量,则不执行扩容操作
if (oldCap >= MAXIMUM_CAPACITY) {//数组容量已超过最大容量
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//数组扩容后容量依然不超过最大容量,此时,将扩容阈值设置为原来的2倍
newThr = oldThr << 1;
}
//此处oldcap=0表示散列表并未创建,但oldThr>0表明已经调用了构造方法,只需将数组容量设为oldthr即可
else if (oldThr > 0)
newCap = oldThr;
else { //数组尚未构造,此时使用默认值进行初始化
// zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {//若新数组的扩容阈值为0,则进行相应的初始化操作
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建扩容后的新数组
table = newTab;
//扩容后的新数组已经构建完毕,此时将原数组的内容赋值给新数组
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
- // j位置原本元素存在,此时根据j位置存放的数,链表,红黑树进行不同的操作
oldTab[j] = null;//将oidTab中j位置的元素置为null便于进行垃圾回收
if (e.next == null)//对应只存了一个数
- // 如果该位置没有形成链表,则再次计算index,放入新table
- // 假设扩容前的table大小为2的N次方,有上述put方法解析可知,元素的table索引为其hash值的后N位确定
- 那么扩容后的table大小即为2的N+1次方,则其中元素的table索引为其hash值的后N+1位确定,比原来多了一位
- 因此,table中的元素只有两种情况:
- 元素hash值第N+1位为0:不需要进行位置调整
- 元素hash值第N+1位为1:调整至原索引的两倍位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)//红黑树
- // split拆分出高位红黑树和低位红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 链表
- // 将链表拆分为高位链表和低位链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
- // 用于确定元素hash值第N+1位是否为0(区分低位链和高位链):
- 若为0,则使用loHead与loTail,将元素移至新table的原索引处,即低位链
- 若不为0,则使用hiHead与hiHead,将元素移至新table的两倍索引处,即高位链
if ((e.hash & oldCap) == 0) {//此处为构建低位链表的操作
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;//loTail=lotail.next
}
else {//构建高位链表
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {//此处注意loTail指向的结点后面可能还连接着别的元素,故将低位链表的尾指针指向null,避免出现错误,将低位链表存入j位置处,对高位链表,同理可证。
loTail.next = null;
newTab[j] = loHead;//构建的低位链表放入newTab[index]
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;//构建的高位链表放入newTab[index+oldCap]
}
}
}
}
}
return newTab;
}
2-5 获取get(Object key)
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
可以看到hashMap调用了getNode方法通过key值获取对应结点,若结点不为空,则返回该结点的value
对于getNode的具体流程如下:
https://note.youdao.com/s/3doEZcxh
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// table不为空,且hash对应index元素不为空
// 如果index位置就是我们要找的key,则直接返回(对应相应数,链表头结点,或红黑树根节点)
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果不是,则从链表或红黑树的角度继续找
if ((e = first.next) != null) {
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;
}
2-6 remove
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;//失败返回null,成功返回删除的值
}
value=null,
matchValue=false,//用于判断node的value值是否匹配
movable=true//设为false时删除该结点不移动其他结点
同样是调用removeNode()方法删除对应的结点,并返回,若返回的结点为null,则remove()方法返回null,否则返回删除结点的value值
具体过程如下:
https://note.youdao.com/s/JuX7aU5n
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//如果hash 对应index即为我们要找的key,则表示已经找到了要删除的结点
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//tab[index]处元素为链表或红黑树,应遍历其每个结点找出对应key值的结点
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 = e;
} while ((e = e.next) != null);
}
}
// 找到后,进行删除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {//判断value值是否匹配
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
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);//HashMap中为空方法,由LinkedHashMap重写该方法
return node;
}
}
return null;
}
2-7 containsKey & containsValue
//containsKey依然调用getNode()方法,判断能否找到对应key值的结点,具体过程详见getNode()
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;//getNode方法找出对应的结点,此处不再赘述
}
//依次遍历散列表的所有位置,在tab[i]中继续遍历所存的所有元素,若找出对应的value值,返回true,若不存在,返回false
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
//判断此位置上是否存在元素,如果不存在元素,则退出,若存在元素,则对tab[i]存的元素进行遍历
if ((v = e.value) == value || (value != null && value.equals(v)))
return true;
}
}
}
return false;
}
2-8 HashIterator
abstract class HashIterator {
Node<K,V> next; // 要返回的下一个hash桶
Node<K,V> current; // 当前的hash桶
int expectedModCount; // 用于判断修改次数与当前线程期望的修改次数是否相等
int index; // current slot,也就是index要返回的位置
HashIterator() {//初始化HashIterator,核心步骤是找到散列表中第一个不为null的位置,记为next
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);//找到第一个存放的元素不为
null的位置
}
}
public final boolean hasNext() {//是否存在下一个元素
return next != null;
}
final Node<K,V> nextNode() {//返回下一个结点
Node<K,V>[] t;
Node<K,V> e = next;//此时next指向散列表中不为空的元素
if (modCount != expectedModCount)//二者不相等时,表示此时有其他线程对hashMap进行了修改,则此时读出的结果可能有误,无法继续遍历
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//此时current指向当前结点(table[index]),若此处存放的是链表或红黑树,next为current的下
一个结点
// next的next为空且table不为空的话,说明已完成对table[index]处的链表/红黑树的遍历,此时继
续找到下一个不为空的table[index]即可
if ((next = (current = e).next) == null && (t = table) != null) {//若为链表或红黑树,此处不进if代
码块,直接返回相应结点
do {} while (index < t.length && (next = t[index++]) == null);//next指向散列表中下一个不为空
的元素
}
return e;//current=e,返回current
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;//便于gc垃圾回收
K key = p.key;//获取待删除结点的Key值
removeNode(hash(key), key, null, false, false);//调用删除方法
expectedModCount = modCount;
}
}
3.总结
1.HashMap的实现基于数组+链表+红黑树,要注意链表树化的条件不单单是链表长度达到8
2.HashMap的table长度为2的幂次方,理解为什么要是2的幂次方,以及HashMap如何保证table的长度一定为2的幂次方
3.源码部分的if分支较多,但只要弄清底层原理,分情况进行处理,就可以理清其中的关系