hashmapvalue为数组初试数组大小为什么一定要是2 的倍数

HashMap的默认容量为何为16?为何是2的整数倍?
static int indexFor(int h, int length) {
return h & (length-1); }我们知道对于HashMap的table而言,数据分布需要均匀(最好每项都只有一个元素,这样就可以直接找到),不能太紧也不能太松,太紧会导致查询速度慢,太松则浪费空间。计算hash值后,怎么才能保证table元素分布均与呢?我们会想到取模,但是由于取模的消耗较大,HashMap是这样处理的:调用indexFor方法。HashMap源码中有一个indexFor方法,返回的是key的hashcode跟初始容量-1做与运算。首先length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;其次,length为2的整数次幂的话,为偶数。这样length-1为奇数,奇数的最后一位为1,这样便保证了h&(length-1)的最后一位为0,也可能为1(这取决于h的值),即与后的结果可能为偶数也可能是奇数。这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间。所以,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。
HashMap的默认长度为什么是16?
什么是 哈希表
HashMap 中数组的 size 为什么必须是 2 的整数次幂
2、HashMap的扩容是怎样扩容的,为什么是2的N次幂的大小?
HashMap的容量与扩容
Hashmap为什么容量是2的幂次,什么是负载因子
hashmap初试数组大小为什么一定要是2 的倍数
Hashmap的容量为什么是2的幂次
hashMap为啥初始化容量为2的次幂
Java HashMap的扩容
到底什么是hash呢?hash碰撞?为什么HashMap的初始容量是16?
没有更多推荐了,&nbsp&#8250&nbsp&nbsp&#8250&nbsp
图解HashMap(一)
HashMap是日常开发中经常会用到的一种数据结构,在介绍HashMap的时候会涉及到很多术语,比如时间复杂度O、散列(也叫哈希)、散列算法等,这些在大学课程里都有教过,但是由于某种不可抗力又还给老师了,在深入学习HashMap之前先了解HashMap设计的思路以及以及一些重要概念,在后续分析源码的时候就能够有比较清晰的认识。
HashMap是什么
在回答这个问题之前先看个例子:小明打算从A市搬家到B市,拿了两个箱子把自己的物品打包就出发了。
到了B市之后,他想拿手机给家里报个平安,这时候问题来了,东西多了他忘记手机放在哪个箱子了?小明开始打开1号箱子找手机,没找到;再打开2号箱子找,找到手机。当只有2个箱子的时候,东西又不多的情况下,他可能花个2分钟就找到手机了,假如有20个箱子,每个箱子的东西又多又杂,那么花的时间就多了。小明总结了下查找耗时的原因,发现是因为这些东西放的没有规律,如果他把每个箱子分个类别,比如定一个箱子专门放手机、电脑等电子设备,有专门放衣服的箱子等等,那么他找东西花的时间就可以大大缩短了。
其实HashMap也是用到这种思路,HashMap作为一种数据结构,像数组和链表一样用于常规的增删改查,在存数据的时候(put)并不是随便乱放,而是会先做一次类似“分类”的操作再存储,一旦“分类”存储之后,下次取(get)的时候就可以大大缩短查找的时间。我们知道数组在执行查、改的效率很高,而增、删(不是尾部)的效率低,链表相反,HashMap则是把这两者结合起来,看下HashMap的数据结构
从上面的结构可以看出,通常情况下HashMap是以数组和链表的组合构成(Java8中将链表长度超过8的链表转化成红黑树)。结合上面找手机的例子,我们简单分析下HashMap存取操作的心路历程。put存一个键值对的时候(比如存上图盖伦),先根据键值&分类&,&分类&一顿操作后告诉我们,盖伦应该属于14号坑,直接定位到14号坑。接下来有几种情况:
14号坑没人,nice,直接存值;
14号有人,也叫盖伦,替换原来的攻击值;
14号有人,叫老王!插队到老王前面去(单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置)
get取的时候也需要传键值,根据传的键值来确定要找的是哪个&类别&,比如找火男,&分类&一顿操作够告诉我们火男属于2号坑,于是我们直接定位到2号坑开始找,亚索不是…找到火男。
HashMap是由数组和链表组合构成的数据结构,Java8中链表长度超过8时会把长度超过8的链表转化成红黑树;存取时都会根据键值计算出&类别&(hashCode),再根据&类别&定位到数组中的位置并执行操作。
HashCode是什么
还是举个栗子:一个工厂有500号人,下图用两种方案来存储厂里员工的信件。
左右各有27个信箱,左边保安大哥存信的时候不做处理,想放哪个信箱就放哪个信箱,当员工去找信的时候,只好挨个信箱找,再挨个比对信箱里信封的名字,万一哥们脸黑,要找的放在最后一个信箱的最底下,悲剧…所以这种情况的时间复杂度为O(N);右边采用HashCode的方式将27个信箱分类,分类的规则是名字首字母(第一个箱子放不写名字的哥们),保安大哥将符合对应姓名的信件放在对应的信箱里,这样员工就不用挨个找了,只需要比对一个信箱里的信件即可,大大提高了效率,这种情况的时间复杂度趋于一个常数O(1)。
例子中右图其实就是hashCode的一个实现,每个员工都有自己的hashCode,比如李四的hashCode是L,王五的hashCode是W(这取决于你的hash算法怎么写),然后我们根据确定的hashCode值把信箱分类,hashCode匹配则存在对应信箱。在Java的Object中可以调用hashCode()方法获取对象hashCode,返回一个int值。那么会出现两个对象的hashCode一样吗?答案是会的,就像上上个例子中盖伦和老王的hashCode就一样,这种情况网上有人称之为&hash碰撞&,出现这种所谓&碰撞&的处理上面已经介绍了解决思路,具体源码后续介绍。
hashCode是一个对象的标识,Java中对象的hashCode是一个int类型值。通过hashCode来指定数组的索引可以快速定位到要找的对象在数组中的位置,之后再遍历链表找到对应值,理想情况下时间复杂度为O(1),并且不同对象可以拥有相同的hashCode。
HashMap的时间复杂度
通过上面信箱找信的例子来讨论下HashMap的时间复杂度,在使用hashCode之后可以直接定位到一个箱子,时间的耗费主要是在遍历链表上,理想的情况下(hash算法写得很完美),链表只有一个节点,就是我们要的
那么此时的时间复杂度为O(1),那不理想的情况下(hash算法写得很糟糕),比如上面信箱的例子,假设hash算法计算每个员工都返回同样的hashCode
所有的信都放在一个箱子里,此时要找信就要依次遍历C信箱里的信,时间复杂度不再是O(1),而是O(N),因此HashMap的时间复杂度取决于算法的实现上,当然HashMap内部的机制并不像信箱这么简单,在HashMap内部会涉及到扩容、Java8中会将长度超过8的链表转化成红黑树,这些都在后续介绍。
HashMap的时间复杂度取决于hash算法,优秀的hash算法可以让时间复杂度趋于常数O(1),糟糕的hash算法可以让时间复杂度趋于O(N)。
负载因子是什么
我们知道HashMap中数组长度是16(什么?你说不知道,看下源码你就知道),假设我们用的是最优秀的hash算法,即保证我每次往HashMap里存键值对的时候,都不会重复,当hashmap里有16个键值对的时候,要找到指定的某一个,只需要1次;
之后继续往里面存值,必然会发生所谓的&hash碰撞&形成链表,当hashmap里有32个键值对时,找到指定的某一个最坏情况要2次;当hashmap里有128个键值对时,找到指定的某一个最坏情况要8次
随着hashmap里的键值对越来越多,在数组数量不变的情况下,查找的效率会越来越低。那怎么解决这个问题呢?只要增加数组的数量就行了,键值对超过16,相应的就要把数组的数量增加(HashMap内部是原来的数组长度乘以2),这就是网上所谓的扩容,就算你有128个键值对,我们准备了128个坑,还是能保证&一个萝卜一个坑&。
其实扩容并没有那么风光,就像ArrayList一样,扩容是件很麻烦的事情,要创建一个新的数组,然后把原来数组里的键值对&放&到新的数组里,这里的&放&不像ArrayList那样用原来的index,而是根据新表的长度重新计算hashCode,来保证在新表的位置,老麻烦了,所以同一个键值对在旧数组里的索引和新数组中的索引通常是不一致的(火男:&我以前是3号,怎么现在成了127号,给我个完美的解释!&新表:&大清亡了,现在你得听我的&)。另外,我们也可以看出这是典型的以空间换时间的操作。
说了这么多,那负载因子是个什么东西?负载因子其实就是规定什么时候扩容。上面我们说默认hashmap数组大小为16,存的键值对数量超过16则进行扩容,好像没什么毛病。然而HashMap中并不是等数组满了(达到16)才扩容,它会存在一个阀值(threshold),只要hashmap里的键值对大于等于这个阀值,那么就要进行扩容。阀值的计算公式:
阀值 = 当前数组长度✖负载因子
hashmap中默认负载因子为0.75,默认情况下第一次扩容判断阀值是16 &#1 = 12;所以第一次存键值对的时候,在存到第13个键值对时就需要扩容了;或者另外一种理解思路:假设当前存到第12个键值对:12 / 16 = 0.75,13 / 16 = 0.8125(大于0.75需要扩容) 。肯定会有人有疑问,我要这铁棒有何用?不,我要这负载因子有何用?直接规定超过数组长度再扩容不就行了,还省得每次扩容之后还要重新计算新的阀值,Google说取0.75是一个比较好的权衡,当然我们可以自己修改,HashMap初识化时可以指定数组大小和负载因子,你完全可以改成1。
public HashMap(int initialCapacity, float loadFactor)
我的理解是这负载因子就像人的饭量,有的人吃要7分饱,有的人要10分饱,稳妥起见默认让我们7.5分饱。
在数组大小不变的情况下,存放键值对越多,查找的时间效率会降低,扩容可以解决该问题,而负载因子决定了什么时候扩容,负载因子是已存键值对的数量和总的数组长度的比值。默认情况下负载因子为0.75,我们可在初始化HashMap的时候自己修改。
hash与Rehash
hash和rehash的概念其实上面已经分析过了,每次扩容后,转移旧表键值对到新表之前都要重新rehash,计算键值对在新表的索引。如下图火男这个键值对被存进hashmap到后面扩容,会经过hash和rehash的过程
第一次hash可以理解成'&分类&',方便后续取、改等操作可以快速定位到具体的&坑&。那么为什么要进行rehash,按照之前元素在数组中的索引直接赋值,例如火男之前3号坑,现在跑到30号坑。
个人理解是,在未扩容前,可以看到如13号链的长度是3,为了保证我们每次查找的时间复杂度O趋于O(1),理想的情况是&一个萝卜一个坑&,那么现在&坑&多了,原来&3个萝卜一个坑&的情况现在就能有效的避免了。
Java7源码分析
先看下Java7里的HashMap实现,有了上面的分析,现在在源码中找具体的实现。
//HashMap里的数组
transient Entry&K,V&[] table = (Entry&K,V&[]) EMPTY_TABLE;
//Entry对象,存key、value、hash值以及下一个节点
static class Entry&K,V& implements Map.Entry&K,V& {
Entry&K,V&
//默认数组大小,二进制1左移4位为16
static final int DEFAULT_INITIAL_CAPACITY = 1 && 4;
//负载因子默认值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当前存的键值对数量
//阀值 = 数组大小 * 负载因子
//负载因子变量
final float loadF
//默认new HashMap数组大小16,负载因子0.75
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
//可以指定数组大小和负载因子
public HashMap(int initialCapacity, float loadFactor) {
//省略一些逻辑判断
this.loadFactor = loadF
threshold = initialC
以上就是HashMap的一些先决条件,接着看平时put操作的代码实现,put的时候会遇到3种情况上面已分析过,看下Java7代码:
public V put(K key, V value) {
//数组为空时创建数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
//key为空单独对待
if (key == null)
return putForNullKey(value);
//①根据key计算hash值
int hash = hash(key);
//②根据hash值和当前数组的长度计算在数组中的索引
int i = indexFor(hash, table.length);
//遍历整条链表
for (Entry&K,V& e = table[i]; e != e = e.next) {
//③情况1.hash值和key值都相同的情况,替换之前的值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.
e.recordAccess(this);
//返回被替换的值
return oldV
modCount++;
//③情况2.坑位没人,直接存值或发生hash碰撞都走这
addEntry(hash, key, value, i);
先看上面key为空的情况(上面画图的时候总要在第一格留个空key的键值对),执行 putForNullKey() 方法单独处理,会把该键值对放在index0,所以HashMap中是允许key为空的情况。再看下主流程:
步骤①.根据键值算出hash值 ― & hash(key)
步骤②.根据hash值和当前数组的长度计算在数组中的索引 ― & indexFor(hash, table.length)
static int indexFor(int h, int length) {
//hash值和数组长度-1按位与操作,听着费劲?其实相当于h%取余数(取模运算)
//如:h = 17,length = 16;那么算出就是1
//&运算的效率比%要高
return h & (length-1);
步骤③情况1.hash值和key值都相同,替换原来的值,并将被替换的值返回。
步骤③情况2.坑位没人或发生hash碰撞 ― & addEntry(hash, key, value, i)
void addEntry(int hash, K key, V value, int bucketIndex) {
//当前hashmap中的键值对数量超过阀值
if ((size &= threshold) && (null != table[bucketIndex])) {
//扩容为原来的2倍
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
//计算在新表中的索引
bucketIndex = indexFor(hash, table.length);
//创建节点
createEntry(hash, key, value, bucketIndex);
如果put的时候超过阀值,会调用 resize() 方法将数组大小扩大为原来的2倍,并且根据新表的长度计算在新表中的索引(如之前17%16 =1,现在17%32=17),看下resize方法
void resize(int newCapacity) { //传入新的容量
//获取旧数组的引用
Entry[] oldTable =
int oldCapacity = oldTable.
//极端情况,当前键值对数量已经达到最大
if (oldCapacity == MAXIMUM_CAPACITY) {
//修改阀值为最大直接返回
threshold = Integer.MAX_VALUE;
//步骤①根据容量创建新的数组
Entry[] newTable = new Entry[newCapacity];
//步骤②将键值对转移到新的数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//步骤③将新数组的引用赋给table
table = newT
//步骤④修改阀值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
上面的重点是步骤②,看下它具体的转移操作
void transfer(Entry[] newTable, boolean rehash) {
//获取新数组的长度
int newCapacity = newTable.
//遍历旧数组中的键值对
for (Entry&K,V& e : table) {
while(null != e) {
Entry&K,V& next = e.
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
//计算在新表中的索引,并到新数组中
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] =
这段for循环的遍历会使得转移前后键值对的顺序颠倒(Java7和Java8的区别),画个图就清楚了,假设石头的key值为5,盖伦的key值为37,这样扩容前后两者还是在5号坑。第一次:
最后再看下创建节点的方法
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry&K,V& e = table[bucketIndex];
table[bucketIndex] = new Entry&&(hash, key, value, e);
创建节点时,如果找到的这个坑里面没有存值,那么直接把值存进去就行了,然后size++;如果是碰撞的情况,
前面说的以单链表头插入的方式就是这样(盖伦:”老王已被我一脚踢开!“),总结一下Java7 put流程图
相比put,get操作就没这么多套路,只需要根据key值计算hash值,和数组长度取模,然后就可以找到在数组中的位置(key为空同样单独操作),接着就是遍历链表,源码很少就不分析了。
Java8源码分析
基本思路是一样的
//定义长度超过8的链表转化成红黑树
static final int TREEIFY_THRESHOLD = 8;
//换了个马甲还是认识你!!!
static class Node&K,V& implements Map.Entry&K,V& {
看下Java8 put的源码
public V put(K key, V value) {
//根据key计算hash值
return putVal(hash(key), key, value, false, true);
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node&K,V&[] Node&K,V& int n,
//步骤1.数组为空或数组长度为0,则扩容(咦,看到不一样咯)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).
//步骤2.根据hash值和数组长度计算在数组中的位置
//如果"坑"里没人,直接创建Node并存值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
Node&K,V& K
//步骤3."坑"里有人,且hash值和key值都相等,先获取引用,后面会用来替换值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//步骤4.该链是红黑树
else if (p instanceof TreeNode)
e = ((TreeNode&K,V&)p).putTreeVal(this, tab, hash, key, value);
//步骤5.该链是链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//步骤5.1注意这个地方跟Java7不一样,是插在链表尾部!!!
p.next = newNode(hash, key, value, null);
//链表长度超过8,转化成红黑树
if (binCount &= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
//步骤5.2链表中已存在且hash值和key值都相等,先获取引用,后面用来替换值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
if (e != null) { // existing mapping for key
V oldValue = e.
if (!onlyIfAbsent || oldValue == null)
//统一替换原来的值
afterNodeAccess(e);
//返回原来的值
return oldV
//步骤6.键值对数量超过阀值,扩容
if (++size & threshold)
afterNodeInsertion(evict);
通过上面注释分析,对比和Java7的区别,Java8一视同仁,管你key为不为空的统一处理,多了一步链表长度的判断以及转红黑树的操作,并且比较重要的一点,新增Node是插在尾部而不是头部!!!。当然上面的主角还是扩容resize操作
final Node&K,V&[] resize() {
//旧数组的引用
Node&K,V&[] oldTab =
//旧数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.
//旧数组阀值
int oldThr =
//新数组长度、新阀值
int newCap, newThr = 0;
if (oldCap & 0) {
//极端情况,旧数组爆满了
if (oldCap &= MAXIMUM_CAPACITY) {
//阀值改成最大,放弃治疗直接返回旧数组
threshold = Integer.MAX_VALUE;
return oldT
//扩容咯,这里采用左移运算左移1位,也就是旧数组*2
else if ((newCap = oldCap && 1) & MAXIMUM_CAPACITY &&
oldCap &= DEFAULT_INITIAL_CAPACITY)
//同样新阀值也是旧阀值*2
newThr = oldThr && 1; // double threshold
else if (oldThr & 0) // initial capacity was placed in threshold
newCap = oldT
//初始化在这里
// 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 * loadF
newThr = (newCap & MAXIMUM_CAPACITY && ft & (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
//更新阀值
threshold = newT
@SuppressWarnings({"rawtypes","unchecked"})
//创建新数组
Node&K,V&[] newTab = (Node&K,V&[])new Node[newCap];
table = newT
if (oldTab != null) {
for (int j = 0; j & oldC ++j) {
if ((e = oldTab[j]) != null) {
//遍历旧数组,把原来的引用取消,方便垃圾回收
oldTab[j] =
//这个链只有一个节点,根据新数组长度计算在新表中的位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] =
//红黑树的处理
else if (e instanceof TreeNode)
((TreeNode&K,V&)e).split(this, newTab, j, oldCap);
//链表长度大于1,小于8的情况,下面高能,单独拿出来分析
else { // preserve order
Node&K,V& loHead = null, loTail =
Node&K,V& hiHead = null, hiTail =
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loTail.next =
if (hiTail == null)
hiTail.next =
} while ((e = next) != null);
if (loTail != null) {
loTail.next =
newTab[j] = loH
if (hiTail != null) {
hiTail.next =
newTab[j + oldCap] = hiH
return newT
可以看到,Java8把初始化数组和扩容全写在resize方法里了,但是思路还是一样的,扩容后要转移,转移要重新计算在新表中的位置,上面代码最后一块高能可能不太好理解,刚开始看的我一脸懵逼,看了一张的分析图才豁然开朗,在分析前先捋清楚思路
下面我们讲解下JDK1.8做了哪些优化。经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1(5)和key2(21)两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
图a中key1(5)和key(21)计算出来的都是5,元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
图b中计算后key1(5)的位置还是5,而key2(21)已经变成了21,因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。
有了上面的分析再回来看下源码
else { // preserve order
//定义两条链
//原来的hash值新增的bit为0的链,头部和尾部
Node&K,V& loHead = null, loTail =
//原来的hash值新增的bit为1的链,头部和尾部
Node&K,V& hiHead = null, hiTail =
//循环遍历出链条链
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loTail.next =
if (hiTail == null)
hiTail.next =
} while ((e = next) != null);
//扩容前后位置不变的链
if (loTail != null) {
loTail.next =
newTab[j] = loH
//扩容后位置加上原数组长度的链
if (hiTail != null) {
hiTail.next =
newTab[j + oldCap] = hiH
为了更清晰明了,还是举个栗子,下面的表定义了键和它们的hash值(数组长度为16时,它们都在5号坑)
假设一个hash算法刚好算出来的的存储是这样的,在存第13个元素时要扩容
那么流程应该是这样的(只关注5号坑键值对的情况),第一次:
省略中间几次,第六次
两条链找出来后,最后转移一波,大功告成。
//扩容前后位置不变的链
if (loTail != null) {
loTail.next =
newTab[j] = loH
//扩容后位置加上原数组长度的链
if (hiTail != null) {
hiTail.next =
newTab[j + oldCap] = hiH
总结下Java8 put流程图
1.发生hash冲突时,Java7会在链表头部插入,Java8会在链表尾部插入
2.扩容后转移数据,Java7转移前后链表顺序会倒置,Java8还是保持原来的顺序
3.关于性能对比可以参考,引入红黑树的Java8大程度得优化了HashMap的性能一名准Android程序员的博客
Java HashMap的扩容
最近博主参加面试,发现自己对于Java的HashMap的扩容过程理解不足,故最近在此进行总结。
首先说明博主德Java为1.8版本
HashMap中的变量
首先要了解HashMap的扩容过程,我们就得了解一些HashMap中的变量:
Node&K,V&:链表节点,包含了key、value、hash、next指针四个元素table:Node&K,V&类型的数组,里面的元素是链表,用于存放HashMap元素的实体size:记录了放入HashMap的元素个数loadFactor:负载因子threshold:阈值,决定了HashMap何时扩容,以及扩容后的大小,一般等于table大小乘以loadFactor
HashMap的构造函数
HashMap的构造函数主要有四个,代码如下:
public HashMap(int initialCapacity, float loadFactor) {
this.loadFactor = loadF
this.threshold = tableSizeFor(initialCapacity);
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
public HashMap(Map&? extends K, ? extends V& m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
其中主要有两种形式:
直接拷贝别的HashMap的形式,在此不作讨论定义初始容量大小(table数组的大小,缺省值为16),定义负载因子(缺省值为0.75)的形式值得注意的是,当我们自定义HashMap初始容量大小时,构造函数并非直接把我们定义的数值当做HashMap容量大小,而是把该数值当做参数调用方法tableSizeFor,然后把返回值作为HashMap的初始容量大小:
* 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;
该方法会返回一个大于等于当前参数的2的倍数,因此HashMap中的table数组的容量大小总是2的倍数。
何时进行扩容?
HashMap使用的是懒加载,构造完HashMap对象后,只要不进行put 方法插入元素之前,HashMap并不会去初始化或者扩容table:
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&[] Node&K,V& int n,
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
if (++size & threshold)
afterNodeInsertion(evict);
在putVal方法第8、9行我们可以看到,当首次调用put方法时,HashMap会发现table为空然后调用resize方法进行初始化
在putVal方法第16、17行我们可以看到,当添加完元素后,如果HashMap发现size(元素总数)大于threshold(阈值),则会调用resize方法进行扩容
在这里值得注意的是,在putVal方法第10行我们可以看到,插入元素的hash值是一个32位的int值,而实际当前元素插入table的索引的值为 :
(table.size - 1)& hash
又由于table的大小一直是2的倍数,2的N次方,因此当前元素插入table的索引的值为其hash值的后N位组成的值
resize扩容
final Node&K,V&[] resize() {
Node&K,V&[] oldTab =
int oldCap = (oldTab == null) ? 0 : oldTab.
int oldThr =
int newCap, newThr = 0;
if (oldCap & 0) {
if (oldCap &= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldT
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 = oldT
// 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 * loadF
newThr = (newCap & MAXIMUM_CAPACITY && ft & (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
threshold = newT
@SuppressWarnings({"rawtypes","unchecked"})
Node&K,V&[] newTab = (Node&K,V&[])new Node[newCap];
table = newT
if (oldTab != null) {
for (int j = 0; j & oldC ++j) {
if ((e = oldTab[j]) != null) {
oldTab[j] =
if (e.next == null)
newTab[e.hash & (newCap - 1)] =
else if (e instanceof TreeNode)
((TreeNode&K,V&)e).split(this, newTab, j, oldCap);
else { // preserve order
Node&K,V& loHead = null, loTail =
Node&K,V& hiHead = null, hiTail =
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loTail.next =
if (hiTail == null)
hiTail.next =
} while ((e = next) != null);
if (loTail != null) {
loTail.next =
newTab[j] = loH
if (hiTail != null) {
hiTail.next =
newTab[j + oldCap] = hiH
return newT
从第15 ~ 20行可以看到,若threshold(阈值)不为空,table的首次初始化大小为阈值,否则初始化为缺省值大小16
当table需要扩容时,从第11 ~ 13行可以看到,扩容后的table大小变为原来的两倍,接下来就是进行扩容后table的调整:
假设扩容前的table大小为2的N次方,有上述put方法解析可知,元素的table索引为其hash值的后N位确定
那么扩容后的table大小即为2的N+1次方,则其中元素的table索引为其hash值的后N+1位确定,比原来多了一位
因此,table中的元素只有两种情况:
元素hash值第N+1位为0:不需要进行位置调整元素hash值第N+1位为1:调整至原索引的两倍位置在resize方法中,第45行的判断即用于确定元素hashi值第N+1位是否为0:
若为0,则使用loHead与loTail,将元素移至新table的原索引处若不为0,则使用hiHead与hiHead,将元素移至新table的两倍索引处扩容或初始化完成后,resize方法返回新的table
HashMap的扩容机制---resize()
HashMap的容量与扩容
2、HashMap的扩容是怎样扩容的,为什么是2的N次幂的大小?
深入理解HashMap(原理,查找,扩容)
HashMap和ArrayList如何扩容
HashMap扩容
HashMap扩容机制、线程安全
调试JDK源码-一步一步看HashMap怎么Hash和扩容
HashMap 的扩容机制
java线程池与五种常用线程池策略使用与解析
没有更多推荐了,}

我要回帖

更多关于 在一维数组查找一个整数倍数的个数 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信