HashMap 源码解析二、put 相关函数

本文详细解析了JavaHashMap的putVal函数实现,包括链表插入、红黑树处理、哈希冲突避免和resize操作,重点介绍了putVal函数的工作原理和第一次调用的流程。
摘要由CSDN通过智能技术生成

putVal(hash(key), key, value, true, true),
后面一个应该还有印象,跟参数为Map构造函数调用的是同一个函数putMapEntries()。 接着我们就看下putVal(hash(key), key, value, true, true) 这个函数

putVal(hash(key), key, value, true, true) 函数

transient Node<K,V>[] table;

/**

  • 在put 相关方法中被调用
  • @param hash hash for key
  • @param key the key
  • @param value the value to put
  • @param onlyIfAbsent if true, don’t change existing value
  • @param evict if false, the table is in creation mode.
  • @return previous value, or null if none
    */
    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)//注释1
    n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null) //注释2
    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;//注释3
    if (++size > threshold)
    resize();
    afterNodeInsertion(evict);
    return null;
    }
第一次调用时

假设我们是通过HashMap()这个构造函数创建的HashMap 对象并第一次调用 put(K key, V value) 函数。

  1. 我们先看下Node 类的结构

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//省略代码。。。
}

可以看出这是一个单链表结构,存放着 hash、key和value

  1. resize() 函数,作用:初始化或者扩容表为原大小的2倍。源码后面再分析。
  2. 知道以上的信息我们在看 putVal() 函数的代码,注释1

if ((tab = table) == null || (n = tab.length) == 0)//注释1
n = (tab = resize()).length;

我们知道DEFAULT_INITIAL_CAPACITY = 1 << 4 // aka 16 因此可以知道
tab = (Node<K,V>[])new Node[16]n = 16

  1. 接着往下看,注释2

if ((p = tab[i = (n - 1) & hash]) == null) //注释2
tab[i] = newNode(hash, key, value, null);

我们是第一次调用,p = tab[i = (n - 1) & hash]肯定是null ,于是我们这次就成功的把key,value 存到了tab[i] 中。

  1. 我们走进了if,else 的代码就不用看了,直接到了//注释3 的位置 ++modCount 用于记录修改的次数,接着往下看:

if (++size > threshold)
resize();

threshold为扩容阈值,初始化时为 DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR //aka 16*0.75 = 12, size 为HashMap 中保存 Node的数量,当等于 扩容阈值 时就需要对 tab 进行扩容。
接着往下是 afterNodeInsertion(evict);这是一个空方法,什么都没做。

void afterNodeInsertion(boolean evict) { }

好了,我们第一次调用就结束了。成功的把数据存储到了HashMap 中,再回头看下我们之前没有看的 else 中的情况

else 中的情况

tab[i = (n - 1) & hash]中已经有值的情况就会走到 else 中,看代码:

static final int TREEIFY_THRESHOLD = 8;

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 //注释4
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}

  1. TreeNode 为红黑树,有兴趣的同学可以自行了解 红黑树深入剖析及Java实现

  2. 我们看接下来的判断,可以分为3中情况

  • p.key 与 参数key 相同

直接将p 赋值给 Node e

  • p 为 TreeNode

将需要保存的内容 添加到红黑树p 中,返回值赋给e

  • else

遍里链表p,且结果赋值给e; 也可分为3 中情况
1). if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) 当链表中有Node 的key 与参数key 相同时,结束遍历。
2). if ((e = p.next) == null) 链表遍历完时
将需要保存的内容 添加到链表末尾。如果链表长度小于8,结束遍历
3). 链表遍历完,且添加新增内容。链表长度大于等于8 时。
执行 treeifyBin(tab, hash) 函数,然后结束遍历。

static final int MIN_TREEIFY_CAPACITY = 64;
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)
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(tab, hash) 函数的逻辑是当tab 长度小于64 时就执行resize() 扩容,否则将链表转为红黑树

  1. 我们会过来看注释4 处代码

if (e != null) { //当参数key 在tab 中有映射时
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}

onlyIfAbsent 为true 时不修改现有值
当参数key 在tab 中有映射时,根据条件覆盖现有值,并返回旧值。

int hash(Object key) 函数

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

  1. ^ 如果相对应位值相同,则结果为0,否则为1, 如果相对应位都是1,则结果为1,否则为0

a = 1010
b = 0011

a ^ b = 1001
a & b = 0010

  1. (h = key.hashCode()) ^ (h >>> 16) 这个方法最重要的一句代码。为什么要做 ^ (h >>> 16)这个操作呢?

是因为在putVal 函数中,是这样是使用的 tab[index = (n - 1) & hash],n 是表的长度,表的长度永远都是2的幂次方,那么n-1的高位应该全是0,做 & 操作时会导致hash 的高位无法参与运算,从而会带来哈希冲突的风险。所以在计算key的哈希值的时候,做(h = key.hashCode()) ^ (h >>> 16)操作。这也就让高位参与到tab[index = (n - 1) & hash]的计算中来了,即降低了哈希冲突的风险又不会带来太大的性能问题。

resize() 函数

final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {//table中已经有数据
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

前因为秋招收集的二十套一二线互联网公司Android面试真题** (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-kjT5fJG6-1712654576020)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值