看了不少的hashmap解析源码的文章,然后自己也想总结记录一下,很久就想抽点时间写一篇关于hashmap的文章了,但现实就是不允许,原因有很多,主要还是太菜了,哈哈,现在终于有时间写了。
这篇文章分为hashmap1.7和1.8的解析,自己也不知道怎么开头,也是想到什么写什么,文章顺序可能会有点乱,望各位谅解。现在是北京时间:2019年4月2日 01:30:19。
HashMap1.7解析
首先看看一张图片来了解下HashMap的结构把
简单说下hashmap的数据结构把。
数组+链表
数组在知道下标之后查询速度尤其快,O(1)的时间复杂度。
链表在增删的时候速度非常快,找到位置后(前提),处理只需要O(1)的时间复杂度,因为不需要移动数据的位置,只需要更改指向的地址即可。但是链表在遍历对比的时候非常慢,时间复杂度为O(n),当然,我觉得这里的链表主要还是用来做哈希冲突时的解决方法,也称为拉链法。
//默认初始化化容量,即16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,即2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态
static final Entry<?,?>[] EMPTY_TABLE = {};
//空的存储实体
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//实际存储的key-value键值对的个数
transient int size;
//阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold
int threshold;
//负载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;
//用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
transient int modCount;
//默认的threshold值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
PUT的过程分析
public V put(K key, V value) {
// 当插入第一个元素的时候,需要先初始化数组大小
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 如果 key 为 null,最终会将这个 entry 放到 table[0] 中
if (key == null)
return putForNullKey(value);
// 1. 计算key 的 hash 值
int hash = hash(key);
// 2. 找到对应的数组下标
int i = indexFor(hash, table.length);
// 3. 遍历一下对应下标处的链表,看是否有重复的 key 已经存在,如果有,直接覆盖,put 方法返回旧值就结束了。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 4. 不存在重复的 key,将此 entry 添加到链表中。
addEntry(hash, key, value, i);
return null;
}
数组初始化
private void inflateTable(int toSize) {
// 比如这样初始化:new HashMap(20),那么处理成初始数组大小是 32,保证数组的长度是2的n次方
int capacity = roundUpToPowerOf2(toSize);
// 计算扩容阈值:capacity * loadFactor
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 初始化数组
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
这里有个小问题,无论是1.7还是1.8的hashmap都是要将数组的大小初始化为2的n次方,该问题留在后面讲。。
计算该key在数组的位置
static int indexFor(int hash, int length) {
//hash是key的hash值,length为数组长度。
return hash & (length-1);
}
其实这里有个小问题,有些面试官也会问到,hash % length和 hash & (hash - 1)为什么不直接模 ,而要减一取余?是因为那样计算相对于模的话效率更高一点。各位可以自行测试,我在这里就不举例了。
添加节点到链表中
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经