概要
这篇文章主要从初始化、添加元素、容量resize这几个角度,讲解HashMap的实现原理
HashMap核心属性
Node数组,这是HashMap底层存放数据的地方,我们调用put添加数据,其实就是操作这个Node数组
transient Node<K,V>[] table;
- Node是HashMap内部静态类
- hash:存放的是key进行hash计算的结果
- key、value:就是我们调用put方法时传的key-value
- next:存放当前元素的下一个元素,数据结构为单向链表,这块下面会讲到。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key; //key
V value; //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;
}
}
初始化
调用new HashMap<>()构造函数时,内部只是对loadFactor(加载因子)进行赋值而已,初始值为0.75,然后并没有做其他操作。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
put添加元素
- 假设当前添加一个元素,put(“张三”, 25)
- put方法源码如下,最终调用的是putVal方法,这个才是真正添加元素的方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
再来看下putVal源码,重点先看下注释部分:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
/*
* tab = table,因为第一次执行table肯定是null,所以这个if条件为true
* 此时会执行以下代码:
* (1)resize重新初始化
* tab = resize()
* (2)把初始化后的长度赋给n
* n = tab.length;
*/
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
// existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize 方法的源码如下,第一次会执行红框部分的代码。
DEFAULT_INITIAL_CAPACITY常量用的是左移计算,1 >> 4对应的二进制是10000,相当于十进制的16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
因此,第一次resize之后,table = new Node<K,V>[16]。如下图:
重新回到putVal方法,resize过后执行下面这个代码
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
- i = (n - 1) & hash:计算当前元素会落在0 ~ n - 1之间的哪个位置,也就是计算数组下标值。当key=="张三"时,此时i = 2
- p = tab[2],根据上图可以确定,p == null 结果是true
- 此时执行newNode创建一个节点
再多加几个数据:李四、王五、赵六
我这边整理了下标计算的代码,要测试的直接拿过去执行就行
public static void main(String[] args) {
int n = 16; //Node[]数组长度
String key = "张三";
int hashCode = key.hashCode();
int hash = hashCode ^ hashCode >>> 16;
int i = (n - 1) & hash;
System.out.println(i);
}
当加到key="孙七"时,计算的下标刚好为1(这个位置已经被李四占用了),此时再判断p == null肯定是false。此时执行的代码是下图红框部分:
这里的意思是
- 查找p.next对象,如果为空就创建节点作为p.next,如果有值就继续往下判断,也就是p.next.next,直到找到null为止
- 上面有提过,这是个单向链表,其实就是查找链表最后一个元素,给最后一个元素继续添加节点
- 另外,当单向链表长度>8时,tab[i]当前元素会转换为红黑树。也就是tab[i]变成TreeNode<K,V>,而不再是Node<K,V>
- 后续再需要添加节点时,先判断当前节点是不是TreeNode类型,以此判断是执行p.next追加数据,还是往红黑树添加节点
尾篇
整个HashMap从初始化到重构大小,再到添加数据的源码基本就是这样。由于个人水平有限,红黑树的实现原理就不继续介绍了,哈哈,保住头发要紧吧。感兴趣的可以搜下其他文章,资料应该很多~~