1. 概述
本文很长,详细描述了HashMap源码级别的实现原理,并讨论了包括扩容,hash计算,新建HashMap的开销等问题,同时还提供了一些外部资料。由于内容太多,建议阅读时结合目录快速跳转查看。
Java源码阅读最好采用IDEA,Ctrl + N 输入HashMap即可看到HashMap的源码了,HashMap总共有2444行源码
本文查看的是JDK-11.0.1的源码
文章目录
- 1. 概述
- 2. HashMap的变量
- 3. HashMap的方法
-
- 3.1 构造函数
- 3.2 hash(Object key)
- 3.3 comparableClassFor(Object x)
- 3.4 compareComparables(Class<?> kc, Object k, Object x)
- 3.5 tableSizeFor(int cap)
- 3.6 putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
- 3.7 size()
- 3.8 isEmpty()
- 3.9 get(Object key)
- 3.10 getNode(int hash, Object key)
- 3.11 containsKey(Object key)
- 3.12 put(K key, V value)
- 3.13 putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
- 3.14 resize() 扩容
- 3.15 treeifyBin(Node < K,V > [] tab, int hash)
- 3.16 putAll(Map<? extends K, ? extends V> m)
- 3.17 remove(Object key)
- 3.18 removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)
- 3.19 clear()
- 3.20 containsValue(Object value)
- 3.21 keySet()
- 3.22 values()
- 3.23 entrySet()
- 3.24 getOrDefault(Object key, V defaultValue)
- 3.25 putIfAbsent(K key, V value)
- 3.26 remove(Object key, Object value)
- 3.27 replace(K key, V oldValue, V newValue)
- 3.28 replace(K key, V value)
- 3.29 computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
- 3.30 computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
- 3.31 compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
- 3.32 merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)
- 3.33 forEach(BiConsumer<? super K, ? super V> action)
- 3.34 replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
- 3.35 clone()
- 3.36 loadFactor()
- 3.37 capacity()
- 3.38 writeObject(java.io.ObjectOutputStream s)
- 3.39 readObject(java.io.ObjectInputStream s)
- 3.41 replacementNode(Node < K,V > p, Node < K,V > next)
- 3.42 newTreeNode(int hash, K key, V value, Node < K,V > next)
- 3.43 replacementTreeNode(Node < K,V > p, Node < K,V > next)
- 3.44 reinitialize()
- 3.45 afterNodeAccess(Node < K,V > p)
- 3.46 afterNodeInsertion(boolean evict)
- 3.47 afterNodeRemoval(Node < K,V > p)
- 3.48 internalWriteEntries(java.io.ObjectOutputStream s)
- 4. HashMap的内部类
- 后记
- 参考资料
咱们按照源码顺序来分析HashMap,除了HashMap本身的变量和方法,HashMap中还定义了定义如下内部类:
1.1 内部类
- Node
- KeySet
- Values
- EntrySet
- HashIterator
- KeyIterator
- ValueIterator
- EntryIterator
- HashMapSpliterator
- KeySpliterator
- ValueSpliterator
- EntrySpliterator
- TreeNode:这个类代表红黑树节点,HashMap中对红黑树的操作的方法都在此类中
1.2 基本实现
HashMap底层使用哈希表(数组 + 单链表),当链表过长会将链表转成 红黑树以实现 O(logn) 时间复杂度内查找。
HashMap的定义为class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
。
1.3 扩容原理
HashMap采用的扩容策略是,每次加倍,这样,原来位置的Entry在新扩展的数组中要么依然在原来的位置,要么在<原来位置+原来的容量>
的位置。
1.4 hash计算
hash()
函数计算hash值方法为(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
,计算出的hash值会被缓存在Node.hash
中。
为什么这样计算hash值
hash值计算相当于就是将高16位与低16位进行异或,结果是高16不变,低16位变成其异或的新结果。
为什么让低16位与高16为异或合成一个新的结果呢?是因为HashMap的容量通常比较小,在进行长度取模运算时采用的是只取二进制最右端几位,这样高位的二进制信息就没有用到,所带来的结果就是Hash结果分布不太均匀。而高16位与低16位异或后就可以让低位附带高位的信息,加大低位的随机性。具体请参考JDK 源码中 HashMap 的 hash 方法原理是什么? - 胖君的回答 - 知乎
不明白异或结果的朋友来看下这段验证代码,复制此代码运行即可明白高16位与低16位的异或的结果:
import java.util.Random;
class Scratch {
public static void main(String[] args) {
generateTestCase(41132564);
Random random = new Random();
for (int j = 0; j < 10; j++) {
generateTestCase(random.nextInt(Integer.MAX_VALUE));
}
}
/**
* 显示根据key的hashCode算出最终元素的hash值
*
* @param hashCode 代表key的hashCode
*/
public static void generateTestCase(int hashCode) {
System.out.println("hashCode = " + hashCode + " 时");
show(hashCode);
int k = hashCode >>> 16;
show(k);
int x = hashCode ^ k;
show(x);
System.out.println();
}
/**
* 显示一个数字的二进制,按照高16位在左,低16位在右的方式显示
*/
public static void show(int n) {
String s = Integer.toBinaryString(n);
s = fillZero(s);
System.out.print(s.substring(0, 16));
System.out.print(" | ");
System.out.println(s.substring(16));
}
/**
* 填充0到字符串前面使得总长32
*/
public static String fillZero(String src) {
StringBuilder sb = new StringBuilder(32);
for (int i = 0; i < 32 - src.length(); i++) {
sb.append('0');
}
return sb.append(src).toString();
}
}
这就是为什么HashMap可以放入键值null,因为计算hash中为null的hash值为0,然后putVal插入
1.5 元素实际位置计算
根据hash()获取元素所在链表的位置的方法为:tab[(n - 1) & hash]
,由于n为容量是2的幂,n-1的二进制形式是111111
这类二进制左边全1的形式,所以这个方法本质是截取hash二进制相应长度的0和1,如下例。
hash: 10111101
n - 1: 00111111
result: 00111101
// hash的最左端的1没有了,相当于只取二进制最右端几位
1.6 插入null原理
在hash计算中(上文),null的hash值为0,然后按照正常的putVal()
插入
1.7 new HashMap()开销
从源码中(下文构造函数)我们可以看到:
new HashMap()开销非常少,仅仅确认装载因子。真正的创建table的操作尽可能的往后延迟,这使得HashMap有不少操作都需要检查table是否初始化。这种设计我猜想应该是为了让人们可以不用担心创建HashMap的开销,大量创建HashMap,比如ArrayList<HashMap> a = new ArrayList<>(1000)
2. HashMap的变量
2.1 DEFAULT_INITIAL_CAPACITY
HashMap的默认容量是16,被DEFAULT_INITIAL_CAPACITY定义。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
2.2 MAXIMUM_CAPACITY
其最大容量为 1073741824(2的30次方)
static final int MAXIMUM_CAPACITY = 1 << 30;
2.3 DEFAULT_LOAD_FACTOR
默认装载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
2.4 TREEIFY_THRESHOLD
将链表转换成红黑树的阈值为8,即当链表长度>=8时,链表转换成红黑树
static final int TREEIFY_THRESHOLD = 8;
2.5 UNTREEIFY_THRESHOLD
将红黑树转换成链表的阈值为6(<6时转换),注意,这个是在resize()的过程中调用TreeNode.split()实现
static final int UNTREEIFY_THRESHOLD = 6;
2.6 MIN_TREEIFY_CAPACITY
要树化并不仅仅是超过TREEIFY_THRESHOLD ,同时容量要超过MIN_TREEIFY_CAPACITY,如果只是超过TREEIFY_THRESHOLD,则会进行扩容(调用resize(),因为扩容可以让链表变短),直到扩容>=MIN_TREEIFY_CAPACITY
static final int MIN_TREEIFY_CAPACITY = 64;
2.7 table
哈希表的数组主体定义,使用时初始化,在构造函数中并不会初始化,所以在各种操作中总是要检查其是否为null
transient Node<K,V>[] table;
2.8 entrySet
作为一个entrySet缓存,使用entrySet方法首先检查其是否为null,不为null则使用这个缓存,否则生成一个并缓存至此。
transient Set<Map.Entry<K,V>> entrySet;
2.9 size
HashMap中Entry的数量
transient int size;
2.10 modCount
记录修改内部结构化修改次数,用于实现fail-fast,ConcurrentModificationException就是通过检测这个抛出
transient int modCount;
2.11 threshold
其值=capacity * load factor,当size超过threshhold便进行一次扩容
int threshold;
2.12 loadFactor
装载因子
final float loadFactor;
2.13 serialVersionUID
用于序列化
private static final long serialVersionUID = 362498820763181265L;
3. HashMap的方法
3.1 构造函数
-
该构造函数并不初始化
transient Node<K,V>[] table;
,进行容量和装载因子的(范围)合法性验证,然而并没有对容量进行存储,只是用来确定扩容阈值thresholdpublic 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); }
-
显然
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
-
无参构造函数仅仅确认装载因子
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
-
通过Map构造HashMap时,使用默认装载因子,并调用putMapEntries将Map装入HashMap
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
3.2 hash(Object key)
Hash函数负责产生HashCode,计算法则为若key为null则返回0,否则:对key的hashCode的高16位和低16位进行异或
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// >>>表示无符号右移
}
3.3 comparableClassFor(Object x)
对于一个Object,若其定义时是class X implement Comparable<X>
,则返回X,否则返回null,注意一定Comparable<X>
中的X一定得是X不能为其子类或父类,用于红黑树中的比较
- 源码如下
static Class<?> comparableClassFor(Object x) {
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; ParameterizedType p;
if ((c = x.getClass()) == String.class) // bypass checks
return c;
if ((ts = c.getGenericInterfaces()) != null) {
for (Type t : ts) {
if ((t instanceof ParameterizedType) &&
((p = (ParameterizedType) t).getRawType() ==
Comparable.class) &&
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) // type arg is c
return c;
}
}
}
return null;
}
- 验证代码如下,将如下代码拷贝即可明白其原理。
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
class Scratch {
public static void main(String[] args) {
System.out.println(comparableClassFor(new C()));// class Scratch$C
System.out.println(comparableClassFor(new CS()));// null
System.out.println(comparableClassFor(new CSI()));// null
System.out.println(comparableClassFor(new CSIC()));// class Scratch$CSIC
}
static Class<?> comparableClassFor(Object x) {
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; ParameterizedType p;
if ((c = x.getClass()) == String.class) // bypass checks
return c;
if ((ts = c.getGenericInterfaces()) != null) {
for (Type t : ts) {
if ((t instanceof ParameterizedType) &&
((p = (ParameterizedType) t).getRawType() ==
Comparable.class) &&
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) // type arg is c
return c;
}
}
}
return null;
}
static class C implements Comparable<C> {
@Override
public int compareTo(C o) {
return 0;
}
}
static class CS extends C {
}
static class CSI implements Comparable<C> {
@Override
public int compareTo(C o) {
return 0;
}
}
static class CSIC implements Comparable<CSIC> {
@Override
public int compareTo(CSIC o) {
return 0;
}
}
}
3.4 compareComparables(Class<?> kc, Object k, Object x)
如果x=null,返回0;如果x的类型为kc,则返回k.compare(x);否则返回0
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
3.5 tableSizeFor(int cap)
对于给定cap,计算>=cap的2的幂。用于计算table数组大小
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
3.6 putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
先确定放入map时容量是否应该调整,调整好后,通过putVal一个个放入
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
//放入的map的size要大于0才插入
if (table == null) {
// pre-size,如果本map的table未初始化(同时没有任何元素),就根据放入map大小以及loadfactor计算出threshold,依然不初始化table
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)//放入的map超过threshold就扩容
resize();
//到这里容量问题解决了,就一个一个putVal插入
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);
}
}
}
3.7 size()
直接返回size变量的值
public int size(