Java HashMap的put过程
1. 通过源码来分析HashMap的put过程
Object key = new Object();
Object value = new Object();
Map<Object, Object> map = new HashMap<Object, Object>();
map.put(key, value);
当我们执行上面这段代码时,HashMap的底层是怎么的一个实现过程呢,下面我们就来一一揭晓。
首先程序会执行HashMap里的put方法就是下面的这个代码啦。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
从这个put方法的代码可以看出首先调用hash方法,这个方法时干嘛的呢?实际就是计算这个key的hash值,hash方法的代码如下,经过这个操作之后就获取到了key的hash值。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
然后就调用了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;
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;
}
首先这里先判断一下这个存放元素的数组是不是null(我这里分析的是JDK1.8的代码,在jdk1.8中new HashMap的时候不会先创建这个数组,而是在第一次添加元素的时候创建)。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
就是这个if 判断,如果数组是空,就调用 resize方法去new出这个数组并且大小为16(默认初始大小)。然后就去计算当前我们添加的这个元素在数组中的索引,代码如下。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
代码中的 i就是当前的这个元素应该在数组中的索引,(p = tab[i = (n - 1) & hash]) == null,p表示当前的这个位置储存的元素。说明现在已经出现了hash冲突,下面就是解决hash冲突的方法。
2. 解决Hash冲突
- 如果
p == null就表示当前这个位置没有存储任何元素,就把我们当前新添加的 生产一个新的 Node 元素,并把这个新的Node元素存放在数组中 i 的位置。 newNode方法如下。Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next); } - 否则(也就是 p != null)就是数组i的位置存有元素,就进行下面的操作。
首先比较Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;p.hash == hash && ((k = p.key) == key数组i位置的元素p的hash等于我们新添加的元素key的hash(这个p是一个Node类型的,p的hash实际就是p的key的hash)并且这个元素p的key==我们新添加元素的key或者key != null && key.equals(k)))就表明我们添加的元素的key就是数组i位置p元素的key。后面就会更新这个p元素的value为我们新添加元素的value。p.hash == hash && ((k = p.key) == key这个判断是为了比较当key是基本数据类型的时使用的。key != null && key.equals(k)))是为了当key为引用数据类型时使用的。 - 如果 元素p既不等于null,并且p的key也不是我们新添加的元素的key,这时就就需要进行下面的操作啦,就要把我们新添加的元素添加到数组i位置下组成的链表里。
for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 如果p的后一个是null就把我们新添加的元素添加到这个链表的末尾 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // 判断这个链表的长度是不是大于阈值,大于就把链表转成红黑树 treeifyBin(tab, hash); break; } //执行和上面解决hash冲突的第二步一样的判断和操作。 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; //说明这个链表中存在元素的key和我们新添加的key是相等的, p = e; } - 最后判断 e 是不是null,如果不是说明这个map中存在我们新添加的key,然后就执行下面的程序判断是不是要覆盖掉原来的value值。
if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) // onlyIfAbsent 表示当原来的值为null的时候才替换我们添加的值 e.value = value; // 执行后序操作,如果使用的是LinkedHashMap的话会去维护链表的结构。LinkedHashMap继承了HashMap afterNodeAccess(e); return oldValue; }
最后执行下面的程序,表示我们新添加的元素,在map中没有相同的key,正常的添加到了map中,之后执行一些后序的操作
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
3.最后总结
Object key = new Object();
Object value = new Object();
Map<Object, Object> map = new HashMap<Object, Object>();
我们调用 map.put(key, value)方法时
- 计算key的hash值 ,
hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - 通过一定的方法计算 key 在数组中的索引 i ,
i = (n - 1) & hash] - 判断数组中索引为i的位置是否已经有值,
if ((p = tab[i = (n - 1) & hash]) == null)- 如果没有值,就 创建一个新的 Node,并且把这个新的Node存到这个数组索引为i的位置,
tab[i] = newNode(hash, key, value, null); - 如果有值,就在当前元素组成的链表上搜索是否存在已经存在的Node的key我们新添加的key相同的。
- 如果在当前的这个链表上存在Node的key和我们新添加的key相同,就把我们新添加的value覆盖掉这个Node的value。(实际上最后执行putVal方法时有个参数onlyIfAbsent可以控制是否覆盖)
- 如果在当前的这个链表上没有Node的key和我们新添加的key相同,就把我们新添加的key和value组成一个新的Node添加到这个链表的最后面 (JDK1.7之前都是插在这个链表的最前面就是数组i的位置,这样就会存在线程安全的问题容易发生链表的环形结构。JDK1.8之后插到这个链表的最后面,这样也不是线程安全的,因为在1.8中的put主函数中如下,如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中,造成先插入的数据被覆盖掉)。
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
- 如果没有值,就 创建一个新的 Node,并且把这个新的Node存到这个数组索引为i的位置,
本文详细解析了Java HashMap的put方法实现,包括计算hash值、解决hash冲突及链表与红黑树转换策略,深入理解HashMap的工作原理。
619

被折叠的 条评论
为什么被折叠?



