. 前言

HashMap的容量大小会根据其存储数据的数量多少而自动扩充,即当HashMap存储数据的数量到达一个阈值(threshold)时,再往里面增加数据,便可能会扩充HashMap的容量。

可能?

事实上,由于JDK版本的不同,其阈值(threshold)的默认大小也变得不同(主要是计算公式的改变),甚至连判断条件也变得不一样,所以如果说threshold = capacity * loadFactor(容量 * 负载因子)将不再绝对正确,甚至说超过阈值容量就会增长也不再绝对正确,下面就以JDK1.6、1.7、1.8中的源码说明。

注:本文无图,标题仅是为了与前一篇文字标题符合

2. JDK 1.6

JDK 1.6中HashMap构造函数源码如下(其中以Mark开头注释以及中文注释,非JDK源码中注释,下同):

public HashMap(int initialCapacity, float loadFactor) {
// Mark A Begin
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); // Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
// Mark A End this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor); // 计算阈值,重点在这句代码
table = new Entry[capacity];
init();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

其中,代码片段A(Mark A Begin - Mark A End,下同)处可以先忽略,主要的是

threshold = (int)(capacity * loadFactor);
  • 1

这边是阈值的计算公式,其中capacity(容量) 的缺省值为16,loadFactor(负载因子)缺省值为0.75,那么

threshold = (int)(16 * 0.75) = 12
  • 1

再来看addEntry函数(put(K, V)方法最后通过此函数插入数据,具体参见【图解JDK源码】HashMap的基本原理与它的线程安全性):

void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold) // 判断是否扩充容量
resize(2 * table.length);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

可以看见,只要当前数量大于或等于阈值,便会扩充HashMap的容量为其当前容量的2倍。这是在JDK 1.6下的特性。

3. JDK 1.7

JDK1.7中HashMap构造函数源码如下:

public HashMap(int initialCapacity, float loadFactor) {
// Mark A Begin
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);
// Mark A End this.loadFactor = loadFactor;
threshold = initialCapacity; // 计算阈值,重点在这句代码
init();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

同样的,代码片段A可以先忽略,那么对么上面代码,可以看出,阈值的计算与JDK 1.6中完全不同,它与合约因子无关,而是直接使用了初始大小作为阈值的大小,但是这仅是针对第一次改变大小前,因为在resize函数(改变容量大小的函数,扩充容量便是调用此函数)中,有如下代码:

threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  • 1

也即是说,在改变一次大小后,threshold的值仍然跟负载因子相关,与JDK 1.6中的计算方式相差无几(未讨论容量到达最大值1,073,741,824 时的情况)。

而addEntry函数也与JDK 1.6中有所不同,其源码如下:

void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) { // 判断语句发生了改变
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
} createEntry(hash, key, value, bucketIndex);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

从上面的代码可以看出,在JDK 1.6中,判断是否扩充大小是直接判断当前数量是否大于或等于阈值,而JDK 1.7中可以看出,其判断是否要扩充大小除了判断当前数量是否大于等于阈值,同时也必须保证当前数据要插入的桶不能为空(桶的详细可参见【图解JDK源码】HashMap的基本原理与它的线程安全性)。那么JDK 1.8中又是如何呢?

3. JDK 1.8

说明:JDK 1.8对于HashMap的实现,新增了红黑树的特点,所以其底层实现原理变得不一样,再此不讨论。

JDK 1.8中HashMap构造函数源码如下:

public HashMap(int initialCapacity, float loadFactor) {
// Mark A Begin
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);
// Mark A End this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity); // 计算阈值
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这里使用到了tableSizeFor方法,其源码如下:

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

因为使用了位运算,所以这个方法可能不能明确的知道结果,但是只要知道不管输入什么值,它的最后结果都会是0,1,2,4,8,16,32,68… 这些数字中的一个就对了(其实是有规律的),对于以下输入值有:

tableSizeFor(16) = 16
tableSizeFor(32) = 32
tableSizeFor(48) = 64
tableSizeFor(64) = 64
tableSizeFor(80) = 128
tableSizeFor(96) = 128
tableSizeFor(112) = 128
tableSizeFor(128) = 128
tableSizeFor(144) = 256
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

也即是说,对于容量的初始值16来说,其初始阈值便是16,与JDK 1.7中初始阈值相同,而其resize函数中,threshold的计算源码如下:

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; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
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; // 代码太多,省略后面的代码
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

计算变得更复杂,因为其底层实现原理已经不仅仅是像之前的JDK中数组加链表那样简单,但是仍然可以看见其阈值的计算是与负载因子相关的。

而其判断是否要扩充的语句在putVal函数内(put方法会调用),其源码如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// 代码太多,省略
++modCount;
if (++size > threshold) // 判断是否扩充语句
resize();
afterNodeInsertion(evict);
return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

可以看见,其判断是否达到阈值与JDK 1.6是相同的,而没有像JDK 1.7中那样判断桶是否不为空。

总结

JDK 1.6 当数量大于容量 * 负载因子即会扩充容量。

JDK 1.7 初次扩充为:当数量大于容量时扩充;第二次及以后为:当数量大于容量 * 负载因子时扩充。

JDK 1.8 初次扩充为:与负载因子无关;第二次及以后为:与负载因子有关。其详细计算过程需要具体详解。

注:以上均未考虑最大容量时的情况。

HashMap的容量大小增长原理(JDK1.6/1.7/1.8)

