本片重点
- HashMap存储结构
- HashMap的put和get方法
- HashMap扩容机制
HashMap存储结构
在jdk1.8以前,HashMap的存储结构是数组+链表,jdk1.8之后,引入了红黑树,当链表长度等于8时,链表就会转为红黑树,网上很多人说链表长度大于8就会转为红黑树,其实这是不够准确的,至于为什么,待会往下看源码就知道。
数组的特点:查询效率高,插入,删除效率低。
链表的特点:查询效率低,插入删除效率高。
红黑树的特点:查询效率高,插入删除效率低。
HashMap之put方法
接下来,我们来看看HashMap的put方法的源码。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
在HashMap的put方法里面,调用putVal方法,这个方法传了五个参数,其中有一个是hash(key),调用了hash方法,传了一个key进去,这个方法是干什么的呢?顾名思义,就是计算key的hash值。我们进去这个方法看看怎么处理的
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看到,当key==null时,返回0,当key不等null时,将key的hash值与其的hash值无符号右移16位进行异或运算得到的结果返回,最终得到了key的hash值。
接下来,我们进入到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;
}
}
//新的value覆盖原来的值
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;
}
这里有几个步骤:
1.初始化数组:
这个方法里面看到第一个判断if ((tab = table) == null || (n = tab.length) == 0),tabel就是hashmap数据结果中的数组。如果数组为空或者数组长度为0时,调用resize()方法(后面会讲),对数组进行初始化。
2.计算key在数组中的位置:
第二个判断if ((p = tab[i = (n - 1) & hash]) == null),i = (n - 1) & hash 就是将数组的大小与key的hash值进行与运算(&),得到key所在数组的位置。当key所在的位置为空时,直接插入数据并判断是否需要扩容。
注意:这里可能有人会有疑惑,为什么使用与运算,首选我们要明白,与运算得到的结果都会小于等于左右两边最小的值,也就是说,这里的i不管结果是啥,都是小于等于(n - 1) 或者 hash,而hash是远大于n-1的,所以就保证了i小于等于n-1。
3.key比较
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))。
当key所在的位置不为空时,首先会先判断新插入的key和原来的key的hash值是否相等,如果相等,也就是发生hash冲突时,会进行equal比较,如果相等则将value覆盖。如果不相等,则判断节点是否是红黑树
else if (p instanceof TreeNode)
如果是红黑树,则在红黑树插入数据,否则在链表中插入数据
4.红黑树中插入数据
if (binCount >= TREEIFY_THRESHOLD - 1) // TREEIFY_THRESHOLD = 8
treeifyBin(tab, hash);
当链表长度大于等于7时,执行treeifyBin(tab, hash)方法。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //MIN_TREEIFY_CAPACITY = 64
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
treeifyBin方法有个判断,(n = tab.length) < MIN_TREEIFY_CAPACITY,MIN_TREEIFY_CAPACITY为常量64,如果数组大小小于64,执行resize()进行扩容,而不是转为红黑树,所以HashMap链表转为红黑树的前提条件是:“链表长度大于等于7并且数组大小大于等于64”。只有符合条件了,链表才会执行do-while循环,将链表转为红黑树。replacementTreeNode(e, null)方法将链表节点转为树节点,treeify(tab)生成红黑树。红黑树的插入比较复杂,这里不多讲。
5.链表中插入数据
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;
}
首选我们要明白,链表有头指针(heade),节点(node),尾指针(tail),每一个节点有一个next指向下一个节点。链表插入操作是遍历循环链表,(e = p.next) == null,判断当前节点的next是否为null,如果为null,插入一个新节点(node),插入节点后,判断链表个数是否大于等于数组长度-1,如果是,则转为红黑树。也就是说当链表的长度大于等于数组长度-1时,就会转为红黑树。新增一个节点之后需要判断是否需要扩容
当前节点的next不为null时,判断key的值是否相等,如果相等,结束循环,执行后面的代码,将新的value覆盖原来的value
//新的value覆盖原来的值
if (e != null) { // existing mapping for key
V oldValue = e.value; //覆盖原来的值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//覆盖值之后,需要对链表的节点挪位置,这里不讲,感兴趣可以自己研究
afterNodeAccess(e);
return oldValue;
}
put方法在新增数据时需要根据threshold判断是否需要扩容
if (++size > threshold)
resize();
put方法操作步骤总结:
1.计算出key的hash值
2.如果是第一次插入,需要初始化数组(懒加载),默认大小为16
3.根据key的hash值和数组的大小计算出key在数组中的位置
4.如果当前位置为空,则在当前位置中插入一条数据,然后根据threshold判断数组是否需要扩容
5.如果当前位置不为空,即发生hash冲突,判断key是否相等,如果相等则覆盖原来的值,不相等则判断是否存在红黑树,存在则在红黑树中插入数据,不存在则在链表中插入数据,如果链表的长度大于等于数组长度-1,并且数组大小大于等于64时,则链表转为红黑树。
**总结:**这篇文章就讲到这里,主要讲了HashMap的结构,put方法源码,下一篇讲HashMap的get方法。