求面试别再问我HashMap原理了——史上最全源码解读,别再说你不知道HashMap 原理!

}

public HashMap(int initialCapacity) {

this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

public HashMap(Map<? extends K, ? extends V> m) {

this.loadFactor = DEFAULT_LOAD_FACTOR;

putMapEntries(m, false);

}

public HashMap(int initialCapacity, float loadFactor) {

if (initialCapacity < 0)

throw new IllegalArgumentException("Illegal initial capacity: " +

initialCapacity);

if (initialCapacity > MAXIMUM_CAPACITY)

initialCapacity = MAXIMUM_CAPACITY;

if (loadFactor <= 0 || Float.isNaN(loadFactor))

throw new IllegalArgumentException("Illegal load factor: " +

loadFactor);

this.loadFactor = loadFactor;

this.threshold = tableSizeFor(initialCapacity);

}

可以看到重载了4个构造方法,我们大多数基本用的就是第一个无参方法,其他的几个方法也是做一些初始化操作,主要关心这几个变量:

| 名称 | 用途 |

| — | — |

| initialCapacity | HashMap 初始容量 |

| loadFactor | 负载因子 |

| threshold | 当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容 |

HashMap 初始容量是16,负载因子为 0.75,但是有的朋友会细心发现,第一个构造方法,摆明就只是赋值了负载因子,初始容量和阈值都没有被初始化,这里先不解释,后面扩容机制会告诉你答案,然后看最后一个构造函数,我们可以把初始容量和负载因子作为值传递进来,threshold是通过一个方法计算出来的,看看方法具体实现:

/**

  • Returns a power of two size for the given target capacity.

*/

static final int tableSizeFor(int cap) {

int n = cap - 1;

n |= n >>> 1;

n |= n >>> 2;

n |= n >>> 4;

n |= n >>> 8;

n |= n >>> 16;

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}

相信大家和我一样,第一次看到这个方法是蒙蔽的…先把结论给出来:找到大于或等于 cap 的最小2的幂,这里引用一张图解释下,侵删:

比如cap等于5,那么最终返回的就是8,如果cap等于10,返回的就是16,这样一说大家结合上面的应该能理解了。

2.1 插入

插入逻辑算是比较复杂的了,我们先来看看put方法代码:

public V put(K key, V value) {

return putVal(hash(key), key, value, false, true);

}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

boolean evict) {

Node<K,V>[] tab; Node<K,V> p; int n, i;

//初始化数组table

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

//通过hash算法找到下标,如果对应的位置为空,直接将数据放进去

if ((p = tab[i = (n - 1) & hash]) == null)

tab[i] = newNode(hash, key, value, null);

else {

//对应的位置不为空,hash冲突

Node<K,V> e; K k;

//判断插入的key如果等于当前位置的key的话,先将 e 指向该键值对,后续覆盖

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

e = p;

//如果桶中的引用类型为 TreeNode,则调用红黑树的插入方法

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)

treeifyBin(tab, hash);

break;

}

//如果链表中包含该节点,赋值,后续覆盖,跳出循环

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

break;

p = e;

}

}

//判断插入的是否存在HashMap中,上面e被赋值,不为空,则说明存在,更新旧的键值对

if (e != null) { // existing mapping for key

V oldValue = e.value;

if (!onlyIfAbsent || oldValue == null)

e.value = value;

afterNodeAccess(e);

return oldValue;

}

}

++modCount;

//当前HashMap键值对超过阈值时,进行扩容

if (++size > threshold)

resize();

afterNodeInsertion(evict);

return null;

}

可以看到主要逻辑在putVal()方法中,不清楚的可以看下注释,总结一下主要是几个方面:

  • 如果当前table为空,先进行初始化

  • 查找插入的键值对是否存在,存在的话,先进行赋值,后续将更新旧的键值对

  • 不存在,插入链表尾部,如果链表长度大于一个阈值,进行链表转化树的操作

  • 如果size大于一个阈值,进行扩容

那么重点当然就是扩容方法了,看看具体实现:

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) {

//超过最大值,不再扩容,直接返回

if (oldCap >= MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return oldTab;

}

//通过位运算,计算出新的容量以及新的阈值,2倍计算

else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

oldCap >= DEFAULT_INITIAL_CAPACITY)

newThr = oldThr << 1; // double threshold

}

//使用 threshold 变量暂时保存 initialCapacity 参数的值

else if (oldThr > 0)

newCap = oldThr;

else {

//这里就能回答上面的初始化的问题了,调用空的构造函数时的赋值

newCap = DEFAULT_INITIAL_CAPACITY;

newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

}

// newThr 为 0 时,按阈值计算公式进行计算,容量*负载因子

if (newThr == 0) {

float ft = (float)newCap * loadFactor;

newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

(int)ft : Integer.MAX_VALUE);

}

//更新当前最新的阈值

threshold = newThr;

//创建新的桶数组,调用空的构造方法,这里也就是桶数组的初始化

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

table = newTab;

//如果旧的数组不为空,遍历,将值移植到新的数组中去

