简述:
HashMap是双列集合Map接口下的一个实现类,以KV键值对映射的形式存储数据
特点:key唯一,value允许重复,无序,“按键得值”,在HashMap中key和value都允许为null。
数据结构
HashMap中用于存放数据的是一个Node类型的数组【Node<K,V>[] table】
jdk1.8之前:数组+单向链表
在put元素时计算hash值是通过哈希值%数组长度
jdk1.8:数组+单向链表+红黑树
在put元素时计算hash值是通过(数组长度【此处的数组长度必须为2^n】-1)&哈希值
下面我们重点了解一下HashMap内部的具体存储
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//数组默认初始容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子
static final int TREEIFY_THRESHOLD = 8;//转换为红黑树时链表的阈值
static final int MIN_TREEIFY_CAPACITY = 64;//转换为红黑树时做需要的数组最小容量
final float loadFactor;//加载因子,代表数组的使用率
int threshold;//扩容阈值,数组中实际能存入的最大元素个数
table数组内部是一个个Node类型的对象,此处的Node类是HashMap中的内部类,它是Map接口内Entry接口下的一个实现类,实质上是一个个Entry
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//代表数组中的索引【final修饰不可变】
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;
}
……
}
一个键值对封装为Node类型的对象,通过哈希算法计算出在table[]中的索引位置并存入数据
HashMap如何扩容
第一次添加KV键值对,数组如果为空,会扩容为默认容量16
//第一次往map中添加元素,若table[]长度为0,那么会走如下的代码分支:
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//resize()调用如下else分支:
if (oldCap > 0) {
……
}
else if (oldThr > 0) {
……
}
else {
newCap = DEFAULT_INITIAL_CAPACITY;//16
//新的扩容阈值:默认加载因子0.75*16=12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
因为下面在往HashMap中添加元素过程中还会存在扩容的现象,我们需要具体了解resize的内部逻辑
- 数组容量超出最大容量,不扩容
- 数组容量>0且未超出最大容量,扩容操作【2倍】————例如:当map中节点个数>扩容阈值【加载因子*数组容量】
- 数组为null或长度为0且扩容阈值未初始化【0】,扩容【16】————即初始化一个空的HashMap,第一次put元素
final Node<K,V>[] resize() {
//临时存储table数组
Node<K,V>[] oldTab = table;
//判断数组是否初始化
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//存储原始扩容阈值
int oldThr = threshold;
//定义新的数组容量,并初始化新的扩容阈值为0
int newCap, newThr = 0;
//数组容量>0,接着判断是否超出定义的最大容量,超出了修改扩容阈值为Integer类型的最大值【不扩容】,并返回原table数组
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//未超出【扩容操作】(将数组容量扩容2倍)<最大容量并且原容量>=16,将扩容阈值也变为2倍赋给新阈值
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//若【数组为null或长度为0】并且旧扩容阈值>0,用旧阈值初始化新阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//若【数组为null或长度为0】并且旧扩容阈值为0,将数组容量扩容为16,计算新扩容阈值(默认加载因子【0.75】*数组容量【16】=【12】)
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//以上条件都不成立,阈值未变化仍为0,根据条件将新阈值设为【新容量*加载因子】或【Integer类型的最大值】
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
…… …… ……
…… …… ……
return newTab;
}
那么在添加过程中如何确定在数组中的位置呢?它会通过下面的hash()
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap中允许key为null
判断key是否为null,如果key==null,存放数组下标为0处;否则,拿key的hashCode与它的高16位再进行一次异或运算(同为0,异为1)【为了减少哈希冲突】
上述操作时为了减少哈希冲突,那么当出现了哈希冲突,它又将何去何从呢?
这时如果不同的key计算出了相同的值,它会在对应数组的该位置形成一个单向链表,将计算出值相同的数据都放在这一条链表上
下面看一下HashMap中添加KV键值对的方法put()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//tab[]为null或长度为0,对tab数组通过resize()方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//通过哈希算法【(n - 1) & hash】计算下标位置,但此处的数组长度n必须为2^n
//若当前下标位置为null,代表没有占用,将数据封装为Node节点存入数组相应的下标中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//当前位置已占用
else {
Node<K,V> e; K k;
//先比较hash,若hash相同再调用equals比较内容,若equals比较也相同,则直接替换
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//若Node p是一个TreeNode类型,当前链表已是红黑树,将当前节点加入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//当前还是链表
else {
for (int binCount = 0; ; ++binCount) {
//jdk1.8后采用尾插法,找到尾结点,尾结点——>next=封装的Node节点,且新的尾结点next=null
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//若链表长度>8,调用treeifyBin()【还需进一步判断数组长度才能做出决定是否转换为红黑树】
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//比较hash,hash相同再调用equals比较是否相同,相同则替换
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//若map中存放的节点已经达到扩容阈值,则需要对数组进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
链表与红黑树之间的转换
但是不是这条链表是不是能一直延伸下去呢?答案是否
当链表上节点个数>8时,会调用treeifyBin方法
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//binCount记录链表上节点的个数,从0开始
treeifyBin(tab, hash);
treeifyBin方法具体代码:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//数组为空或数组长度<64会进行扩容操作
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//数组不为null并且数组长度>=64时才会执行如下操作
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
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);
}
}
注意:在treeifyBin方法中我们看见并没有直接进行链表转换红黑树的操作,而是先判断了当链表节点个数>8时它的数组长度是否<64,若满足条件,会将数组再次进行扩容,而不会进行转换红黑树的操作,只有当链表节点个数>8且数组长度>64,此时链表会转换为一颗红黑树
小结
- HashMap内部存储采用数组+链表+红黑树
- 链表转换为红黑树的条件:参照注意内容
- 注意扩容时机
- 1、数组为空,第一次添加元素会扩容为16
- 2、当HashMap中存在节点数>threshold数组可用的最大容量时,扩容2倍
- 3、当HashMap链表节点个数>8且数组长度<64,扩容2倍