HashMap 的初始化
在 HashMap 中,提供了一个指定初始容量的构造方法 HashMap(int initialCapacity)
,这个方法最终会调用到 HashMap 另一个构造方法,其中的参数 loadFactor 就是默认值 0.75f。
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);
}
其中的成员变量 threshold
就是用来存储,触发 HashMap 扩容的阈值,也就是说,当 HashMap 存储的数据量达到 threshold
时,就会触发扩容。
从构造方法的逻辑可以看出,HashMap 并不是直接使用外部传递进来的 initialCapacity
,而是经过了 tableSizeFor()
方法的处理,再赋值到 threshole
上。
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;
}
在 tableSizeFor()
方法中,通过逐步位运算,就可以让返回值,保持在 2 的 N 次幂。以方便在扩容的时候,快速计算数据在扩容后的新表中的位置。
那么当我们从外部传递进来 1w 时,实际上经过 tableSizeFor()
方法处理之后,就会变成 2 的 14 次幂 16384,再算上负载因子 0.75f,实际在不触发扩容的前提下,可存储的数据容量是 12288(16384 * 0.75f)。
这种场景下,用来存放 1w 条数据,绰绰有余了,并不会触发我们猜想的扩容。
HashMap 的 table 初始化
当我们把初始容量,调整到 1000 时,情况又不一样了,具体情况具体分析。
再回到 HashMap 的构造方法,threshold
为扩容的阈值,在构造方法中由 tableSizeFor()
方法调整后直接赋值,所以在构造 HashMap 时,如果传递 1000,threshold
调整后的值确实是 1024,但 HashMap 并不直接使用它。
仔细想想就会知道,初始化时决定了 threshold
值,但其装载因子(loadFactor)并没有参与运算,那在后面具体逻辑的时候,HashMap 是如何处理的呢?
在 HashMap 中,所有的数据,都是通过成员变量 table 数组来存储的,在 JDK 1.7 和 1.8 中虽然 table 的类型有所不同,但是数组这种基本结构并没有变化。那么 table、threshold、loadFactor 三者之间的关系,就是:
table.size == threshold * loadFactor
那这个 table 是在什么时候初始化的呢?这就要说会到我们一直在回避的问题,HashMap 的扩容。
在 HashMap 中,动态扩容的逻辑在 resize()
方法中。这个方法不仅仅承担了 table 的扩容,它还承担了 table 的初始化。
当我们首次调用 HashMap 的 put()
方法存数据时,如果发现 table 为 null,则会调用 resize()
去初始化 table,具体逻辑在 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; // 调用 resize()
// …
}
在 resize()
方法中,调整了最终 threshold
值,以及完成了 table 的初始化。
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;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
else if (oldThr > 0)
newCap = oldThr; // ①
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
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; // ④
// …
}
注意看代码中的注释标记。
因为 resize()
还糅合了动态扩容的逻辑,所以我将初始化 table 的逻辑用注释标记出来了。其中 xxxCap 和 xxxThr 分别对应了 table 的容量和动态扩容的阈值,所以存在旧和新两组数据。
当我们指定了初始容量,且 table 未被初始化时,oldThr 就不为 0,则会走到代码 ① 的逻辑。在其中将 newCap
赋值为 oldThr
,也就是新创建的 table
会是我们构造的 HashMap 时指定的容量值。
之后会进入代码 ② 的逻辑,其中就通过装载因子(loadFactor)调整了新的阈值(newThr),当然这里也做了一些限制需要让 newThr
在一个合法的范围内。
在代码 ③ 中,将使用 loadFactor
调整后的阈值,重新保存到 threshold
中。并通过 newCap
创建新的数组,将其指定到 table 上,完成 table 的初始化(代码 ④)。
到这里也就清楚了,虽然我们在初始化时,传递进来的 initialCapacity
虽然经过 tableSizeFor()
方法调整后,直接赋值给 threshold
,但是它实际是 table 的尺寸,并且最终会通过 loadFactor
重新调整 threshold
。
那么回到之前的问题就有答案了,虽然 HashMap 初始容量指定为 1000,会被 tableSizeFor()
调整为 1024,但是它只是表示 table 数组为 1024,扩容的重要依据扩容阈值会在 resize()
中调整为 768(1024 * 0.75)。
它是不足以承载 1000 条数据的,最终在存够 1k 条数据之前,还会触发一次动态扩容。
通常在初始化 HashMap 时,初始容量都是根据业务来的,而不会是一个固定值,为此我们需要有一个特殊处理的方式,就是将预期的初始容量,再除以 HashMap 的装载因子,默认时就是除以 0.75。
结语
看到这篇文章的人不知道有多少是和我一样的Android程序员。
35岁,这是我们这个行业普遍的失业高发阶段,这种情况下如果还不提升自己的技能,进阶发展,我想,很可能就是本行业的职业生涯的终点了。
我们要有危机意识,切莫等到一切都成定局时才开始追悔莫及。只要有规划的,有系统地学习,进阶提升自己并不难,给自己多充一点电,你才能走的更远。
千里之行始于足下。这是上小学时,那种一元钱一个的日记本上每一页下面都印刷有的一句话,当时只觉得这句话很短,后来渐渐长大才慢慢明白这句话的真正的含义。
有了学习的想法就赶快行动起来吧,不要被其他的事情牵绊住了前行的脚步。不要等到裁员时才开始担忧,不要等到面试前一晚才开始紧张,不要等到35岁甚至更晚才开始想起来要学习要进阶。
给大家一份系统的Android学习进阶资料,希望这份资料可以给大家提供帮助。
了前行的脚步。不要等到裁员时才开始担忧,不要等到面试前一晚才开始紧张,不要等到35岁甚至更晚才开始想起来要学习要进阶。
给大家一份系统的Android学习进阶资料,希望这份资料可以给大家提供帮助。
[外链图片转存中…(img-kjZ5R0fL-1723784743942)]