热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

HashMap的扩容机制

复习散列数据结构之余重新研究了一下Java中的HashMap;本文主要针对:1、HashMap的初始化;2、HashMap的插入;3:HashMap的扩容这三个方面进行总结1、HashMap的初始

复习散列数据结构之余重新研究了一下Java中的HashMap;本文主要针对:1、HashMap的初始化;2、HashMap的插入;3:HashMap的扩容这三个方面进行总结

1、HashMap的初始化

首先我们来看看代码:

 1 public HashMap(int initialCapacity, float loadFactor) {
 2     if (initialCapacity <0)
 3         throw new IllegalArgumentException("Illegal initial capacity: " +
 4                                            initialCapacity);
 5     if (initialCapacity > MAXIMUM_CAPACITY)
 6         initialCapacity = MAXIMUM_CAPACITY;
 7     if (loadFactor <= 0 || Float.isNaN(loadFactor))
 8         throw new IllegalArgumentException("Illegal load factor: " +
 9                                            loadFactor);
10     this.loadFactor = loadFactor;
11     this.threshold = tableSizeFor(initialCapacity);
12 }
13 
14 /**
15  * 返回一个等于指定容量的2的N次方的容量
16  * Returns a power of two size for the given target capacity.
17  */
18 static final int tableSizeFor(int cap) {
19     int n = cap - 1;
20     n |= n >>> 1;
21     n |= n >>> 2;
22     n |= n >>> 4;
23     n |= n >>> 8;
24     n |= n >>> 16;
25     return (n <0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
26 }
由此我们可知hashmap的容量总是2的N次方,而且这个值大于且最接近指定值大小的2次幂,比如就算我们指定new hashmap(1000),实际上构造出来的也是:hashmap(1024);

那问题来了:为什么JDK要这样做呢?

要解决这个问题我们需要看看hashmap的是如何找到元素的存放位置的:
 1 方法一:
 2 static final int hash(Object key) {   //jdk1.8 & jdk1.7
 3      int h;
 4      // h = key.hashCode() 为第一步 取hashCode值
 5      // h ^ (h >>> 16)  为第二步 高位参与运算
 6      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 7 }
 8 方法二:
 9 static int indexFor(int h, int length) {  
10 //jdk1.7的源码,jdk1.8没有这个方法,取而代之的是在1.8中的putVal()方法中的第3行中:
11 //if ((p = tab[i = (n - 1) & hash]) == null))  
12 //原理都是一样的,作用也是一样的,都是定位元素位置
13      return h & (length-1);  //第三步 取模运算
14 },

看到这里我们自然会问:为什么hash()函数中要用对象的hashcode与自身的高16位进行异或运算(hashcode ^ (hashcode >>> 16))?

这是一个很精妙的设计:

a:其实我们大可以直接用对象的hashcode直接作为下标来存储对象,这个值对于不同的对象必须保证唯一(JAVA规范),这也是大家常说的,重写equals必须重写hashcode的重要原因。但是对象的hashcode返回的是一个32位的int,那这个数组就有40亿左右,大部分情况下我们不需要这么长的数组,我们只需要低位就行,比如只根据低16位创建数组,那数组长度大概就只需要6万多,但是直接创建6万多长度的数组肯定也不合理,而且只取低16位的随机性肯定没有取32位的随机性大,冲突概率也更高,那JDK如何解决的呢?
b:JDK的处理非常巧妙,hashcode ^ (hashcode >>> 16) 该运算是用对象的hashcode与自己的高十六位进行异或运算,这样计算出来的hash值同时具有高位和低位的特性,这样算出来的hash值可以说就是一个增大了低十六位随机性的hashcoede。这样我们试想一下:只要对象的32位hashcode有一位发生了变化,那返回的hash值就会发生变化,更厉害的是不管这发生变化的那一位是高16位还是低16位,最后低十六位都会被影响到,这样也使得后面取模运算下标时所截取的低位的随机性增加,所计算出来的下标更加随机和均匀;
为什么JDK中要用h & (length-1)来计算元素存储位置下标?
计算元素的存放位置,我们首先想到的是根据对象的hash值对数组长度取模,这样元素的分布也还算均匀,但是取模运算效率不算高,所以JDK采用了h & (table.length -1)来得到该对象的保存位,数组长度是2的整次幂时,(数组长度-1)正好相当于一个“低位掩码”,“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。“与”操作的结果就是截取了最低的四位值。也就相当于取模操作,而且经过前面的hash()函数的的处理,低位的随机性增加了,所以可知最后运算得到的存储下标也会更加随机更加均匀。
综上:当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。

2、HashMap的插入:

 1   public V put(K key, V value) {
 2         return putVal(hash(key), key, value, false, true);
 3     }
 4  
 5     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 6                    boolean evict) {
 7         Node[] tab;
 8         Node p;
 9         int n, i;
10         // 如果表为空则创建,这也体现了hashmap是懒加载的,构造完hashmap之后,如果没有put操作,table是不会初始化的
11         if ((tab = table) == null || (n = tab.length) == 0)
12             n = (tab = resize()).length;
13         // 这一步是根据hash值对数组长度取模,找到元素应该存放的位置,
14         //JDk1.7中把该步骤写成另一个方法,1.8中直接写在此处
15         //如果为空则创建一个节点
16         if ((p = tab[i = (n - 1) & hash]) == null)
17             tab[i] = newNode(hash, key, value, null);
18         //不为空的情况
19         else {
20             Node e;
21             K k;
22             // 节点已经存在,并且key一样,直接覆盖
23             if (p.hash == hash &&
24                     ((k = p.key) == key || (key != null && key.equals(k))))
25                 e = p;
26             //判断是否是红黑树
27             else if (p instanceof TreeNode)
28                 e = ((TreeNode) p).putTreeVal(this, tab, hash, key, value);
29             //执行到这里说明该位置存放的是链表
30             else {
31                 for (int binCount = 0; ; ++binCount) {
32                     if ((e = p.next) == null) {
33                         p.next = newNode(hash, key, value, null);
34                         //链表长度大于8转换为红黑树进行处理 TREEIFY_THRESHOLD = 8
35                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
36                             treeifyBin(tab, hash);
37                         break;
38                     }
39                     // key已经存在直接覆盖value
40                     if (e.hash == hash &&
41                             ((k = e.key) == key || (key != null && key.equals(k))))
42                         break;
43                     p = e;
44                 }
45             }
46             if (e != null) { // existing mapping for key
47                 V oldValue = e.value;
48                 if (!onlyIfAbsent || oldValue == null)
49                     e.value = value;
50                 afterNodeAccess(e);
51                 return oldValue;
52             }
53         }
54         ++modCount;
55         // 超过最大容量threshold 就扩容
56         if (++size > threshold)
57             resize();
58         afterNodeInsertion(evict);
59         return null;
60     }