if (oldTab != null) {

for (int j = 0; j < oldCap; ++j) {

Node<K,V> e;

if ((e = oldTab[j]) != null) {

//将旧数组对象置位空,方便回收

oldTab[j] = null;

//计算新的位置,赋值操作

if (e.next == null)

newTab[e.hash & (newCap - 1)] = e;

else if (e instanceof TreeNode)

//如果原来节点是红黑树,则需要重新进行拆分

((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

else {

//遍历整个链表,重新hash,根据新的下标重新分组

Node<K,V> loHead = null, loTail = null;

Node<K,V> hiHead = null, hiTail = null;

Node<K,V> next;

do {

next = e.next;

if ((e.hash & oldCap) == 0) {

if (loTail == null)

loHead = e;

else

loTail.next = e;

loTail = e;

}

else {

if (hiTail == null)

hiHead = e;

else

hiTail.next = e;

hiTail = e;

}

} while ((e = next) != null);

if (loTail != null) {

loTail.next = null;

newTab[j] = loHead;

}

if (hiTail != null) {

hiTail.next = null;

newTab[j + oldCap] = hiHead;

}

}

}

}

}

return newTab;

}

代码稍微长了点,大家耐心点看下逻辑,总结也就几点

  • 判断当前oldTab长度是否为空,如果为空,则进行初始化桶数组,也就回答了空构造函数初始化为什么没有对容量和阈值进行辅助,如果不为空,则进行位运算,左移一位,2倍运算。

  • 扩容,创建一个新容量的数组,遍历旧的数组:

  • 如果节点为空,直接赋值插入

  • 如果节点为红黑树,则需要进行进行拆分操作

  • 如果为链表,根据hash算法进行重新计算下标,将链表进行拆分分组

这里主要说明下链表拆分是什么意思,我们知道下标计算是hash&(n-1),假如原始数组长度为16,进行求余计算:那么n-1也就是15,对应二进制 0000 1111,这时候分别有2个hash值分别为:1101 1100和1110 1100,计算可以得到,得到的下标都是0000 1100,也就是12,如果进行扩容之后呢?长度变成32,n-1也就对应 0001 1111,2个hash再次进行计算得到的就是 0001 1100 和 0000 1100,一个下标还是12,而另一个则是28了

可以看到扩容后,参与模运算的位数由4位变为了5位,所以对应得出来的值自然就不一样了,相信大家也应该理解了

2.2 查找

相对于复杂的插入操作,查找的逻辑相对就相对简单点了,代码如下:

public V get(Object key) {

Node<K,V> e;

return (e = getNode(hash(key), key)) == null ? null : e.value;

}

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) {

//定位下标,如果第一个节点是所要查找的值,直接返回

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类型,去遍历红黑树

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;

}

上面也提到了,通过(n - 1) & hash 即可算出在数组中的位置,这里简单解释一下。HashMap 中桶数组的大小 length 总是2的幂,此时,(n - 1) & hash 等价于对 length 取余。但取余的计算效率没有位运算高,所以(n - 1) & hash也是一个小的优化

还有一个计算hash值得方法

static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

可以看到,这里的hash并不是用原有对象的hashcode最为最终的hash值,而是做了一定位运行,具体原因个人想法如下:

因为如果(n-1)的值太小的话(n - 1) & hash的值就完全依靠hash的低位值,比如n-1为0000 1111,那么最终的值就完全依赖于hash值的低4位了,这样的话hash的高位就玩完全失去了作用,h ^ (h >>> 16),通过这种方式,让高位数据与低位数据进行异或,也是变相的加大了hash的随机性,这样就不单纯的依赖对象的hashcode方法了。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

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

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

img

img

img

img

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

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

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

尾声

开发是需要一定的基础的,我是08年开始进入Android这行的,在这期间经历了Android的鼎盛时期,和所谓的Android”凉了“。中间当然也有着,不可说的心酸,看着身边朋友,同事一个个转前端,换行业,其实当时我的心也有过犹豫,但是我还是坚持下来了,这次的疫情就是一个好的机会,大浪淘沙,优胜劣汰。再等等,说不定下一个黄金浪潮就被你等到了。

  • 330页 PDF Android核心笔记

  • 几十套阿里 、字节跳动、腾讯、华为、美团等公司2020年的面试题

  • PDF和思维脑图,包含知识脉络 + 诸多细节

  • Android进阶系统学习视频

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

oid”凉了“。中间当然也有着,不可说的心酸,看着身边朋友,同事一个个转前端,换行业,其实当时我的心也有过犹豫,但是我还是坚持下来了,这次的疫情就是一个好的机会,大浪淘沙,优胜劣汰。再等等,说不定下一个黄金浪潮就被你等到了。

  • 330页 PDF Android核心笔记

[外链图片转存中…(img-YrPmGcno-1713675930768)]

  • 几十套阿里 、字节跳动、腾讯、华为、美团等公司2020年的面试题

[外链图片转存中…(img-86V9Bi0q-1713675930770)]

[外链图片转存中…(img-t5mLAVxx-1713675930772)]

  • PDF和思维脑图,包含知识脉络 + 诸多细节

[外链图片转存中…(img-0EOzoBYw-1713675930772)]

  • Android进阶系统学习视频

[外链图片转存中…(img-L6ssjAHZ-1713675930773)]

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值