「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」。
-
HashMap 数组最大容量是 (1<<30) ::: tip HashMap 数组最大容量是(1<<30),但是 HashMap 的容量是没有限制的,因为 HashMap 是使用数组加链表结构存储的,每个数组里面存储的是链表的头结点(1.8 之后当链表长度超过 8,会用红黑树的形式存储),当产生 hash 冲突的时候会在链表或者树新增结点,所以容量不受限制。 ::: ::: warning 为什么数组最大容量设置为(1<<30),而不是整数的最大范围呢? :::
-
HashMap 是通过计算出来的 hashCode 和数组总容量取模运算获取数组下标的,但是由于取模运行速度要比位运算慢很多,HashMap 是通过如下方法进行运算从而达到取模运算的效果,即
a%b == (b-1)&a当 b 是 2 的幂次方时成立
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
-
HashMap 容量是 2 的幂次方的原因 ::: tip HashMap 在添加元素的时候会计算 hash 值,计算的时候会用(n-1)&hash,当 n 为 2 的幂次方的时候,(n-1)就是所有位全部都是 1,如果换成其他的值的话,转成二进制,必定会有位上面是 0 的,这个时候这个位置永远都不会被用到。这个时候产生碰撞的概率更高,减慢了查询速度。 :::
-
HashMap 扩容 ::: tip 当 HashMap 中元素越来越多的时候,碰撞的概率也就会越来越高,为了提高查询效率,这个时候就要对 HashMap 数组进行扩容了,当 HashMap 的元素个数超过了 数组大小*负载因子(0.75)的是偶会进行扩容 :::
-
为什么我们极力避免 HashMap 扩容呢? ::: tip 很多时候我们都会建议估算 HashMap 容量大小,然后在创建 HashMap 的时候指定容量大小,就是为了避免 HashMap 扩容,因为 HashMap 扩容的代价很大,首先会新建一个 HashMap,然后把老的数据全部都重新计算 hash 值,计算在新的数组中的位置,放到新的数组中,这个过程是很耗时的,应该尽量避免这种情况。 :::
-
HashMap 不是线程安全的,如果必须用 HashMap 的话可以使用
Collections.synchronizedMap(new HashMap)创建线程安全 HashMap
疑问
为什么 HashMap 数组容量最大为(1<<30)而不是(1<<31 - 1)这个正整数的最大值呢?
HashMap 数据结构
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
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;
}
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 (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
重新计算 HashMap 容量
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;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
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];
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)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
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;
}