HashMap工作原理回顾 (基于JDK1.8源码分析)
程序员文章站
2022-03-03 23:07:25
...
HashMap工作原理回顾 (基于JDK1.8源码分析)
空的HashMap()构造方法
/**
* 默认初始容量为16.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量为2的30次方,如果有参构造器指定了更大的数值,那么仍然以2的30次方为准
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认加载因子,用来在扩容的时候使用
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 空参构造器,使用初始容量16和默认加载因子0.75
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
put(K key, V value) 方法
/**
* put方法源代码
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
- 我们可以看到方法内部实际调用了一个putVal方法,hash(key)是对key取hash值。
/**
* putVal源代码
* onlyIfAbsent参数:如果为true,则不改变已存在的value。
* evict参数:该参数在HashMap中并没有具体使用到,暂且忽略。
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果table为空或长度为0, 则初始化一个初始容量和默认阈值的Node数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 经过(n-1)&hash这样的逻辑与运算得到数组下标,在该数组下标位置的元素如果为空,则根据参数构造一个Node放入该下标位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 如果数组下标位置的Node不为空
else {
Node<K,V> e; K k;
// 如果下标位置的节点的hash值与给定的hash值相同且key相同,那么为同一个Node,替换之。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果Node为树节点的处理
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果下标位置的节点hash值以及key值对比不一样(其中包括hash值相同而key值不同的情况,也就是我们常遇到的hash冲突,应如下处理)
else {
for (int binCount = 0; ; ++binCount) {
// 如果下标位置的节点next节点为空,则指定该下标节点的next节点为新节点,跳出循环
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果下标位置的节点next节点不为空且hash值及key相同,直接跳出循环。不然替换为自身。
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;
// 如果size超过阈值(如16*0.75=12),则调用resize方法进行扩容且以2的N次幂进行扩容。扩容过程中会对数组内节点的位置进行重新计算从而进行变动。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
get(K key) 方法
/**
* 方法源码
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
- 我们可以看到getNode方法,我们进入此方法看看其具体实现。
/**
* 此处的hash值与put方法中的hash值计算算法是一样的,有兴趣可以自己看下源码
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 如果节点数组table不为空且长度大于0,经过(n-1)&hash这样的逻辑与运算得到下标后,在节点数组table中该下标位置的节点如果不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果该下标位置的节点hash值与参数hash值相等且key相同,则返回该节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果该下标位置的节点hash值与参数hash值不相等或者key不相同,且该节点的next节点不为空
if ((e = first.next) != null) {
// 处理如果是树节点的情况
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 不断循环查找next节点,直到找到匹配hash和key的那个节点并返回。
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
remove(K key) 方法
/**
* 方法源码
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
- 同样的,我们可以看到一个removeNode方法,我们再看看这个方法的具体实现。
/**
* 此处的hash也是和put方法中的hash值运算算法相同得到的,
* matchValue指是否需要匹配value值,movable指是否需要移除。
*/
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;
// 如果Node数组table不为空且长度大于0,经过(n-1)&hash这样逻辑与运算得到数组下标后,在数组该下标位置的Node不为空
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;
// 该下标位置的节点hash值与指定hash值相等且key相同,则匹配
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 若下标位置的节点hash值与指定hash值不相等或key不相同
else if ((e = p.next) != null) {
// 处理树节点的情况
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 循环查找next节点直到找到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);
}
}
// 如果匹配到hash值以及key值都相同的节点,且不需要匹配value或者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);
// 如果node节点与数组下标位置节点是同一个节点,那么将下标位置指向node的next节点,这样下标位置的原节点就不存在了。
else if (node == p)
tab[index] = node.next;
// 如果node节点与下标位置节点不是同一个节点,而是其链表中的其它节点(循环匹配到的其中一个next节点),那么就循环后p节点(看循环代码处实际是原来的e节点)的next节点指向node节点(看循环代码处实际是新的e节点)的next节点。
else
p.next = node.next;
// 修改变动变量加一,容量减一。
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
至此,HashMap的三个常用的基本方法源码分析就介绍到这里,欢迎各位同僚提出批评与建议,转载请说明出处,谢谢!
上一篇: jdk1.8 Optional使用
下一篇: SHELL删除空白行