HashMap源码解读
今天面蘑菇街的时候遇到了面试官问我put过程和链表退化的问题,发现自己对HashMap还是不够了解。故作此篇源码分析,旨在加强自身理解。
put
map,是一个k-v存储结构,put也仅仅存入key 和 v即可。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
然后调用了putval
这个函数,结合源码注释进行解读。
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* key的哈希值
* @param key the key
* key的值
* @param value the value to put
* v的值
* @param onlyIfAbsent if true, don't change existing value
* 如果onlyIfAbsent==true,不覆盖
* @param evict if false, the table is in creation mode.
* exict==false,
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果列表为空,或者链表容量为0,则扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//由key计算出的hash值,再计算出hash值在hash表中的位置,如果位置为空,则创建新的存储节点插入。
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果想要插入散列表的位置上一级有了元素,看看他们的key值是否相同
// 待插入节点的key值等于原有节点的key值,则覆盖更新。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//如果挂在散列表的结点已经变成红黑树(节点数量>7),则以红黑树的方式继续插入
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//未变成红黑树
for (int binCount = 0; ; ++binCount) {
//在链表里,他们拥有相同的hash值了,所以在链表里试图插入
//如果成功遍历到链表尾部,就创建新节点进行插入,插入后检查是否满足树化阈值
//如果未能成功遍历到链表尾部,且遇到相同的key,打断查找。e.key=node.key
if ((e = p.next) == null) {
//找到合适的链表位置插入。
p.next = newNode(hash, key, value, null);
// 如果达到了树化的阈值,进行树化操作
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果在链表里找到了相等key值的结点,就进行覆盖
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// onlyIfAbsent 默认为true,覆盖value值。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//在hash表中,这个函数的内容是空的
afterNodeAccess(e);
return oldValue;
}
}
//统计节点数量
++modCount;
//看散列表是否需要扩容,
if (++size > threshold)
resize();
//在hash表中,这个函数的内容是空的
afterNodeInsertion(evict);
return null;
}
get
HashMap是一个典型的存储复杂,查找快的存储结构,故get值也是值得一看
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
通过只算好key的hash值,在getNode
中进行查找
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//散列表空表,散列表的中hash值计算出的位置为null,都为查找不到
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//先把链表头结点单独拿出来检测
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//树化结构以红黑树的方式查询优化
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//非树化结构的话,链表进行遍历,找到key值相同的结点。
//如果这都没找到,直接返回null了
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
remove
因为被面试官问到了退化问题,这个remove
我不想放过
支持两种,一种是按key,一种是按key&&value
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
看来删除节点的核心为removeNode
方法
/**
* Implements Map.remove and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue if true only remove if value is equal
* 就是是否拿值也来当做匹配参数
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//对空表,散列表的中hash值计算出的位置为null,都视为查找不到,与get中相同。
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//判断头结点是否为目标,如果是则找到目标删除节点,赋值给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
//在红黑树中查找待删除节点
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//未树化的链表中查找待删除节点
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//开始删除了!如果matchValue==true,则需要验证key相同的前提下,value是否相同,如果不相同,则没找到删除节点。
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//分别试图从 树中,散列表中,链表中删除。
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
//记录修改次数
++modCount;
//更新尺寸信息
--size;
//hashmap中此函数为空
afterNodeRemoval(node);
return node;
}
}
return null;
}
注意,我在面试的时候被问及退化的问题,我在面试中回答的是,应该会退化成链表,因为退化成链表的话,查询的性能更优,但是我的思考方式是存在有问题的,因为看到了之前一篇博客里的想法:出于平均查找长度考量,在链表值到8的时候,进行树化。
树退化成链表
这个步骤是存在的,就在上面代码的removeTreeNode
的方法中
至于真正的退化的代码,则如下,把树节点装配成链表。
/**
* Returns a list of non-TreeNodes replacing those linked from
* this node.
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
真正的原因
挂在散列表上的链表长度,小于8的话,平均查找长度基本在O(3)水平,如果超7,平均查找长度就大于O(4)了。故进行树化更能发挥HashMap快速查询的优势。但并不是说短链表的查找性能比红黑树好了,一开始不直接用红黑树是因为转化等消耗不值得。如果仅仅保存少量数据就存红黑树里,会造成空间的浪费。
顺便了解一下存储的结点
链表的结点
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V 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 int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {}
public final boolean equals(Object o) { }
}
红黑树的结点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
//...红黑树操作省略
}
TreeNode<K,V> 继承与 LinkedHashMap.Entry<K,V>继承于 HashMap.Node<K,V>
所以占用空间的角度看,红黑树的存储结构比HashMap.Node多不少。因此每月必要一开始就使用红黑树来存储数据。
如有纰漏,感谢指出!