目录
一、HashMap数据结构概述
HashMap类是集合框架中Map接口的实现类,是一种键-值映射表,所以我们对比单列集合,它的优点就在于能够高效的通过key查找出对应的value。
- 在JDK 1.7及之前,HashMap结构采用数组+ 单向链表的形式存储键值对。而且在链表中插入元素时,采用的是[头插法],即将新节点每次放在链表的头部。
- 到了JDK 1.8以后其内部结构采用数组+单向链表+红黑树的形式存储键值对,在链表插入元素时改用[尾插法]。
二、详述以及具体代码
2.1 Node[ ] table数组
数组的类型为Node[ ],用于保存KV键值对,每个KV键值对都被封装成一个Node类型的对象,同时数组中的每个Node对象也是链表中的头结点。
public class HashMap{
//每个Node对象保存一个KV键值对,同时也是链表中的头结点
transient Node<K,V>[] table;
//数组的默认容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量为2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
}
2.2 静态内部类Node
public class HashMap{
//静态内部类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;
}
//hashcode方法,用key和value的hash值作异或运算
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
}
2.3 存放键值对的方式(关键)
前面介绍了基本的table数组,以及Node类的基本结构。那么如何把每一个Node存入table数组,这也是HashMap的精妙之处了,其存放过程为:
1. 当使用put()方法存放键值对时,首先调用hash()方法,计算出key的哈希值。该方法里将key的哈希值与其高16位进行了异或运算,这样算出的哈希值在计算下标时会更散列,减少了哈希冲突。
public class HashMap{
//此方法的作用是将一个键值对存入HashMap对象中
//调用了putVal()方法,putVal()方法又调用了hash(key)方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
}
static final int hash(Object key) {
int h;
// 通过key的hashCode()方法返回的哈希值与它的高16位进行异或运算
// 作用:计算出的hash值,在计算下标位置时,会更“散列”,减少哈希冲突
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2. 得到key的哈希值后与数组的长度length进行&运算得到的值为Node对象在数组中的下标位置。
// 两种计算方式结果相同,但效率不同
int index1 = (数组长度 - 1) & hash值 // 位运算效率高,要求数组长度必须为2的n次幂
int index2 = hash值 % 数组长度 // 算术运算效率低
得到下标后,将其对应的节点赋给临时节点p,如果为null则表示当前位置没有存入节点,那么就可以放在数组中,调用newNode()方法在该位置创建新的节点。
public class HashMap{
// 添加键值对
final V putVal(int hash, K key, V value) {
Node<K,V>[] tab; // 临时数组
Node<K,V> p; // 临时节点
int n, i; // n代表数组长度,i代表元素下标位置
// 根据当前元素的key的hash值,计算该元素在数组tab中的下标位置i
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
}
}
2.4 HashMap的扩容
HashMap在扩容的过程中需要按照数组容量和加载因子(loadFactor)来进行判断。
数组容量: table[]数组的长度。如果没有指定容量,那么在添加第一个元素时,该数组按照默认值16进行初始化。
加载因子: 表示着HashMap集合中元素的填满程度,默认为0.75f,可以理解为百分比(个人看法)。该值越大表示该HashMap集合允许填满的元素就越多,对应的空间利用率就越高。但是由于元素的存储越来越密集,就会导致哈希冲突的概率增加。反之虽然降低了空间利用率,但是哈希冲突的概率就会降低。
public class HashMap{
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始化容量为16
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子
int threshold; // 扩容阈值
final float loadFactor; // 加载因子
}
2.5 什么情况下发生扩容
HashMap的扩容方法是resize()方法。在两种情况下会发生扩容:
- 情况1: 这里又会提到一个新的属性threshold(扩容阈值=数组容量*加载因子)。当HashMap集合中的元素个数超过这个阈值时,就会进行扩容。比如,默认情况下数组容量为16,加载因子为0.75f,那么阈值则为16*0.75=12,当元素个数超过12时数组就会扩容。
- 情况2: 当添加新元素时,如果链表的长度大于8,就会将该条链表转换为红黑树结构。在这之前会有一步判断数组容量的操作。如果容量小于64,则会进行数组扩容;大于的话才会将链表转换为红黑树。由于链表的查找操作属于线性查找,效率比较低,所以会在长度大于8的时候转换为红黑树,以提高查找的效率。
public class HashMap{
/**
* The next size value at which to resize (capacity * load factor).
*
*
*/
int threshold;//扩容阈值(容量*加载因子)
}
// 添加新元素
final V putVal(int hash, K key, V value) {
//...
// 判断当前集合中的元素数量,是否超过阈值threshold
if (++size > threshold)
resize(); // 扩容
//...
}
// 链表转换为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
//...
// 数组为空或者数组的长度n小于64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); // 扩容
// ...
}
当链表长度大于8时,将其转换为红黑树。
public clas HashMap{
// 默认链表长度
static final int TREEIFY_THRESHOLD = 8;
// 判断链表数量是否大于8
if (binCount >= TREEIFY_THRESHOLD - 1)
// 链表长度超过8,将当前链表转换为红黑树
treeifyBin(tab, hash);
}
转换为红黑树的源代码:
public class HashMap{
// 转换为红黑树
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();
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);
}
}
}
2.6 每次扩容多大
HashMap在扩容时会按照当前数组容量的2倍进行扩容。
// 新容量 = 原有容量的2倍
newCap = oldCap << 1;
2.7 最大容量为多少
在每次扩容时都会检查当前容量是否超出常量值MAXIMUM_CAPACITY,如果超出则不会扩容。常量值 MAXIMUM_CAPACITY为1<<30 ,计算结果为 1073741824。所以, HashMap 集合数组的最大容量为 1073741824 。
static final int MAXIMUM_CAPACITY = 1 << 30;
2.8 扩容操作resize()源代码
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) {
// 如果原容量超出最大容量,则退出扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 计算新容量(新容量=原容量的2倍)
// 新容量没有超出最大容量,并且原容量大于等于默认初始容量16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 设置新扩容阈值(同样为原扩容阈值的2倍)
}
else if (oldThr > 0) // 新容量按照初始化(构造方法)中的扩容阈值设置
newCap = oldThr;
else { // 默认
newCap = DEFAULT_INITIAL_CAPACITY; // 默认初始化容量16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 默认扩展阈值 = 默认加载因子0.75 * 默认初始化容量16
}
// 如果新扩容阈值等于0,则需要重新计算,最大不超过Integer.MAX_VALUE
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 创建数组
@SuppressWarnings({"rawtypes","unchecked"})
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) {
oldTab[j] = null;
if (e.next == null) // 当前元素为单节点(不是链表),则重新按照哈希值,计算下标位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode) // 当前元素为红黑树节点,将当前红黑树拆分为2棵红黑树
((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;
// 遍历链表,根据每个节点的hash值,重新计算链表位置
do {
next = e.next;
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) {
loTail.next = null;
newTab[j] = loHead; // 原位置
}
// 高位链表保存在数组的新位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead; // 新位置
}
}
}
}
}
// 返回新数组
return newTab;
}
以上就是我对HashMap集合的数据结构的说明,请大家多多参考,不完善的地方请提供更好的意见,感谢阅读! ! !