感谢 shixinzhang的文章, 参考此: https://blog.csdn.net/u011240877/article/details/53351188 文章
一. HashMap的12个成员变量含义:
/**
* 初始容量为16
*/
static final int DEFAULT_INITIAL_CAPACITY =1 <<4; // aka 16
/**
* 最大容量为2的三十次方
*/
static final int MAXIMUM_CAPACITY =1 <<30;
/**
* 加载因子: 0.75f
* 分成四等份, 0.25 , 0.25 * 3 = 0.75, 在容量3/4时(0.75)进行扩容
*/
static final float DEFAULT_LOAD_FACTOR =0.75f;
/**
* 1. 当前槽位Entry(也就是Node节点数 >= TREEIFY_THRESHOLD此值, 并且当前table数组的长度 >= MIN_TREEIFY_CAPACITY, 则将链表转成红黑树.
* 2. 当前槽位Entry(也就是Node)节点数 >=TREEIFY_THRESHOLD, 并且当前table数组的长度 < MIN_TREEIFY_CAPACITY, 则进行扩容,不发生树化
*/
static final int TREEIFY_THRESHOLD =8;
/**
* 当前槽位Entry(也就是Node)节点数小于等于6时, 由红黑树转成链表
*/
static final int UNTREEIFY_THRESHOLD =6;
/**
* 最小的元素容量, 结合 TREEIFY_THRESHOLD 使用, 判断什么转成树和扩容
*/
static final int MIN_TREEIFY_CAPACITY =64;
/**
*哈希表中的链表数组
*/
transient Node[] table;
/**
*键值对集合
*/
transient Set
> entrySet;
/**
* 键值对
*/
transient int size;
/**
* 当前 HashMap 修改的次数,这个变量用来保证 fail-fast 机制
fail-fast 机制 : https://blog.csdn.net/zymx14/article/details/78394464
*/
transient int modCount;
/**
* The next size value at which to resize (capacity * load factor).
* 阈值, 下一次扩容的值(容量*负载系数)
int threshold;
/**
* 哈希表的加载因子
*/
final float loadFactor;
HashMap本身就是Entry数组,每个槽位就是第一个Entry节点,下一个节点就是由前一个 next指向下一个节点, 所以同一个链表的hash相同
二. HashMap 的初始容量和加载因子
由于 HashMap 扩容开销很大(需要创建新数组、重新哈希、分配等等),因此与扩容相关的两个因素:
1.容量:数组的数量
2. 加载因子:决定了 HashMap 中的元素占有多少比例时扩容
成为了 HashMap 最重要的部分之一,它们决定了 HashMap 什么时候扩容。
HashMap 的默认加载因子为 0.75,这是在时间、空间两方面均衡考虑下的结果:
1. 加载因子太大的话发生冲突的可能就会大,查找的效率反而变低
2. 太小的话频繁 rehash,导致性能降低
当设置初始容量时,需要提前考虑 Map 中可能有多少对键值对,设计合理的加载因子,尽可能避免进行扩容。
如果存储的键值对很多,干脆设置个大点的容量,这样可以少扩容几次。
三. HashMap四个构造方法
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//根据指定容量设置阈值
this.threshold = tableSizeFor(initialCapacity);
}
// 这个阈值经过 无符号右移、求异运算;
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;
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
/**
* 向哈希表中添加整个集合
*/
final void putMapEntries(Map m, boolean evict) {
int s = m.size();
if (s >0) {
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)
resize();
for (Map.Entry e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
// copy添加集合的值
putVal(hash(key), key, value, false, evict);
}
}
}
五. 链表节点Node
static class Nodeimplements Map.Entry {
final int hash; // 哈希值,
final K key; // 键
V value; // 值
Node next; // 指向下一个node
Node(int hash, K key, V value, Node 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 (oinstanceof Map.Entry) {
// Map.Entry 相等的条件: 键相等, 值相等, 个数相等, 顺序相等.
Map.Entry e = (Map.Entry)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
六. putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 如果当前table为空, 则新建; n:指向最后一个桶的位置; tab: 新哈希表
Node[] tab; Node p; int n, i;
if ((tab = table) ==null || (n = tab.length) ==0)
n = (tab = resize()).length;
// 如果要插入的位置没有元素, 新建个节点放进去
if ((p = tab[i = (n -1) & hash]) ==null)
tab[i] = newNode(hash, key, value, null);
else {
// 如果要插入的桶已经有元素,替换
// e : 指向被替换的元素
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key !=null && key.equals(k))))
// p: 指向要插入的桶第一个元素的位置, 如果p 的哈希值,键,值和要添加的一样, 就停止找, e指向p;
e = p;
else if (pinstanceof TreeNode)
// 如果桶第一个元素是树形node, 则在树中插入
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
// 进行链表查找,替换
for (int binCount =0; ; ++binCount) {
// 如果下个元素为空, 则插入后面
if ((e = p.next) ==null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD -1)// -1 for 1st
// 当这个桶内链表个数大于等于8, 就要树形化
treeifyBin(tab, hash);
break;
}
// 如果找到要替换的节点, 就停止,
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;
// 如果超过阈值, 扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
添加方法的逻辑概括为:
七. 计算hash()方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 右移位是去掉低位, 然后异或之后, 高低位结合, 减少碰撞率;
八.resize() 扩容方法
当集合所有元素 > threshold 时;
final Node[] resize() {
// 复制一份当前的数据
Node[] 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;
}
// 新的容量为旧的两部
else if ((newCap = oldCap <<1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 如果旧容量大于等于16, 新的阈值就是旧阈值的两倍
newThr = oldThr <<1; // double threshold
}
// 如果旧容量为0, 并且旧阈值>0,说明之前创建了哈希表但没有添加元素,初始化容量等于阈值
else if (oldThr >0)// initial capacity was placed in threshold
newCap = oldThr;
else {// zero initial threshold signifies using defaults
// 旧容量,旧阈值都是0,说明还没有创建哈希表,容量为默认容量,阈值为 容量*加载因子
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的阈值为0, 就得用 新容量 * 加载因子
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[] newTab = (Node[])new Node[newCap];
table = newTab;
// 接下来就得遍历复制了
if (oldTab !=null) {
for (int j =0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) !=null) {
// 旧的桶置为空
oldTab[j] =null;
// 当前 桶只有一个元素, 直接赋值给对应的位置
if (e.next ==null)
newTab[e.hash & (newCap -1)] = e;
else if (einstanceof TreeNode)
// 如果旧哈希表中这个位置的桶是树形, 把新哈希表里当前桶也变成树形
((TreeNode)e).split(this, newTab, j, oldCap);
else {// preserve order
// 保留旧哈希表桶中链表的顺序
Node loHead =null, loTail =null;
Node hiHead =null, hiTail =null;
Node next;
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;
}
扩容过程中几个关键的点:
新初始化哈希表时,容量为默认容量,阈值为 容量*加载因子
已有哈希表扩容时,容量、阈值均翻倍
如果之前这个桶的节点类型是树,需要把新哈希表里当前桶也变成树形结构
复制给新哈希表中需要重新索引(rehash),这里采用的计算方法是
e.hash & (newCap - 1),等价于 e.hash % newCap
结合扩容源码可以发现扩容的确开销很大,需要迭代所有的元素,rehash、赋值,还得保留原来的数据结构。
所以在使用的时候,最好在初始化的时候就指定好 HashMap 的长度,尽量避免频繁 resize()。