3、HashMap的扩容:

有了前面的铺垫,下面理解HashMap的扩容应该不会有太大的困难了:

我们先来看看JDK对扩容函数的注释:
 1 /**
 2  * Initializes or doubles table size.  If null, allocates in
 3  * accord with initial capacity target held in field threshold.
 4  * Otherwise, because we are using power-of-two expansion, the
 5  * elements from each bin must either stay at same index, or move
 6  * with a power of two offset in the new table.
 7  *
 8  * @return the table
 9  */
10 final Node[] resize() {...}
这段话后面描述到: 因为我们使用2的N次幂的扩容机制,所以元素在扩容后的数组中要么是留在原来的下标处,要么是在原位置基础上再移动2的N次幂
这有点费解,我们看一下下面的寻址过程:
 
上图的(a)(b)分别对应扩容前和扩容后的hash&(n-1)也就是寻找元素存放位置的过程,可以看到扩容后的n-1相比扩容前的n-1多了一高位1,则再进行&运算时,key1和key2也多了一高位参与运算:
所以,原hash值新增参与运算的的那一bit如果是0,则在新数组中的下标不变,如果原hash值新增参与运算的那一bit是1,则在新数组中的下标为:原索引+原数组容量。
因此现在JDK 只需要判断每个元素的hash值新增参与运算的那一bit是1还是0就可以给每个元素确定新数组中的位置,这样做可以巧妙的把原来处于同一个下标索引处的多个元素在新的数组中分散开来,如上面的(a)(b)过程,(a)过程中key1和key2虽然hash值不同,但是运算出了同一个索引值,所以存在同一个位置,但是在(b)过程中由于扩容的影响多了1bit参与运算,所以key1和key2就被分配到了不同的索引处!
下面看看JDK如何实现扩容,真是太巧妙了!
 1 final Node[] resize() {
 2     Node[] oldTab = table;
 3     int oldCap = (oldTab == null) ? 0 : oldTab.length;
 4     int oldThr = threshold;
 5     int newCap, newThr = 0;
 6     if (oldCap > 0) {
 7         if (oldCap >= MAXIMUM_CAPACITY) {
 8             threshold = Integer.MAX_VALUE;
 9             return oldTab;
10         }
11         else if ((newCap = oldCap <<1) 12                  oldCap >= DEFAULT_INITIAL_CAPACITY)
13             newThr = oldThr <<1; // double threshold
14     }
15     else if (oldThr > 0) // initial capacity was placed in threshold
16         newCap = oldThr;
17     else {               // zero initial threshold signifies using defaults
18         newCap = DEFAULT_INITIAL_CAPACITY;
19         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
20     }
21     if (newThr == 0) {
22         float ft = (float)newCap * loadFactor;
23         newThr = (newCap float)MAXIMUM_CAPACITY ?
24                   (int)ft : Integer.MAX_VALUE);
25     }
26     threshold = newThr;
27     @SuppressWarnings({"rawtypes","unchecked"})
28         Node[] newTab = (Node[])new Node[newCap];
29     table = newTab;
30     //移动数据
31     if (oldTab != null) {
32         for (int j = 0; j j) {
33             Node e;
34             if ((e = oldTab[j]) != null) {
35                 oldTab[j] = null;
36                 if (e.next == null)
37                     newTab[e.hash & (newCap - 1)] = e;
38                 else if (e instanceof TreeNode)
39                     ((TreeNode)e).split(this, newTab, j, oldCap);
40                 else { // preserve order
41                     Node loHead = null, loTail = null;
42                     Node hiHead = null, hiTail = null;
43                     Node next;
44                     do {
45                         next = e.next;
46                         //把元素的hash值与旧的容量做&运算,便可得出元素的hash值
47                         //新增的参与运算的那一bit是1还是0
48                         //hash&(n-1) 与 hash & n  的区别:
49                         //加入n为16,则n-1为:1111 ,n为:10000
50                         //n比n-1高了一bit,且因为n为2的n次幂,
51                         //所以,hash&n 可以得出扩容后元素hash值多参与运算的那一bit是0还是1
52                         //新增参与运算的bit是0,则位置不变
53                         if ((e.hash & oldCap) == 0) {
54                             if (loTail == null)
55                                 loHead = e;
56                             else
57                                 loTail.next = e;
58                             loTail = e;
59                         }
60                         //新增参与运算的bit是1,位置变为: 原索引+原数组容量
61                         else {
62                             if (hiTail == null)
63                                 hiHead = e;
64                             else
65                                 hiTail.next = e;
66                             hiTail = e;
67                         }
68                     } while ((e = next) != null);
69                     if (loTail != null) {
70                         loTail.next = null;
71                         //位置不变
72                         newTab[j] = loHead;
73                     }
74                     if (hiTail != null) {
75                         hiTail.next = null;
76                         //位置变为: 原索引+原数组容量
77                         newTab[j + oldCap] = hiHead;
78                     }
79                 }
80             }
81         }
82     }
83     return newTab;
84 }

 第一次写博客,有很多表述可能不是很清楚,望谅解。


