HashMap扩容原理
一. HashMap源码分析
1. HashMap重要属性介绍
/**
* 默认初始容量-16 必须是2的幂。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大支持容量1073741824
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认负载因子0.75f
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 当链表的长长度大于8时会将链表转换为红黑树
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 当链表的长长度小于6时会将链表红黑树转换为连表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 当entry数组的长度大于64才允许将链表转换为红黑树,否则应该直接扩容而不是将连表树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 存放entry的数组
*/
transient Node<K,V>[] table;
/**
* 存放entry的数组
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* key-value对的数量
*/
transient int size;
/**
* 扩容时的阈值,计算规则为 容量*负载因子
*/
int threshold;
/**
* 负载因子,用于计算HashMap的容量达到多少时进行扩容
*/
final float loadFactor;
2. HashMap构造方法说明
HashMap一共提供了三个构造方法,无参构造,指定初始容量和指定初始容量及负载因子的构造方法。
- 无参构造:所有的属性都取默认值,如loadFactory = 0.75F,默认的初始容量为DEFAULT_INITIAL_CAPACITY即16。
public HashMap() {
//负载因子默认值 0.75f
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
- 指定初始容量:负载因为为默认的0.75F,而指定初始容量之后,HashMap并没有直接将指定的初始容量作为真正的HashMap的容量,而是经过 计算获取一个比给定整数大或者等于的最接近的2的幂次方整数,如给定3,则设置为4,给定9,则设置为16。
public HashMap(int initialCapacity) {
//指定初始容量和负载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
- 指定初始容量及负载因子:其实该构造方法并不常用,因为负载因子的设定关系到扩容的时机,负载因子设置过大会导致HashMap的hash冲突比较严重,影响查询的效率,设置过小,会导致频繁地进行扩容,影响添加元素的效
public HashMap(int initialCapacity, float loadFactor) {
// 如果初始容量小于0则抛异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果初始容量超出最大容量则设置为最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//负载因子小于0或为NaN则抛异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
3. HashMap容量为什么要设置为2的幂次方整数
HashMap初始化容量非得是2的幂次方,2的倍数不行么,奇数不行么?
- 2的幂次方:hashmap在确定元素落在数组的位置的时候,计算方法是(n - 1) & hash,n为数组长度也就是初始容量 。hashmap结构是数组,每个数组里面的结构是node(链表或红黑树),正常情况下,如果你想放数据到不同的位置,肯定会想到取余数确定放在那个数组里,计算公式:hash% n,这个是十进制计算。在计算机中, (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n,计算更加高效。
- 奇数不行:在计算hash的时候,确定落在数组的位置的时候,计算方法是(n - 1) & hash,奇数n-1为偶数,偶数2进制的结尾都是0,经过hash值&运算后末尾都是0,那么0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这样就会造成空间的浪费而且会增加hash冲突。
static final int tableSizeFor(int cap) {
//其中capacity设置为2的幂次方整数是为了减少hash冲突的第一步
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;
}
4. HashMap扩容逻辑
- 通过hash(Object key)算法得到hash值;
- 判断table是否为null或者长度为0,如果是执行resize()进行扩容;
- 通过hash值以及table数组长度得到插入的数组索引i,判断数组table[i]是否为空或为null;
- 如果table[i] == null,直接新建节点添加,转向 8),如果table[i]不为空,转向 5)
- 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,这里的相同指的是hashCode以及equals,否则转向 6)
- 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转7)
- 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可
- 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容
5. HashMap的扩容机制
HashMap扩容的时机取决于扩容阈值threshold,阈值的计算规则为threshold = capicaty *loadFactor,初始容量capicaty 默认16,负载因子为0.75, 当HashMap的size大于或等于 threshold则HashMap会进行扩容处理。
详情请参考以下链接:
tableSizeFor
hashmap扩容
HashMap原理(二) 扩容机制及存取原理