前言
上一篇讲解了HashMap
的一些基本知识,包括扩容阈值、载入因子、扩容机制、哈希冲突等,能够完全理解这些内容对自己来说是十分有帮助的。
探索JDK源码-每行代码堪称教科书级别(2)HashMap.java(上)
这篇文章我们将来剖析HashMap
中最基本的用法:put、get、replace、remove。平时开发工作中想必都离不开对这么几个方法的使用了,那都有哪些最佳实践呢?
put()放置一对key-value
- 直接看源码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* @param key的哈希值
* @param onlyIfAbsent true的话只有key不存在时才插入
* @param evict false时说明表处于创建模式
* @return 已存在key的话返回之前的值,不存在则返回空
*/
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;
// 对当前的key计算表的索引:i = (n - 1) & hash
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; // key已经存在,并且在链表的头节点
else if (p instanceof TreeNode)
// 当前key的哈希通过二叉树保存了
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
// 将hash值对应的节点链表树化
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // 已经存在这个key了
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value; // 刷新key对应的value
afterNodeAccess(e);
return oldValue;
}
}
++modCount; // Map的修改次数加一
if (++size > threshold)
resize(); // 判断是否扩容
// 这个方法在HashMap中是一个空方法,LinkedHashMap才需要用到
// 也就是说LinkedHashMap也是用同一个put方法,不过在put之后需要执行一个当作
afterNodeInsertion(evict);
return null;
}
put方法虽然只传传了两个参数(key,value),但是内部指定了在插入一个值的时候直接覆盖已经存在的值,并返回之前的值,不存在则返回null。
replace()替换一对key-value
- 直接看源码
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
这里会根据key的哈希值,在表中找到对应的Node节点,如果存在的话,直接修改它的value,并且返回原来的value,不存在则返回null。
get()获取键key对应的value
- 直接看源码
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
显然get方法和replace方法基本一样,都需要获取键key对用表中的节点,并返回值value。那这里就很有必要看一下getNode
这个方法是怎么拿到节点的。
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) {
/* 同一个hash值被放到一个Node链表中,需要确定当前key是否在链表中 */
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
/* 如果当前是TreeNode的话,需要去Node树中查找 */
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(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;
}
原理如下:
先确定table中是否存在哈希为hash的节点(节点链表的起始节点);
-----存在则检查该节点的key是否和要查找的key一致:
----------是则返回该节点;
----------否则判断起始节点是否是树节点:
---------------是则说明需要遍历节点树来找到目标节点;(红黑树)
---------------否则说明继续遍历节点链表。
都没有找到的话返回null。
remove()移除一对key-value
- 直接看源码
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
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;
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;
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);
}
}
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;
afterNodeRemoval(node);
return node;
}
}
return null;
}
随让代码较多,但它的思想并不复杂。首先需要找出key所在的链表或者二叉树;其次从链表或二叉树中找出key所在的节点,如果没有这个节点也就不存在移除了,直接返回null;如果存在的话,接着从链表或节点中取消这个节点的链接,并将节点的值value返回。
最佳实践
那么在实践中我们该如何用好这几个常用方法呢?
这里首先以HashMap<String, List<String>> map
这个作为例子来讲
// 假设已经做好初始化工作,我们要尝试对map做修改
// 比如有一个key,和一个value
void addValueByPut(){
List<String> list = map.get(key);
if (list == null) {
list = new ArrayList();
map.put(key, list);
}
list.add(value);
}
这里我们只会在map中不存在key的时候才put一个list进去,因为对于除了基本类型之外的对象,通过get获取到的其实是一个引用,从而我们可以直接对这个引用做修改就可以实现值得插入了list.add(value)
。
void addValueByPut(){
List<String> list = map.get(key);
if (list == null) {
list = new ArrayList();
}
list.add(value);
map.put(key, list);
}
这种是不建议的做法,因为你每次往list里面添加一个数据,但都需要对map做一次put操作,这是毫无意义且浪费资源的,你只需要在map中不存在这个key的时候put一次就行。之后不管对list做多少次add操作都不会受到影响了。
那对于value不是引用类型的,比如Integer
、Long
、String
这种该怎么高效操作呢?建议是直接put,你可能会想先判断key存不存在,再决定要用put或者replace,其实在数据量多一些的时候,哈希碰撞就会相应的增加,你这时用replace和用put的时间复杂度是不相上下的。所以直接put就行了。
那当你开发过程中发现每次要添加的key-value在map中必定是有这个key的时候,那么强烈使用replace,因为它会少一些不必要的判断,而是直接找到key节点,并修改掉它的value。
最后是remove的细节,像一开始举例的,如果你的value是一种引用类型,那么你除非是想要整个key-value从map中删除,不然建议使用以下方式。
void removeValueJustOne(String key, String value){
List<String> list = map.get(key);
if (list == null) {
return;
}
list.remove(value);
// map.put(key, list);
}
注意,map给你缓存的引用型对象在内存中有且只有一份,你完全可以通过直接修改引用,从而达到删除一个list子项的目的,而不必再把remove(value)
后的list再put到map中了,那简直是多此一举。
总结
这次重点探索了HashMap
是如何增加、修改、获取和删除一个键值对的。如果说你不去看这么一份源码的话,你根本不会知道每个方法的复杂度会是怎样的;如果你知道了当数据量够大时哈希冲突变得密集,每次操作的遍历会逐渐变慢时,你就会去思考在代码中怎么去避免一些不必要的操作来提升执行效率了。虽说HashMap
由于用的hash从而速度十分之快,但是作为一名程序员不应该追求更快吗!所以合理的使用HashMap
对你将来处理高并发、多线程等的时候是有一定帮助的。
预告
下一篇我们会带来HashMap
一些更高级的用法!比如forEach(...)
,compute(...)
等。