最近面试被问HashMap
容器的实现原理,答的一塌糊涂。。。虽说一直念叨着说要看看Java
容器的源码,但总是被耽搁了,今天终于静下心来看了🤦♂️。
注明:以下源码分析都是基于jdk 1.8.0_221
版本
HashMap源码分析目录
一、HashMap
概述(一图以蔽之)
HashMap
的类声明如下
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap
是一个<key,value>
(或称键值对
)容器,其底层实现是使用一个hash数组
指向多个不同的链表
。每次我们放入一个<key,value>
,它会自动计算key对应的hash值
,然后根据hash值
插入到不同的链表
中。
注明:可能会有人对为啥要用hash数组
套链表
产生疑问,这是因为实际插入过程中会出现多个<key,value>
的key计算出的hash值
相同(哈希冲突),如上图的table[1]
。但是当链表太长时,在容器中查找<key,value>
,每次都要遍历耗时长,降低了查找效率,所以在Java 8
中,引入了红黑树
。默认当某个hash值下超过了8个<key,value>
,此时就需要转化成红黑树
,如果上图中的table[14]
。
二、HashMap
类的属性
1、HashMap
类静态属性
/**
* 序列化的版本号
*/
private static final long serialVersionUID = 362498820763181265L;
/**
* 默认的初始化容量大小,并且必须是2的幂(主要是考虑效率,后面有介绍)
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大的容量(容器中存放<key, value>的最大数量)
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 负载因子
* 当容器中<key, value>的数量超过capacity * DEFAULT_LOAD_FACTOR时,需要扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 链表转红黑树阈值
* 当某个hash值下<key, value>用链表存储,并且链表长度不小于该值,就需要转成红黑树
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 红黑树转链表阈值
* 当某个hash值下<key, value>是用红黑树存储,并且树中的节点数小于该值,就需要转成链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 最小树形化容量阈值
* 当哈希表中的容量 > 该值时,才允许将链表转成红黑树操作,否则直接扩容。
* 为了避免进行扩容、链表转红黑树选择的冲突,并且这个值不能小于 4 * TREEIFY_THRESHOLD(链表转红黑树阈值)
*/
static final int MIN_TREEIFY_CAPACITY = 64;
2、HashMap
非静态属性
transient
关键字的作用是在序列化的时候排除该属性,比如写入硬盘持久化,用这个关键字修饰的属性在对象保存时不会写入。(不过HashMap
类在尾端重写了序列化方法,手动指定了需要序列化的属性)
/**
* table数组,也称hash桶数组
*/
transient Node<K,V>[] table;
/**
* entrySet属性,把<K,V>存放到Set容器中(一般hashmap的遍历用此属性)
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 容器key-value数量(注意与容器的容量(容器可存放的数量)不同)
*/
transient int size;
/**
* 容器进行结构性调整(增加或者删除键值对等操作,不包括修改value值)的次数
*/
transient int modCount;
/**
* 容器中能容纳的key-value极限,capacity * loadFactor,超过就需要扩容
*/
int threshold;
/**
* 负载因子,默认是0.75(前面类的静态属性已经定义过了)
*/
final float loadFactor;
注 意 : \color{red}注意: 注意:上面提到的容量
就是table
数组的长度,size
是容器中存放的key-value
数量,threshold
= 容量 * 负载因子,表示的该容器最多可以放置多少个key-value
。
三、HashMap
类的构造器
查看HashMap
类文件,可以发现一共有4个构造器。
/**
* @param initialCapacity 初始化容量大小
* @param loadFactor 负载因子
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
// 检查initialCapacity的合法性
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
// 检查initialCapacity是否超过了可设置的最大容量(类静态属性)
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 检查loadFactor负载因子的合法性
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//初始化threshold稍微复杂一点,tableSizeFor方法解析见本博客尾端
this.threshold = tableSizeFor(initialCapacity);
}
/**
* @param initialCapacity 初始化容量大小
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
//默认负载因子为0.75
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 只设置负载因子为0.75,其它值全部默认
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
/**
* 复制构造函数,将另外一个map初始化构造
*
* @param m 其它map容器
* @throws NullPointerException
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
//将m容器中的所有entry放入新建的容器对象中
putMapEntries(m, false);
}
四、增加key-value
相关方法
1、put
方法
put
方法,往容器中添加key-value
,允许key = null
,也允许value = null
。
/**
* 往容器中添加`key-value`,允许`key = null`,也允许`value = null`
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
2、putVal
方法
putVal
方法的作用是往map
容器中插入一个key-value
。
/**
* Implements Map.put and related methods.
*
* @param hash key的hash值(调用hash()方法)
* @param key 插入键值对key
* @param value 插入键值对value
* @param onlyIfAbsent 设为true时,表示如果容器已经存在这个key就不进行修改
* @param evict 为 false时,表示容器正处于创建(其它map传入初始化)
* @return previous value, or null if none
*
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab指向 对象的table数组(hash桶数组),p 指向hash对应的桶
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果容器为空,则需要调用resize方法,初始化table数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 将p指向hash对应的hash桶
if ((p = tab[i = (n - 1) & hash]) == null)
// (n - 1) & hash求出hash对应的table数组下标,如果这个位置为空,说明这个桶为空
// 直接放入table中,不需要生成链表、红黑树等
tab[i] =