HashMap作为一种重要的数据结构,无论是在面试还是开发中都能经常碰到,今天就来聊一聊它吧。
环境:JDK1.8
一 HashMap的数据结构
HashMap是由数组+链表+红黑树组合而成的,具体来说,HashMap底层维护了一个Entry<K,V>[]的数组,数组中存放了Entry,Entry的结构可能是链表,也能是红黑树。具体下面会分析。
二 HashMap的成员变量
//序列号
private static final long serialVersionUID = 362498820763181265L;
//默认的初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
//最大容量
static final int MAXIMUM_CAPACITY = 1073741824;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75F;
//当数组中链表的长度大于这个值时,链表就会转为红黑树
static final int TREEIFY_THRESHOLD = 8;
//当数组中红黑树的大小小于这个值时,红黑树就会转变为链表
static final int UNTREEIFY_THRESHOLD = 6;
//链表转换为红黑树数组要求的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
//存储元素的数组,大小总是2的幂,其中Node实现了Map.Entry接口
transient Node<K, V>[] table;
//存放具体元素的集
transient Set<Map.Entry<K, V>> entrySet;
//存放元素的个数,注意其与数组长度是不同的
transient int size;
//每次扩容和更改map结构时都会用到,用于计数,快速失败会用到
transient int modCount;
//临界值,当(容量*加载因子)大于该值时,数组会进行扩容
int threshold;
//加载因子
final float loadFactor;
其中TREEIFY_THRESHOLD,UNTREEIFY_THRESHOLD,MIN_TREEIFY_CAPACITY这三个常量决定了数组中链表和红黑树之间相互转换的关系,数组中一开始存放的都是链表,只有当链表的长度大于8,并且数组的大小大于64时,链表就会转变为红黑树,而当红黑树的大小小于6时,红黑树就会转变为链表。
三 get方法原理解析
//get方法其实底层是调用了getNode方法,入参为key的hash值和key对象
public V get(Object paramObject) {
Node localNode;
return (localNode = getNode(hash(paramObject), paramObject)) == null ? null : value;
}
//getNode方法返回的是Node对象,Node实现了Entry
final Node<K, V> getNode(int paramInt, Object paramObject) {
//用于存储HashMap的数组
Node[] arrayOfNode;
//用于存储数组的长度
int i;
//Node实现了Entry实体,用于存储返回值
Node localNode1;
//判断数组是否不为空
if (((arrayOfNode = table) != null) && ((i = arrayOfNode.length) > 0)
&& ((localNode1 = arrayOfNode[(i - 1 & paramInt)]) != null)) {
Object localObject;
//数组中对应角标的元素为所找,直接返回
if ((hash == paramInt) && (((localObject = key) == paramObject)
|| ((paramObject != null) && (paramObject.equals(localObject))))) {
return localNode1;
}
Node localNode2;
//如果数组对应角标对应的第一个元素不为所找元素,寻找该元素的next
if ((localNode2 = next) != null) {
//当数组中的结构为红黑树时
if ((localNode1 instanceof TreeNode)) {
return ((TreeNode) localNode1).getTreeNode(paramInt, paramObject);
}
//当数组中的结构为链表时
do {
if ((hash == paramInt) && (((localObject = key) == paramObject)
|| ((paramObject != null) && (paramObject.equals(localObject))))) {
return localNode2;
}
} while ((localNode2 = next) != null);
}
}
return null;
}
说明:get方法其实底层就是调用了getNode方法,根据key的值通过(i - 1 & hash)算出其在数组中的角标,然后将角标对应位置的元素的key与传入的key进行比较,如果相等,则返回该元素,如果不相等,则根据该角标对应的结构是红黑树还是链表进行相应的查找。
四 put方法原理解析
//put方法底层是调用了putVal方法
public V put(K paramK, V paramV) {
return (V) putVal(hash(paramK), paramK, paramV, false, true);
}
final V putVal(int paramInt, K paramK, V paramV, boolean paramBoolean1, boolean paramBoolean2) {
Node[] arrayOfNode;
int i;
//对table进行判断,如果table为空,进行扩容
if (((arrayOfNode = table) == null) || ((i = arrayOfNode.length) == 0)) {
i = (arrayOfNode = resize()).length;
}
int j;
Object localObject1;
//(j = i - 1 & paramInt)算出元素放在数组中的对应角标,如果角标上为空,则生成新节点放入
if ((localObject1 = arrayOfNode[(j = i - 1 & paramInt)]) == null) {
arrayOfNode[j] = newNode(paramInt, paramK, paramV, null);
}
//角标处已经存在节点
else {
Object localObject3;
Object localObject2;
//新增的元素与第一个节点hash值和key相等,直接覆盖
if ((hash == paramInt)
&& (((localObject3 = key) == paramK) || ((paramK != null) && (paramK.equals(localObject3))))) {
localObject2 = localObject1;
}
//如果角标处存放的红黑树节点
else if ((localObject1 instanceof TreeNode)) {
//那就插入一个新的树节点
localObject2 = ((TreeNode) localObject1).putTreeVal(this, arrayOfNode, paramInt, paramK, paramV);
}
//角标处为链表结构
else {
for (int k = 0;; k++) {
if ((localObject2 = next) == null) {
next = newNode(paramInt, paramK, paramV, null);
if (k < 7) {
break;
}
//如果链表的长度大于8,则将链表转换为红黑树
treeifyBin(arrayOfNode, paramInt);
break;
}
//如果新增的元素与链表中的某个节点的hash值和key相等,则进行覆盖
if ((hash == paramInt) && (((localObject3 = key) == paramK)
|| ((paramK != null) && (paramK.equals(localObject3))))) {
break;
}
localObject1 = localObject2;
}
}
//当出现key相等的情况,均到这里进行value值的更新处理(即上文说到的覆盖)
if (localObject2 != null) {
//将老的value赋值,准备返回
Object localObject4 = value;
if ((!paramBoolean1) || (localObject4 == null)) {
//将新的value赋值
value = paramV;
}
afterNodeAccess((Node) localObject2);
return (V) localObject4;
}
}
//修改次数加一,该值与快速失败有关
modCount += 1;
//如果hashmap的大小大于阈值,会进行扩容,具体resize函数下文会讲
if (++size > threshold) {
resize();
}
afterNodeInsertion(paramBoolean2);
return null;
}
说明:put方法底层调用了PutVal方法,PutVal先通过(i - 1 & hash)算出要插入元素对应角标的位置,如果对应位置为空,则直接插入节点,如果不为空,即发生了哈希冲突,则需根据节点具体属于链表还是红黑树结构进行插入。
说明