前言:一文读懂HashMap
本文从初始化、put()两个角度出发,领略HashMap源码,内容包含各参数解释、扩容机制、hash算法及JDK1.7和JDK1.8的不同,在文末会附上面试题及答案。
参数解析
capacity:容量 是一个抽象概念,表示里面HashMap中含有的key-value对个数,用属性size表示
transient int size;
threshold:阀值 是否需要扩容的标记 loadFactory:加载因子,用于计算threshold 加载因子为float类型 threshold = capacity * loacFactory;
1. HashMap初始化
1.1 HashMap1.7初始化
初始化的目的:初始化各参数、初始化内部的table::Entry字段
只需阅读无参构造方法,了解JDK1.7中HashMap的初始化发生在new HashMap中
a. 无参构造方法
HashMap(){
// 1.初始化参数
this.loadFactor = DEFAULT_LOAD_FACTOR; // 默认 DEFAULT_LOAD_FACTOR=0.75f
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
// 2.为table字段赋值
table = new Entry[DEFAULT_INITIAL_CAPACITY];
}
b. 有参构造方法
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
// 1.进行参数校验
...
// 2.初始化参数
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
// 3.为table赋值
table = new Entry[capacity];
// 4.init方法为一个扩展方法,HashMap中并无实际作用
init();
}
1.2 HashMap1.8初始化
JDK1.8中构造方法中只完成了“部分”参数的赋值操作,且并未真正构造table::Node对象。
a.无参构造方法
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
b.有参构造方法
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
1. 检验参数
2. 部分属性赋值
this.loadFactor = loadFactory
// 查找距离initialCapacity最近的2的幂次方:如:initialCapacity = 10,则
this.threshold = 16;
this.threshold = tableSizeFor(initialCapacity);
}
可以看出JDK1.8中构造方法并未真正的进行初始化操作,只是对部分属性赋值
2.HashMap中put()方法
不管1.7还是1.8,put()方法的目的都是保证给HashMap增加一个key-value对。这其中涉及到的扩容:resize()、hash和索引计算等知识。
2.1 JDK1.7中的put()操作
put()流程:
- 判断table是否为空:若为空则初始化(JDK1.7中不会出现为空的情况)
- 判断是否已存在相同key
- 不存在相同key:则调用addEntry()增加entry节点 HashMap扩容.png
public V put(K key, V value) {
// 1. 判断table是否为空:若为空则初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
// 2. 判断是否存在相同key
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))) {
// 判断key相同 替换旧值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 3. 无相同key:直接添加数据
modCount++;
addEntry(hash, key, value, i);
return null;
}
让我们深入探究put()方法的内涵,学习完你将了解以下知识
- 怎么计算hash和索引,如果hashCode冲突了,那么会发生什么?
- HashMap中如何判断key相同?如果新插入元素中索引位置有元素,会发生哪些情况?JDK1.7中HashMap采用的是头插法还是尾插法?请描述一下插入的整个流程?
- JDK1.7中扩容发生在哪些时刻?先插入元素再扩容还是先扩容再插入元素?
让我们来看看第一个问题:如何计算hash和索引?
让我们来看看第一个问题:
- HashMap中如何判断key相同?
答:首先判断索引处是否有值,有值则遍历相同索引的值,判断是否存在相同的key
首先hash相同其次key的索引或内容相同
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) - 如果新插入元素中索引位置有元素,会发生哪些情况? 答:如果索引位置有元素,则分为key相同/key不同,key相同则直接替换旧值即可,key不同则程序往下运行,后续统一利用addEntry()方法插入。
- JDK1.7中HashMap采用的是头插法还是尾插法? 答:要想搞懂这个问题,需要看看addEntry()源码
// 3. 无相同key:直接添加数据
modCount++;
addEntry(hash, key, value, i);
return null;
void addEntry(int hash, K key, V value, int bucketIndex) {
// a.判断是否需要扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
// b.创建元素
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
// 取出当前位置的元素,如果是新添加的key,则e为null,已经有的元素为不为空。
Entry<K,V> e = table[bucketIndex];
// 添加新的key-value值或构建链表
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
所以可以看出是采用头插法插入元素,原索引处元素(或为null)作为新元素的next
- JDK1.7中扩容发生在哪些时刻? 答:扩容采用resize()方法 扩容步骤
- 设置新的entry
- 转移旧元素至新的entry上(原索引为i的数据在新entry中索引是?)
2.2 JDK1.8中的put()操作
JDK1.8中原数组+链表的数据结构修改为数组+链表/红黑树 阅读完本节,你将了解
- JDK1.8中初始化的时间?
- JDK1.8中扩容时机
- JDK1.8中hash和索引计算
回归put()方法本身,put()目的加入一个元素,整体流程图如下
由流程图可以得出:
- JDK1.8中初始化的时间? 答:真正的初始化发生在put(Object key)时
- JDK1.8中扩容时机? 答:先添加节点再判断是否需要扩容
- JDK1.8中hash和索引计算
index:i
hash = hashCode^hashCode>>>16;
i = e.hash & (newCap - 1);