前言
经过前两篇博客的分析,已经算是对HashMap有了较为深入的了解。既然是最后一篇讲解HashMap的博客,那必然要来分析HashMap比较不为人所知,但又相当有用的一些用法及其原理。
可以先从前两篇讲解开始了解HashMap哦:
探索JDK源码-每行代码堪称教科书级别(2)HashMap.java(上)
探索JDK源码-每行代码堪称教科书级别(3)HashMap.java(中)
高级方法
computeIfAbsent
简单理解就是从map中查找给定key的value并返回,如果找不到或者null的话,使用mappingFunction定义的返回值进行返回,并将这个返回值和key存到map中。
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (mappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
... // 初始化
if ((first = tab[i = (n - 1) & hash]) != null) {
... // 查找存在的value
V oldValue;
if (old != null && (oldValue = old.value) != null) {
afterNodeAccess(old);
return oldValue;
}
}
V v = mappingFunction.apply(key);
... // 将新的value放入map中
return v;
}
如果原来不存在key的话,会有几率触发整个map的结构调整,整体的流程相当于第二篇讲解中的最佳实践:
List<String> list = map.get(key);
if (list == null) {
list = new ArrayList();
map.put(key, list);
}
list.add(value);
所以从现在开始我们可以这样处理最佳实践
map.computeIfAbsent(key, k -> new List<String>()).add(value);
通过这个方法大大减少了代码量,而且不会影响执行效率。
computeIfPresent
简单理解就是从map中查找给定key的value,只有存在且不为null时才执行remappingFunction方法,并把方法的返回值刷新到map中;否则返回空。
public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
Node<K,V> e; V oldValue;
int hash = hash(key);
// key必须存在map中,并且存在的value不能为空
if ((e = getNode(hash, key)) != null &&
(oldValue = e.value) != null) {
// 执行额外的方法并返回新的value
V v = remappingFunction.apply(key, oldValue);
if (v != null) {
// 更新原来的value
e.value = v;
afterNodeAccess(e);
return v;
}
else
removeNode(hash, key, null, false, true);
}
return null;
}
所以说如果是想在key-value都已经存在的情况下进行操作的话,可以尝试该方法
HashMap<String, List<String>> map = new HashMap<>();
map.computeIfPresent("key", (key, list) -> {
list.add("new value");
return list;
});
深入思考会发现,由于只会取出存在的value并刷新value,不会有增加key-value的情况,所以也就没必要对整个map刷新结构了。
compute
简单理解就是从map中获取指定key的value,不管存不存在都会执行remappingFunction方法,并把方法的返回值刷新到map中。
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
... // 初始化
... // 查找存在的value
V oldValue = (old == null) ? null : old.value;
// 执行额外的方法,刷新value
V v = remappingFunction.apply(key, oldValue);
if (old != null) {
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
}
else if (v != null) {
// 有可能触发map结构调整
if (t != null)
t.putTreeVal(this, tab, hash, key, v);
else {
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return v;
}
在实践该方法时需要对apply方法的value判断是否为空,防止空指针异常哦。
扩展方法
forEach
该方法可以很方便的遍历整个map数据。对于遍历的每一对key-value都会通过action的apply方法进行进一步的处理,所以我们可以在apply中做自己的逻辑。
public void forEach(BiConsumer<? super K, ? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key, e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
简单的实践:
map.forEach((key, value) -> System.out.println(key + " : " + value));
其实也可以通过KeySet的迭代器进行遍历,但是个人不喜欢这样,因为是在做无用功,一两行代码就能完成的遍历,没必要写一堆代码。
不过特定条件下是有必要使用KeySet的,因为forEach中这块代码决定了这个方法在多线程中执行是有风险的。也就是说一个线程在遍历,另一个线程对map做了一个put或者remove之类会影响modCount的操作,导致modCount和mc不一致,代码会认为这次遍历的数据是不可靠的,然后抛出异常。
if (modCount != mc)
throw new ConcurrentModificationException();
保险起见,我们可以使用KeySet来解决这个多线程问题:
Set<String> keys = new HashSet<>(map.keySet());
keys.forEach(key-> System.out.println(map.get(key)));
这里我们使用map.keySet获取到的当前map中存在的所有key,并复制到我们的keys中,之所以这么做,是确保接下来的遍历不会直接引用到map中的keySet,从而避免了遍历异常。
接着就是对keys的遍历了,因为这里的keys是属于当前线程的,不会出现被其它线程修改的风险,所以可以直接forEach。
总结
总共三篇HashMap的源码解读,先从基础知识了解HashMap底层最为重要的一些原理,最重要的就是哈希冲突了,并如何解决哈希冲突。接着了解了HashMap中最常见的一些方法,其中的重点和难点就是Node链表和TreeNode红黑树的学习理解了。最后是认识一些开发中最为常见的提升代码质量和效率的方法,并知道怎么处理HashMap的遍历和多线程可能带来的异常。
不管读者还是处于什么阶段,~的原理的使用迟早都是要面对的,希望大家看完这三篇源码解读能够有所收获吧。

本文深入探讨HashMap的高级方法,如computeIfAbsent、computeIfPresent和compute,以及扩展方法forEach,解析其工作原理与应用场景,帮助开发者提升代码质量和效率。
472

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



