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<K, V>[] tab;
 8         Node<K, V> 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<K, V> 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<K, V>) 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<K,V>[] 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<K,V>[] resize() {
 2     Node<K,V>[] 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) < MAXIMUM_CAPACITY &&
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 < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
24                   (int)ft : Integer.MAX_VALUE);
25     }
26     threshold = newThr;
27     @SuppressWarnings({"rawtypes","unchecked"})
28         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
29     table = newTab;
30     //移动数据
31     if (oldTab != null) {
32         for (int j = 0; j < oldCap; ++j) {
33             Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap);
40                 else { // preserve order
41                     Node<K,V> loHead = null, loTail = null;
42                     Node<K,V> hiHead = null, hiTail = null;
43                     Node<K,V> 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 }

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

原文地址:https://www.cnblogs.com/mochenghui/p/11154547.html

时间: 2024-10-02 04:29:19

HashMap的扩容机制的相关文章

HashMap的扩容机制, ConcurrentHashMap和Hashtable主要区别

源代码查看,有三个常量, static final int DEFAULT_INITIAL_CAPACITY = 16; static final int MAXIMUM_CAPACITY = 1 << 30; static final float DEFAULT_LOAD_FACTOR = 0.75f; 三个常量中可以看出,默认的容器大小是16,最大长度是2的30次方,load factor默认是0.75,扩充的临界值是16*0.75=12 当我们往HashMap中put元素的时候,先根据k

HashMap原理(二) 扩容机制及存取原理

我们在上一个章节<HashMap原理(一) 概念和底层架构>中讲解了HashMap的存储数据结构以及常用的概念及变量,包括capacity容量,threshold变量和loadFactor变量等.本章主要讲解HashMap的扩容机制及存取原理. 先回顾一下基本概念: table变量:HashMap的底层数据结构,是Node类的实体数组,用于保存key-value对: capacity:并不是一个成员变量,但却是一个必须要知道的概念,表示容量: size变量:表示已存储的HashMap的key-

Java常见集合的默认大小及扩容机制

在面试后台开发的过程中,集合是面试的热话题,不仅要知道各集合的区别用法,还要知道集合的扩容机制,今天我们就来谈下ArrayList 和 HashMap的默认大小以及扩容机制. 在 Java 7 中,查看源码可以知道:ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素(必须是2的幂,为什么呢???下文有解释).这就是 Java 7 中 ArrayList 和 HashMap  类 的代码片段: // from ArrayList.java JDK 1.7 pri

重磅消息!AppCan扩容机制上线,扩大空间随心所欲!

亲爱的AppCan开发者: AppCan在线打包空间扩容机制已经正式上线啦! 开发的项目越来越多,可用的空间却越来越小,怎么办?[扩容空间]让您不再为空间"斤斤计较"! VS 扩容规则: 1.请关注AppCan官方微信号,并在申请原因中注明您的微信号. AppCan微信二维码 2.有至少一个上线的AppCan应用,可申请50M空间,经官方人员审核后即可扩容. 3.任何一个应用安装量超过1000次可申请一次200M空间. 4.3个月内不可重复申请,且3个月后需凭借其他符合上述要求的应用才

ArrayList源码解析(二)自动扩容机制与add操作

目录 1.ArrayList的自动扩容机制 2.add操作 正文 本篇主要分析ArrayList的自动扩容机制,add和remove的相关方法. 作为一个list,add和remove操作自然是必须的. 前面说过,ArrayList底层是使用Object数组实现的.数组的特性是大小固定,这个特性导致的后果之一就是,当ArrayList中成员个数超过capacity后,就需要重新分配一个大的数组,并将原来的成员拷贝到新的数组之中. add操作前都需要保证capacity足够,因此扩容机制和add放

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:负

【面试必备】透过源码角度一步一步带你分析 ArrayList 扩容机制

一 先从 ArrayList 的构造函数说起ArrayList有三种方式来初始化,构造方法源码如下:/** 默认初始容量大小*/private static final int DEFAULT_CAPACITY = 10; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /*默认构造函数,使用初始容量10构造一个空列表(无参数构造)*/public ArrayList() {this.elementDat

ArrayList的扩容机制

ArrayList的扩容机制: 当向ArrayList中添加元素的时候,ArrayList的存储容量如果满足新元素的容量要求,则直接存储:ArrayList的存储容量如果不满足新元素的容量要求,ArrayList会增强自身的存储能力,以达到存储新元素的要求. 因为不同的JDK版本的扩容机制可能有差异,下面以JDK1.8为例说明一.构造方法 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.element

【数组】- ArrayList自动扩容机制

不同的JDK版本的扩容机制可能有差异 实验环境:JDK1.8 扩容机制: 当向ArrayList中添加元素的时候,ArrayList如果要满足新元素的存储超过ArrayList存储新元素前的存储能力,ArrayList会增强自身的存储能力,已达到存储新元素的要求 ArrayList:本质通过内部维护的数组对象进行数据存储 ①:分析ArrayList的add(E)方法 public boolean add(E e) { ensureCapacityInternal(size + 1); // In