那些年,我们又爱又恨的HashMap,你又了解多少呢?

0、推荐阅读

一、HashMa~ & ~ d - 4p集合简介

特点:

  • HashMap是Map接口的一个重要实现类,基于哈希表,以key-value的形式存储数据,线程不安全;
  • nulg g h s M S *l可以作为键,这样的键只能有一个,可以有一个或多个键对应的值为null;
  • 存取元素( Z ^ l无序。

底层数据结构:

  • JDK1.8之前,由数组+Y o 1 @ ` E p W a链表构成,数组是存储数据的主体,链表是为了解决哈希冲突而存在的;
  • JDK1.8以后,由数组+链表+红黑树构成,当链表长度大于阈值(默认为8),并且数组长度大于64时,链表会M } @ z @ % (转化为红黑树去解决哈希冲突。

注意: 链表转化为红黑树之前会进行判断,若果阈值大于8,但是数组长度小于6u 0 L O4,这时链表不会g 1 $ g K ^ (转化为红黑树去存储数据,而是会对u 7 %数组进行扩容。

这样做的原因: 如果数组比较小,应尽量避免红黑树结构。因为红黑树结构较为复杂,红黑树又称为平衡二叉树,需要进行左旋} / d M、右旋、变色O [ & ` n ? `这些g ~ H = r ? K操作+ 9 X m j才能保证平衡。在数组容f D h I量较小的情C 9 c b : f况下,操作数组要比操作红黑树更节省时间。综上所述:为了提高性能以及减少搜索时间,在阈值大于8并且数组长度大于64的情况下链表才会转化为红黑树而存在。具体参考treeifyBin方法。

HashMaps L | ( v - q存储数据结构图:

那些年,我们又爱又恨的HashMap,你又了解多少呢?

二、HashMap底层存储数据的过程

1.以下面代码所示进行分析:

pa= P 1 = Mckage hashmap_demo;
impor} z + 0 Xt java.util.HashMap;p - P F
pup U 2blic class HashMapTest {
public static void main(StringT g E c h v u 2[] args) {
HashMap<String, Intel _ D N C x |ger> map = new HashMap<>();
map.put(\"柳岩\",) g G H 4 , Y e % 18);
map.put(\"杨幂\", 28)J I ! &;
map.put(\"刘德华\"E ? L $, 40);
map.put(\"柳岩\"1 p V e H f # f Q, 20);
System.out.println(h v . Smap);
}
}
/? Z p b R/输出结果:{杨幂=28, 柳岩=20, 刘德华=40}

2.HashMap存储过程T ] : U . p Y M 3图:

那些年,我们又爱又恨的HashMap,你又了解多少呢?

3.存储过程分析:

1u # [.当执行HashMap<String, Integer> map = new HashMap<>();这行代码创建HashMap实例对象时;在JDK1.8之前,会在构造R 5 8 .方法中创建一个长度为16y 6 w T 4 9 i / - 的Entry[] table数组用来存储键值对;JDKL 3 | | O s t1.8之后,创建数组的时机发生了变化,不是在构造方法中创建数组了U 8 8 , L n,而是在第一次调用put()方法时(即第一次向HashMap中添加元素)创建Node[] table数组。

注意: 创建HashMap实例对象在JDK1.8前后发生了变化,主要有两点:创建的时机u * { _ j S c 0 e发生了变化;数组类型发生了变化,由原来的Entry[]类型变为Node[]类型。

2.- 2 M向哈希表中存储柳岩-18,会根据柳岩调用Str* 4 c Uing类中重写后的hashCode()方法计算出柳岩对应的哈希值,然后结合数组长度采用某种算法计算出柳岩在Node[]数组中的索引值。如果该索引位置上无数据,则直接将柳岩-18插入到该索引位置。比a o f V d l 9 t如计算出柳岩对应的索引为3,如上图所示。

面试题:哈希表底层采用那种算法计算出索引值?还有哪些算法计算索引值?

答:采用key的hashCode()方法计算出哈希值,然后结合数组长度进行无符号右移(>>>)、按位异或(^)、按位与(&)计算出索引值;还可以采用平方取中法、取余数、伪随机数法。

取余数:10%8=2 1p W ~ M 7 a n z1%8=3;位0 G ] g运算效率最高,其他方式效I D { 5 } 3率较低。

3.向哈希表中Z ^ f z s O U T存储杨幂-28,计算出该索引位置无数据,直接插入。

4.向哈希表中存储刘德华-40,假设刘德华计算出的索引也是3,那么此时该索引位置不为null,这时底层会比较柳岩和刘德华的哈希值是否一致,如果不一致,则在此索引位置上划出一个节点来存储刘德华-40,这种方式称为拉H L V v ^ D /链法。

补充:索引计算源码p = tab[i = (n - 1) & hash],即索引=哈希值&(数组长度-1),按位与运算等价于取余运算,因为11%8=3,19%8=3,所. j $以会出现索引相同,数组长度相同,但哈希值不同的情况。

5.最后向哈希表中存储柳岩-20,柳岩对应的索引值为3。因为该索引位置已有数据,所以此时会比较柳岩与该a c * z C p索引位置上的其他数据的哈希值是否相等,如果相等,则发生哈希碰撞。此时底层会调用柳岩所属String字符串类中的equals()方法比较两个对象的内容是否相同:

相同:则后添加数据的value值会覆盖之C w a !前的value值,即柳岩-20覆盖掉柳岩-18。

不相同:继续和该索引位置的其他对象进行比较,如果都( k 5不相~ ; ^ F同,则向下划出一个节点存储(拉链法)。

注意点:如果一个索引位置向下拉链,即链表长度& n F [ 7 1 9大于阈值88 h M 3 a且数组长度大于64,则会将此链表转化为红黑树。因为链表的时间复杂度为O(N),红黑树的时间复杂度为O(logN),链表长度多大时O(N)>O(logN)。

三、HashMap的扩容机制

1i 1 L q u W s b U.HashM= D 2 T B 5 ap什么时候进行扩容?

首先看添加元素的put()方法流程:

那些年,我们又爱又恨的HashMap,你又了解多少呢?

说明:

  • 上图中的size表示HashMap中K-V的实时数量v 9 ! ! 2 ~ q,不等于数组的长度N $ l f ) C o j
  • threshold(临C ` H { 5 a 6界值)=ca} [ L Z Ipacity(数组容量)*loadFactory(加载因子),临界值表示当前已占用数组的最大值。size如果c 9 B ^ +超过这个临界值进调用resize()方法进行扩容,扩容后的容量是原来的两倍;
  • 默认情况下,16*x . D 2 U0.75=12,即HashMap中存储的元素超过12就会进行扩容。

2.HashMap扩容后的大小是多S 5 d l #少?

是原来容量的2倍,即HashMap是以2n进行扩容的。

3.HashMap的默认初始r + -容量是多少?

HashMap的无参构造,默认初始值为16,源码如下:

/**
* Constructs an empty <] I !tt>; i S o % iHashMap<+ - & z _ H ] } ^;/) R K 5 S Htt> with the default initial capacity
* (16) and the default lo+ & 7 x Fad factor (0.75).
*/8 2 . T 9
public HashMap() {
this.loadFactor = DEt V Q l sFAULT_LOAD_FACTOR; //^ : 0 all other fields defaulted
}

默认初始值源码:

/**
* The dec 7 N n % z Qfault initial capa. _ } / B : Z dcity -S y ~ v E . MUST be a# d Z x 4 w 4 n power of two.
* 默认初始容K ~ 8 =量必须是2的幂
*/~ A . { ~ W
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

由源码可以看U V O ) T 9到,HashMap的默认初始容量为1左移N ) r r4位,即1*2的4次方为16。如果使用HashMap的无参构造进行初始化1 O =,第一次put元素时,会触发resize()方法(扩容方法),扩容后的容量为16。这一点和ArrayList初始化过程很相似(使用ArrayList的无参构造初始化时,创建的是一个空数{ ` `组,当第一次向空数组添加元素时会触发grow()扩容方法,扩容后P j ! [的容量% 1 j 5 4 9 d A为10)。

4.指定初始容量为什么必须是2的幂?

HashMap的有参构造,即可以指定初始化容量大小,源码如下:

/**
* Constructs an empty <tt>HashMap</tt> with the sp$ P 1 3 o ^ecifR K _ a }ied initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initil D ! u | C ial capacity.
* @throws IllegalArgumentException if the initialA + 4 f | M % 4 ? capacity is negative.
*/
pub~ , T C y ylic HashMap(int inf K ^ u R p E 3 AitialCapacity) {
this(initialCapacity, DEFAULT_LOAD_F! 4 C G GACTOR);
}W 9 o - V J

即构造一个指定容量和默认加载因子(0.75)的空 h `HashMap。

由上面的内容我们知道,当向HashMap中添加元素时,首先会根据keyi k - G n M X , a的哈希值结合数组长度计算出索引位置。HashMap为了存取高效需要减少哈希碰撞,使数据 & J L分配均匀,采用按位与**hash&(l[ } x + . . a bength-1)**计算索引值。

HashMap采用取余的算法计算索引,即hash%length,但是取余运算不如位运算效率高,所以底层采用按位与**hash&(lengtH J ,h-1)**进行运算。两种算法等价的前提就是length是2的n次幂。

5.H 3 m (为什么这样9 L g @ p c C V b就能均匀分布?

我们需要知道两个结论:

  • 2的n次方就是1后面n 7 _ T C + * a个0;如2的4次方为16,二进制表示为10000;
  • 2的n次方-1就是n个1;比如2的4次方-1位15,二进制U { 4 4 E ` S表示为1111。

举例说明为什么数组长度是2的n次幂可以均匀分布:

按位与运算:相同二进制位上都是1,结果为1,否则为0。
假设数组长度为2的3次幂8,哈E V k ^希值为3,即3&(8-1)=3,索引为3;
假设数组长度为2的3次幂8,哈希值为2,即2&(8-1)=2,索引为2;
运算过程如下:
3&(8-1)
0000 0011 -->3
0000 0111 -->7
----------------
0000 0011 -->3

2] r m T f , N S&[ l [;(8-1)
0000 0010 -->2
0000 0111 -->7
----------------
0000 0010 -->2

结论:索引值不同,不同索引位置都有数据分布,分& { 9 : ,布均匀。

h 9 O设数组长度不是2的n次幂,比如长度为9,运算过程如下:

假设数组长度为9,哈希值为3,即3&(9-1)=3,索引为0;
假设数组长度为9,哈希值为2,即2&(9-1)=2,索引为2;
运算过程如下:
3&(9-1)
0000 0011 -->3
0000 1000 -->8
----------------
0000 0000 -->0

2&(9-1)
0000 0010 -->2
0000 1000 -->8
----------------
0000 0000 -->0

结论:索引值都为0,导致同一索引位置上有很多数据,而其他索引位置没有数据,致使链表或红黑树过长,效~ / n o率降低。

注意: ha/ | 3 S 4sh%length等价于hash&(length-1)的前提条件是数组长度为X 8 s ` + P2的n次幂。由于底层采用按位@ 1 t { ! { U .与运算计算索引值,所以需要保证数组长度必须为2的n次幂。

6.如果指定的初始容量不是2的n次幂会怎b + ^ ^ Y L q J样?E * u z M {

这时HashMap会通过位运算和或运算得到一个2; / u i F t R i的幂次方数,并且这个数是离指定容量最小的2的幂次数。比如初始容量为10,经过运算最后会得到16。

该过程涉及到的源码如下:

//创建HashMap! # B & F集合对象,并指定容量为10,不是2的幂
HashMapF } X V Q . 3 ) 4<String, Integer> map = new HashMap<>(10);
//调用` x ; ~ l # s有参构造
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//this关键字继续调用
public HashMap(int initialCapacity, float loadFacH l w s m e { k ator) {//initialCapacity=10
if (initialCapacity < 0)
throw new IllegalArgumentException(\"Illegal ini[ ! = ` a + etial capacity: \" +
initialCapacitM y : I j i B - sy);
if (initialCapacity >a y 5 q MAXIMUM_CAV S = z ; 1PACITY)
initialCapacity = MAXIMUM_CA7 + # zPACx & * , J ;ITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException(\"Illegal load factor: \" +
loadFactor);
this.N Q - N t & o ^loadFactor =& a 8 loadFactor;
this.threshoG d m o U 4 [ Nld = tableSizeFor(initialCapacity);//initialCapacity=10
}
//调用tableSizeFor()方法
/**
* Returns a power3 H q L H 4 ` J of two size for the given target capacity.
* 返回指定目标容量的2的幂。
*/
static finalk L A ` ? int tableSizeFor(int cap) {
iM M . xnt n = cap - 1;
n |= n >>> 1;
n |= n >&g! B Z =t;> 2;
n |= n >>> 4;
n |= n >&gp | h ~t;> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

下面分析tableSizeFor()方法:

  • int n = cap - 1;为什么d 7 n要减1操作呢?
这是为了防止`cpa`已经是2的幂了。如果`cpa`已经是2的幂,又没有执行减1的操作,则执行完下面的无符号右移后,返回的将为`cap`的2倍。
  • n等与0时,返回1,这里不讨论你等于0的情况。
  • |表示按位或运算:运算规则为相r y $ 7 n - _ ;同二进制r S 0 L位上都是0,结果为0,否则为1。

第1次运算:

iJ X p t =nt n = cap - 1;//cap=10,n=9
n |= n >>> 1;//无符号右移1位,然后再与n进行u 9 * O ^或运算
00000000 00000000 00000000 00001001 //n=9
00000000 00000000 00000000 00000100 //9无符号J W r P D ` V v 右移1位变为4
-----------------------------------------------
00000000 00000000 00000000 00001101 //按位或运算结果为13,即此时n=13

第2次运算:

int n = 13
n |= n >>> 2;
00000000 00000000 00000000 00001101 //n=13
00000000 00000000 00000000 00000011 //13无符号右移2位变为3
----------: D f `---------------------------------D 2 H $ a a-----
00000000 00000000 00000000 00001111 //按位或运算结果为15,即此时n=15

第3次运算:

int n = 15
n |= n >>> 4;
00000000 00000000 00000000 00001111 //n=15
00000000 00000000 00000000 00000000 //15无符号右移4位变为0
---------------------------------------) g t i u ) v g 3---------
00000000 00000000 00000000 00001111 //按位或运算结果为15,即此时n=15

接下来的运算结果都是n=15,由于最后有一个n + 1操作,最后结果为16。

总结: 由以上运算过程可以看出S e N p 6,如果指定的初始容量不是2的n次幂,经过运算后会得到离初始容量最W u ( 2 a小的2 % * I b W F u Z幂。

四、HashMap源码分析

1.成员变量

privaO y ? Y V 8 - kte static final long serialVersionUID = 362498820763181269 Z ^ g5L; //序列化版本号
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //初始化容量,必须是2的n次幂
static final int MAXIMUM_CAPAE O H 6 4CITY = 1 << 30; //集合最大容量:2的30次幂
static final floaq ? V  / st DEFAULT_LOAD_FACTOR = 0.75f; //默认的加载因子
/**1.加载因子是用来衡量HashMap的疏密程度,计算HashMap的实时加载因子的方法为:size/capacity;
*2: k [ # / 4 $ i.加载因子太大导致查找| M l C . i元素效率低,太小导致数组的利用率低,默认值为0.75f是官方给出的一个较好的临界值;
*3.当HashMf , I p p % Dap里面容纳的元素已k M - M g g v经达到HashMap数组长度的75%时,g o E 3表示HashMap太挤了,需要扩容,而扩容这个过程涉及到rehash、复制数据等操作,非常消耗性能,所以开发中尽量减少扩& O l , r m C e容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免扩容;
*4. + q同时在HashMap的构造方法中可以指定加载因子大小。
*/
HashMap(int initialCapacity, float loadFactor) //构造一个带指定初始c ! A - ) O h O容量和加载因子的空HashMap
static final int TREEIFY_THRESHOLD = 8; //链表转红黑树的第一个条件,链表长度大于阈值8
static final int UNTREEIFY_THRESHOLD = 6; //删除红黑树节点时,当红黑树节点小于6,转化为链表
static final int MIN_TREEIF} W V * rY_CAx * K 1PACITY = 64; //链表@ : F转红黑树的第二个条件,数组长度大于64

五、常见面试题

1.发生哈希碰撞的条件是什么?

两个对象的索引相同,并且hashCoj T Cde(即哈希值)相等时,会发生哈希碰撞。

2.如何解决哈希冲突?

JDK1.8之前,采用链表解决;JDK1.8之后,采用链表+红黑树解决。

3.如果两个key的hashCode相同,如何存储?

使用equals比较内容是否相同:N Z ? = , 3 ; k

相同:后添加的v@ A * 3 0 ? N falue@ v # F ) V } V值会覆盖之前的value值;

不相同:划出一个节点存储(拉链法)。

4.HashMap的底层数据结构?

JDK1.8:数组+链表+红黑树。其中数组是主体,链表和红黑树是为解决哈希冲突而存在的,M O 1 * 7具体如下图所示:

那些年,我们又爱又恨的HashMap,你又了解多少呢?

5.JDK1.8为什么引入了红黑树?红黑树结构不是更复杂吗?

JDK1.8以前HashMap的底层数据是数组+链表,4 | o k H我们知道,即使哈希函数做得再好,哈希表中的元素也很难达到百分之百均匀分布。当HashMap中有大量的元素都存在同一个桶(同一个索引位置),这个桶下就会产生一个很长的链表,这时HashMap就相当于是一个单链表的结构了,假如单链表上有n个元素,则遍历的时间复杂度就是O(n),遍历效率很低。针对这种情况,JDK1.8引入了红黑树,遍历红黑树的时间Q Z + 7 9 w a Z复杂度为O(logn),由于O(n)>O(logn);所6 S U u e以这一问题得到了优化。

6.为什么链D - z v |表长度大于8才转化为红黑树?

我们知道8是从链表转成红黑树的阈值,在源码中有这样一段注释内容:a O I K f 5

/** Because TreeNodes are about twice the size of regular nodes, we use them only when     * bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they         * become too small (due tN - K mo rem9 H ioval or resizing) they are converted back to plain bins.   * In usages with well-distribut3 Y M S ? y r 7 6ed user hashCodes, tre_ G [ 9 F We bins are rarely used.  Ideally,   * under randB P T X @ G d 0 Dom hashCodes, the frequency of nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on * average for the default resi~ P 5 0 0 ? S ?zing threshold of 0.75, a1 D n b 2 U DlthoughW ( p - - N J R F with a large variance * because of resizing granularity. Ignoring variance, the expected occurrences of list * size k are (exz B 5 [ { dp(-0.5) * pow(0.5, k) / factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
*H Y - 2 4: 0.00157952` M b - i
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
*/

翻译过来的的值意思就是说:

红黑树节点所占空间是普通链表节点的两倍,并且链表中存储数据的频率符合泊松分布,我们可以看到,在链表为8的节点上存储数据的概率是0.00000006,这也就表明超过8F g d [ K e 7 0以后的节点存储数据的概率就非常小了。

由上述分析可以得出:

  • 如果G R [ k j小于阈值8就是用红黑树,会使得结构一开始就很复杂;
  • 如果大于阈值8还使用链表3 L i : &,会导致链表节点不能被充分利用;
  • 所以,阈值8是科学合理的一个值,是空间和时间的权衡值。

7.为什么加载因子设置为0.75?边界值是12?

  • 如果加载因子是0.4,那么16*0.4=6,致使数组中满6个空间就扩容,造成数组利用率太低了;
  • 如果加载因子是0.9,那么16*0.9=14,这样就会使数组太满,很大几率造G - ) { A &成某一个索引节点下的链表过长} z l I T,进而导致查找元素效率低;
  • 所以兼顾数组利用率又考虑链表不要太长,经过大量测试0.75是最佳值。

作者:Helay
原文链接5 [ ) a:htt~ c 6 % 6ps://juejin.im/post/5e817b886fb9a03c7d3cee7c

上一篇

李小璐女儿,刘涛女儿,贾静雯女儿,看到赵薇女儿:等你十年!

下一篇

吴莫愁为什么不红了?不是没有实力,而是娱乐圈太现实

评论已经被关闭。

插入图片
返回顶部