HashMap
引言
以下为我为HashMap翻译的引用
HashMap允许value为null和key为null。但是key为null只能有一个。它不保证元素的顺序。
假设hash函数将数据均匀的分布在桶中,get和put的时间复杂度为O(1)。
只有initial capacity和 load factor会影响一个HashMap实例的性能。
如果有很多映射将要存进HashMap,那么创建一个初始带有大容量的将会使映射的存储更加有效率,比它自己rehash来增加表的大小更有效率。
这是线程不安全的。
当桶内的结点多于TREEIFY_THRESHOLD(8)时,会从链表转换为树结构(红黑树),红黑树在结点较多的情况下可以加快搜索速度,但是存储空间增大。一般不会发生转换,这个概率太小了。当树结构退化为链表时,需要结点小于等于UNTREEIFY_THRESHOLD(6)。
新人第一次看源码,有错误或者增加的地方,还望指出,谢谢大家。
初始化
先了解一个函数
// 返回一个大于等于cap的2^n。例如cap = 3 ,return 4。
static final int tableSizeFor(int cap) {
// 这一步很重要,确保想8这种2的n次幂的,不会翻倍。
// 如果cap = 8,没有cap - 1,那么最后结果会出现16。
int n = cap - 1;
// 下面就都是移位了,将最高位的1后面都填充1。
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
// 最后判断条件,n + 1是让结果回归2的次幂。
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
各个初始化函数
// 指定初始大小和负载因子的构造方法。
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;
// 这里会确保initialCapacity变为2的n次幂
// threshold阈值,HashMap进行reHash的阈值
this.threshold = tableSizeFor(initialCapacity);
}
// 只有初始大小的构造方法,默认负载因子
// DEFAULT_LOAD_FACTO = 0.75f
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 负载因子,注意这里没有出现初始大小赋值,这会在另一个方法中出现。
// 提一句,默认大小是16。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 这个是将其他Map转换成HashMap,看一下这个putMapEntries方法
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
// resize()和putVal()方法等下解析
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
// m的大小小于等于0,就啥也不用做了,就是空map。
if (s > 0) {
// table == null就是未初始化的时候
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);
}
// table != null && s > threshold这就说明新加入的map容量大于阈值了,需要重新计算size了。
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
resize
// 作用:初始化table size或者翻倍。
// 如果table是空的,那么久初始化一个
// 否则,因为我们的大小使用的是2的幂,这些元素要么在原地,或者移到两倍的地方。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 检查table是否初始化
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 原来的table太大了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// table未初始化,且旧阈值大于0(记住阈值的设定在第一个、第二个和第四个构造函数中设定过,其他的都没有设定过)
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 默认初始化(只有第三种构造,才会触发这里的逻辑)
newCap = DEFAULT_INITIAL_CAPACITY;
// 计算默认阈值
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 这里会计算新的阈值(记住阈值的设定在第一个、第二个和第四个构造函数中设定过,其他的都没有设定过,也就是说有自定义的负载因子)
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];
// 下面的操作就是,将扩大的hashMap中的元素重新哈希。
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)
// 注意这个e.hash & (newCap - 1)
// 这里会确定元素保留在原位还是移动旧Cap大小的位置
// 例如元素hash为7 0x0111 旧cap为 4 0x0100那么计算出来的位置就是 0x0011 3
// 新cap是旧cap的两倍,8 0x1000 那么计算出来的位置就是 0x0111 7 增加了旧cap 4大小的位置
// 核心目标是通过位运算和结构拆分(链表/树),高效地将旧数组中的元素分配到新数组中。其设计充分利用了哈希表的容量特性(2 的幂次方),避免了重复计算哈希值,同时保持了数据结构的有序性和性能平衡。
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 尾插法可以避免多线程死循环,但还是线程不安全。
// jdk1.7 是头插法,会陷入多线程死循环
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> 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;
}
理一下核心逻辑
(1)先检查当前Map是否初始化(即是否存在元素)
(2)初始化过的(size > 0),将阈值变为原来的两倍或者Integer.MAX_VALUE
(3)未初始化过的,就要分两种情况了,有初始阈值(这里阈值都是2的幂,你会问这里为啥不是乘法的值,接下来会初始化这种阈值)的,直接将阈值赋给容量。否则赋默认初始大小(16),计算默认负载因子(0.75) * 默认初始大小(16)。
(4)为(3)中的第一种情况计算新的阈值
(5)将扩大的hashMap中的元素重新哈希。
长度使用2的幂的原因猜想:
省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了(引用了tech.meituan.com/2016/06/24/…)。那么就有机会将长的链表或者树的长度减少,这样更有利于搜索。
get
// 这个方法会返回key的映射value或者不存在,返回null。
// 当然这个方法返回null有两种可能,key不存在或者value存储的就是null。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 分两种情况搜索
// 第一种是已经进化成树结点了
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 第二种是 还是链表结点,那就一个个找。
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
put
// 装入指定的key和指定的value。如果map中已经存在对应的key了,那么旧value将会被替换掉
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果没有初始化,那就先进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 如果当前桶是空的,那就直接new一个node插入就行了。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 否则就要看是否存在key,存在就直接找到修改旧值就行了,否则执行插入
else {
Node<K,V> e; K k;
// 第一个判断,是否存在key,这里只是判断了第一个结点,剩下的结点也要判断,在下面的两个逻辑里判断
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 树的插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)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
treeifyBin(tab, hash);
break;
}
// 继续判断是否存在key。
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;
// 插入之后,发现大于阈值了,那就需要resize。
if (++size > threshold)
resize();
// 这个函数我不知道干啥的
afterNodeInsertion(evict);
return null;
}