HashMap的容量大小增长原理(JDK1.6/1.7/1.8)的更多相关文章

  1. 为什么 HashMap 的容量大小要设置为2的N次方?

    原文链接:https://www.changxuan.top/?p=1208 前两天,我在一位同学提交中看到了下面这样的一行代码,让我很是惊讶. Map<String, String> t ...

  2. Java中HashMap底层实现原理(JDK1.8)源码分析

    这几天学习了HashMap的底层实现,但是发现好几个版本的,代码不一,而且看了Android包的HashMap和JDK中的HashMap的也不是一样,原来他们没有指定JDK版本,很多文章都是旧版本JD ...

  3. jdk1.8 HashMap底层数据结构:深入解析为什么jdk1.8 HashMap的容量一定要是2的n次幂

    前言 1.本文根据jdk1.8源码来分析HashMap的容量取值问题: 2.本文有做 jdk1.8 HashMap.resize()扩容方法的源码解析:见下文“一.3.扩容:同样需要保证扩容后的容量是 ...

  4. HashMap实现原理(jdk1.7),源码分析

    HashMap实现原理(jdk1.7),源码分析 ​ HashMap是一个用来存储Key-Value键值对的集合,每一个键值对都是一个Entry对象,这些Entry被以某种方式分散在一个数组中,这个数 ...

  5. 为什么jdk1.8 HashMap的容量一定要是2的n次幂

    一.jdk1.8中,对“HashMap的容量一定要是2的n次幂”做了严格控制 1.默认初始容量: [Java] 纯文本查看 复制代码 ? 1 2 3 4 /**  * The default init ...

  6. 深度剖析HashMap的数据存储实现原理(看完必懂篇)

    深度剖析HashMap的数据存储实现原理(看完必懂篇) 具体的原理分析可以参考一下两篇文章,有透彻的分析! 参考资料: 1. https://www.jianshu.com/p/17177c12f84 ...

  7. 我说HashMap初始容量是16,面试官让我回去等通知

    众所周知HashMap是工作和面试中最常遇到的数据类型,但很多人对HashMap的知识止步于会用的程度,对它的底层实现原理一知半解,了解过很多HashMap的知识点,却都是散乱不成体系,今天一灯带你一 ...

  8. [转贴]从零开始学C++之STL(二):实现一个简单容器模板类Vec(模仿VC6.0 中 vector 的实现、vector 的容量capacity 增长问题)

    首先,vector 在VC 2008 中的实现比较复杂,虽然vector 的声明跟VC6.0 是一致的,如下:  C++ Code  1 2   template < class _Ty, cl ...

  9. ConcurrentHashMap底层实现原理(JDK1.8)源码分析

    ref:https://blog.csdn.net/xu768840497/article/details/79194701 http://www.cnblogs.com/leesf456/p/545 ...

随机推荐

  1. cron job error : c queue max run limit reached

    在cron job的日志中发现以下报错: ! c queue max run limit reached Wed Aug 28 12:56:00 2013 ! rescheduling a cron ...

  2. #关于 OneVsRestClassifier(LogisticRegression(太慢了,要用超过的机器)

    #关于 OneVsRestClassifier #注意以下代码中,有三个类 from sklearn import datasets X, y = datasets.make_classificati ...

  3. C++中cin.get(),cin.getline(),cin>>,gets(),cin.clear()使用总结

    1.cin.get()  实质:类istream所定义对象cin的重载成员函数 用于读取单字符  istream& get(char&)    int get(void) 用于读取字符 ...

  4. 为SSRS配置SMTP服务器身份验证

    此处设置外邮地址却无法填写邮箱密码 一.安装SMTP服务 1.在服务管理器中单击“功能” 2.单击“添加功能”打开“添加功能向导”对话框 3.在“选择功能”页上选择“SMTP服务器”并选择“添加必须的 ...

  5. JS继承方式详解

    js继承的概念 js里常用的如下两种继承方式: 原型链继承(对象间的继承) 类式继承(构造函数间的继承) 由于js不像java那样是真正面向对象的语言,js是基于对象的,它没有类的概念.所以,要想实现 ...

  6. loader的简单使用过程分析

    首先,fragment或者activity必须实现callback接口 必须实现的三个方法为 public Loader<Cursor> onCreateLoader(int id, Bu ...

  7. 【总结整理】display、visibility、overflow的隐藏问题

    display.visibility.overflow的隐藏问题 http://blog.sina.com.cn/s/blog_85e7c239010151r4.html   display:bloc ...

  8. cocos打包到ios与android上音频推荐

    首先贴一张官方对于ios与android上音频格式的推荐: 这里只给出了推荐格式,一般我们在实际运用中会使用如下方式: 一.IOS与安卓各一套:音乐:都使用MP3    音效:ios用caf Andr ...

  9. JAVA面向接口的编程思想与具体实现

    面向对象设计里有一点大家已基本形成共识,就是面向接口编程,我想大多数人对这个是没有什么觉得需要怀疑的.        问题是在实际的项目开发中我们是怎么体现的呢? 难道就是每一个实现都提供一个接口就了 ...

  10. c语言学习笔记 const变量

    在c语言的编程过程中经常会遇到有常数参加运算的运算,比如这种. int a=100*b; 这个100我们叫常数或者叫常量,但是程序中我们不推荐这种直接写常数的方法,有两个缺点. 第一是程序可读性差. ...