对java中hashmap深入理解

1、HashMap的结构是怎样的?

二维结构,第一维是数组,第二维是链表

2、Get方法的流程是怎样的?

先调用Key的hashcode方法拿到对象的hash值,然后用hash值对第一维数组的长度进行取模,得到数组的下标。来看一下 hash 方法的源码(JDK 8 中的 HashMap):

static final int hash(Object key) {

    int h;

    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

这段代码究竟是用来干嘛的呢?

我们都知道,key.hashCode() 是用来获取键位的哈希值的,理论上,哈希值是一个 int 类型,范围从-2147483648 到 2147483648。前后加起来大概 40 亿的映射空间,只要哈希值映射得比较均匀松散,一般是不会出现哈希碰撞的。

但问题是一个 40 亿长度的数组,内存是放不下的。HashMap 扩容之前的数组初始大小只有 16,所以这个哈希值是不能直接拿来用的,用之前要和数组的长度做取模运算,用得到的余数来访问数组下标才行。

取模运算有两处。

取模运算(“Modulo Operation”)和取余运算(“Remainder Operation ”)两个概念有重叠的部分但又不完全一致。主要的区别在于对负整数进行除法运算时操作不同。取模主要是用于计算机术语中,取余则更多是数学概念。

一处是往 HashMap 中 put 的时候(putVal 方法中):

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {

     HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;

     if ((tab = table) == null || (n = tab.length) == 0)

         n = (tab = resize()).length;

     if ((p = tab[i = (n - 1) & hash]) == null)

         tab[i] = newNode(hash, key, value, null);

}

一处是从 HashMap 中 get 的时候(getNode 方法中):

final Node<K,V> getNode(int hash, Object key) {

     Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

     if ((tab = table) != null && (n = tab.length) > 0 &&

            (first = tab[(n - 1) & hash]) != null) {}

}

其中的 (n - 1) & hash 正是取模运算,就是把哈希值和(数组长度-1)做了一个“与”运算。

 

可能大家在疑惑:取模运算难道不该用 % 吗?为什么要用 & 呢?

 

这是因为 & 运算比 % 更加高效,并且当 b 为 2 的 n 次方时,存在下面这样一个公式。

 

a % b = a & (b-1)

 

用 2 n 2^n 2n 替换下 b 就是:

 

Java中HashMap的hash方法原理是什么

 

我们来验证一下,假如 a = 14,b = 8,也就是 2 3 2^3 23,n=3。

 

14%8,14 的二进制为 1110,8 的二进制 1000,8-1 = 7 的二进制为 0111,1110&0111=0110,也就是 0* 2 0 2^0 20+1* 2 1 2^1 21+1* 2 2 2^2 22+0* 2 3 2^3 23=0+2+4+0=6,14%8 刚好也等于 6。

 

这也正好解释了为什么 HashMap 的数组长度要取 2 的整次方(一个是为了保证每次的hash寻址都能保持位运算的高效性,还有就是转移数据的时候,新数组下标只要在旧数据基础上加上一个原来数据长度就行,不需要重新计算下标,则表长应基于上面的公式选取长度定位2的整次方),所以32位数据用4位二进制数存储,就看32位数的后四位就行,如果要存储字母数据到5位二进制长度的hashmap里面,就看字母的后5位数就可以。

因为(数组长度-1)正好相当于一个“低位掩码”——这个掩码的低位最好全是 1,这样 & 操作才有意义,否则结果就肯定是 0,那么 & 操作就没有意义了。

 

a&b 操作的结果是:a、b 中对应位同时为 1,则对应结果位为 1,否则为 0

 

2 的整次幂刚好是偶数,偶数-1 是奇数,奇数的二进制最后一位是 1,保证了 hash &(length-1) 的最后一位可能为 0,也可能为 1(这取决于 h 的值),即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证哈希值的均匀性。

 

& 操作的结果就是将哈希值的高位全部归零,只保留低位值,用来做数组下标访问。异或(^)运算是基于二进制的位运算,采用符号 XOR 或者^来表示,运算规则是:如果是同值取 0、异值取 1

 

由于混合了原来哈希值的高位和低位,所以低位的随机性加大了(掺杂了部分高位的特征,高位的信息也得到了保留)。

 

结果再与数组长度-1(00000000 00000000 00000000 00001111)做取模运算,得到的下标就是 00000000 00000000 00000000 00000101,也就是 5。

 

还记得之前我们假设的某哈希值 10100101 11000100 00100101 吗?在没有调用 hash 方法之前,与 15 做取模运算后的结果也是 5,我们不妨来看看调用 hash 之后的取模运算结果是多少。

 

某哈希值 00000000 10100101 11000100 00100101(补齐 32 位),将它右移 16 位(h >>> 16),刚好是 00000000 00000000 00000000 10100101,再进行异或操作(h ^ (h >>> 16)),结果是 00000000 10100101 00111011 10000000

 

结果再与数组长度-1(00000000 00000000 00000000 00001111)做取模运算,得到的下标就是 00000000 00000000 00000000 00000000,也就是 0。综上所述,hash 方法是用来做哈希值优化的,把哈希值右移 16 位,也就正好是自己长度的一半,之后与原哈希值做异或运算,这样就混合了原哈希值中的高位和低位,正好让每个高位对应一个低位,(若两个数低16位相同如果不这样操作便会发生碰撞,若他们的高位不同,如果再将他们的高16位与相同的低16位进行混合的异或运算,则计算出的hash值便不同,避免了碰撞)增大了随机性。

说白了,hash 方法就是为了增加随机性,让数据元素更加均衡的分布,减少碰撞。这个数组下标所在的元素就是第二维链表的表头。然后遍历这个链表,使用Key的equals同链表元素进行比较,匹配成功即返回链表元素里存放的值。

3、Get方法的时间复杂度是多少?

HashMap中,如果key经过hash算法得出的数组索引位置全部不相同,即Hash算法非常好,那样的话,getKey方法的时间复杂度就是O(1),如果Hash算法技术的结果碰撞非常多,假如Hash算极其差,所有的Hash算法结果得出的索引位置一样,那样所有的键值对都集中到一个桶中,或者在一个链表中,或者在一个红黑树中,时间复杂度分别为O(n)和O(lgn)。

4、请解释一下HashMap的参数loadFactor,它的作用是什么?

loadFactor表示HashMap的拥挤程度,影响hash操作到同一个数组位置的概率。默认loadFactor等于0.75,当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,在HashMap的构造器中可以定制loadFactor。

5、请说明一下HashMap扩容的过程?

扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。这个rehash的过程是很耗时的,特别是HashMap很大的时候,会导致程序卡顿,而2倍内存的关系还会导致内存瞬间溢出,实际上是3倍内存,因为老结构的内存在rehash结束之前还不能立即回收。那为什么不能在HashMap比较大的时候扩容扩少一点呢,关于这个问题我也没有非常满意的答案,我只知道hash的取模操作使用的是按位操作,按位操作需要限制数组的长度必须是2的指数。另外就是Java堆内存底层用的是TcMalloc这类library,它们在内存管理的分配单位就是以2的指数的单位,2倍内存的递增有助于减少内存碎片,减少内存管理的负担。

 

本文转自:https://www.toutiao.com/i6504574147786441229/?group_id=6504574147786441229&group_flags=0

https://blog.csdn.net/qq_51598480/article/details/122408456

 https://blog.csdn.net/u010425839/article/details/106620440/

posted @ 2018-01-25 22:59  ppjj  阅读(455)  评论(0编辑  收藏  举报