本来介绍一下HashMap最重要的两个方法,get和put。在阅读文本之前,请先阅读HashMap实现原理分析(1) 。
- HashMap中大致流程
下面先看一下这些HashMap在实现过程中的一些基本属性。
//数据实际存储结果
transient Node<K,V>[] table;
// table中时间存储的元素个数
transient int size;
//下一次需要对table进行扩容的临界值
int threshold;
// 扩容时的平衡因子 final float loadFactor;
下面是默认常量
//默认table的初始化大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// table 最大长度
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的加载因子,用于对table进行扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当链表转红黑树时,链表最大长度
static final int TREEIFY_THRESHOLD = 8;
// 当树的元素小于此值时,会将红黑树结构转换成链表结构
static final int UNTREEIFY_THRESHOLD = 6;
// 当链表转红黑树时,table最小长度
static final int MIN_TREEIFY_CAPACITY = 64
通常使用不带参数的new HashMap()方式创建一个HashMap对象,该对象就会默认使用DEFAULT_LOAD_FACTOR作为平衡因子。在用户第一次执行put操作的时候,会把table初始化为一个大小为DEFAULT_INITIAL_CAPACITY的数组。
每size>threshold的时候,就会对table进行扩容,threshold的值=loadFactor*table.length。因此当loadFactor越大,扩容触发就越晚,越节省内存空间,但是发生hash冲突越容易。相反当loadFactor越小,扩容触发就容易,就会需要消耗更多的内存空间,但是发生hash冲突相对比较难。因此默认的0.75的平衡因子是容量和性能之间平衡。
当table的同一个位置已经存在元素的时候,这时候相同位置的元素会变成一个链表。如果链表的长度已经达到默认值TREEIFY_THRESHOLD并且table中的元素个数达到MIN_TREEIFY_CAPACITY,链表结构会转换成红黑树。当HashMap中的元素被删除之后,如果树的元素个数小于UNTREEIFY_THRESHOLD,红黑树又会转换为链表结果。
用户执行put操作后,把key和value的封装成为Node对象,然后放入到table中。
static class Node<K,V> implements Map.Entry<K,V> {
// put操作key的hash值
final int hash;
// put操作的key
final K key;
// put操作的value
V value;
// 指向下一个hash相同的Node,用来简历链表和树
Node<K,V> next;
}
- get方法
通过get(key)方法获取key所对应的value值。具体如下:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first, e;
int n; K k;
if ((tab = table) != null &&
(n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash &&
((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 {
//hash和key必须同时满足相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
通过上面源码可知,get方法会调用getNode方法,在调用getNode方法之前,把key调用hash方法计算获取key的hash值,hash方法的实现,请参考在HashMap实现原理分析(1) 。getNode方法第一步是把多个步操作融合在一起了:
if( (tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null)
这条if语句注意完成两件事:
第一:把table赋值给tab。table是HashMap实际存放数据的数组,以后在getNode方法中操作tab就相当于操作table数组;
第二:判断HashMap是否为空。当该HashMap创建后,没有进行过put操作,那么tab == null。
第三:首先把判断tab[(n - 1) & hash])位置的元素复制给first,后面可以直接通过操作first就比较方便。然后判断fisrt的值是否为null。关于(n - 1) & hash这含义也请参考HashMap实现原理分析(1)。
如果if条件成立的话,那说明就是传入的key是在此HashMap中是存在的。
下面来进入第二个if语句:
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
这里的是先判断fisrt的hash是否与key的hash相同,如果hash相同,然后比较first.key与key是否相等。只有当hash与key值同时满足相等,first才是所有查找的对象,最后返回first。
如果上述if条件不满足,并且 fisrt.next的是否为空,则对fisrt.next后续节点进行遍历查找:这里又会分为两种情况,如果fisrt是已经是红黑树,那么遍历红黑树,否则把fisrt作为链表的头结点,从链表中查找同时满足hash和key值相等的Node对象,并返回。
- put方法
下面先看一下源码:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
//n是table的大小 i是table的索引
int n, i;
//1.把table赋值给tab,方法内部操作tab来代替操作table
//2.判断tab是否为null
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//3.如果对应tab[i]的位置为null,把新创建的Node赋值到tab[i]
// 这里还有一个赋值操作p=tab[i],这样后面操作tab[i],可以用操作p来代替
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//当HashMap中已经存在了参数key,那么e指向旧的Node对象或者是null
Node<K,V> e;
K k;
// 4. p.hash和key hash 以及p.key和key如果同时相等,那么用新value代替旧的value
// 这里没有立即执行e.value=value,而放在了最后。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 5.当发现p.key与key不相等,且p是红黑树
else if (p instanceof TreeNode)
//6. 把新key和value插入到红黑树中,并且返回旧的Node对象(如果存在key相同的旧Node对象)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//7.如果p是个链表,那么新key和value创建的Node节点,假如到p链表的尾部
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//8.当链表的大小超过了链表与红黑树转换的临界值的时候,把链表转为树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 9. 当链表中找到一个与方法参数key的hash和key同时相等的节点后,停止链表遍历
// 此时e就指向了链表中与参数key一致的Node节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//10. 如果e不为null表示e是老值,那么put方法返回HashMap之前存在的就value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//11. 当HashMap中的元素个数大于进行扩容的临界值,那么对table进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
在上面的源码中已经给出了比较详细注释信息,通过这些注释,可以大致了解HashMap在执行put操作的流程,或者参考下面的流程图。这里还有一些具体的执行细节,比如红黑树的遍历,链表与红黑树的转换等由于篇幅有限,没有做过多介绍。