推荐阅读
  • HashMap的相关问题及其底层数据结构和操作流程
    本文介绍了关于HashMap的相关问题,包括其底层数据结构、JDK1.7和JDK1.8的差异、红黑树的使用、扩容和树化的条件、退化为链表的情况、索引的计算方法、hashcode和hash()方法的作用、数组容量的选择、Put方法的流程以及并发问题下的操作。文章还提到了扩容死链和数据错乱的问题,并探讨了key的设计要求。对于对Java面试中的HashMap问题感兴趣的读者,本文将为您提供一些有用的技术和经验。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • Whatsthedifferencebetweento_aandto_ary?to_a和to_ary有什么区别? ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 模板引擎StringTemplate的使用方法和特点
    本文介绍了模板引擎StringTemplate的使用方法和特点,包括强制Model和View的分离、Lazy-Evaluation、Recursive enable等。同时,还介绍了StringTemplate语法中的属性和普通字符的使用方法,并提供了向模板填充属性的示例代码。 ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • 本文介绍了一个适用于PHP应用快速接入TRX和TRC20数字资产的开发包,该开发包支持使用自有Tron区块链节点的应用场景,也支持基于Tron官方公共API服务的轻量级部署场景。提供的功能包括生成地址、验证地址、查询余额、交易转账、查询最新区块和查询交易信息等。详细信息可参考tron-php的Github地址:https://github.com/Fenguoz/tron-php。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文介绍了使用哈夫曼树实现文件压缩和解压的方法。首先对数据结构课程设计中的代码进行了分析,包括使用时间调用、常量定义和统计文件中各个字符时相关的结构体。然后讨论了哈夫曼树的实现原理和算法。最后介绍了文件压缩和解压的具体步骤,包括字符统计、构建哈夫曼树、生成编码表、编码和解码过程。通过实例演示了文件压缩和解压的效果。本文的内容对于理解哈夫曼树的实现原理和应用具有一定的参考价值。 ... [详细]
author-avatar
fvcvb_974
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有