![55d0d97cb4ad48d1e8f49c1b653d7c0b.png](https://i-blog.csdnimg.cn/blog_migrate/2975cbdad6c87fda4fca971d9e99bea8.jpeg)
一、HashMap
为什么面试官这么喜欢问HashMap
- HashMap在实际开发中使用较多
- 考察你是否有钻研精神,是否会对常用的只是会用,还是会去了解原理
- HashMap知识面较广,涉及位运算、算法、数据结构等,容易看出一位面试者水平
- ……
版本区别
- 1.8之前 在jdk1.7中,首先是把元素放在一个个数组里面,后来存放的数据元素越来越多,于是就出现了链表,对于数组中的每一个元素,都可以有一条链表来存储元素。这就是有名的“拉链式”存储方法。
- 1.8之后 由于存储的元素越来越多,链表也越来越长,在查找一个元素时候效率不仅没有提高(链表不适合查找,适合增删),反倒是下降了不少,于是就对这条链表进行了一个改进。如何改进呢?就是把这条链表变成一个适合查找的树形结构,没错就是红黑树。值得注意的是,因为需要为了退化成链表和遍历做准备,这个红黑树并不是纯红黑树,而是红黑树和双向链表的叠加结构。
运行流程
![a12c1e0a45ef99d13d50c99a8a7c56af.png](https://i-blog.csdnimg.cn/blog_migrate/dcd488e97004da0267276832fa97a8d7.jpeg)
二、数据结构
HashMap的数据结构采用了“用空间换时间”的思想来保证综合效率。
哈希表
哈希表是一个通过数组和链表相结合而成的数据结构,既避免了数组的增删慢,也避免了链表查询查询慢的的缺陷。
- 插入:通过hash算法计算在数组中的位置,然后插入。如果该位置已有元素,就生成一个链表,使用“头插法”插入元素到链表头(如果插在链表尾,需要先遍历链表,会提升时间复杂度)。
- 查询:通过索引算法算出位置,再遍历该索引上的链表。
- 扩容:数组内容超过负载因子,就使用扩容算法进行扩容。
![9ad09096a5038c790f11d5501d3ed69a.png](https://i-blog.csdnimg.cn/blog_migrate/c7e3e669fd41c1672f31acbadfa1577c.png)
红黑树
红黑树详情
为什么使用红黑树而不使用AVL树(平衡树)
- 1、红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。
- 2、平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
三、位运算
计算机中的数在内存中都是以二进制形式进行存储的,用位运算就是直接对整数在内存中的二进制位进行操作,因此其执行效率非常高,在程序中尽量使用位运算进行操作,这会大大提高程序的性能。而HashMap中也是使用位运算来实现算法的。
二进制转换
转换十进制和二进制非常简单,我们先用十进制的1234和二进制的1011来举例,看看两者的区别
十进制的位表示的倍数:
1(10的3次方)2(10的2次方)3(10的1次方)4(1)
1234=1*10的三次方+2*10的2次方+3*10+4*1
二进制的位表示的倍数:
1(2的3次方)0(2的2次方)1(2的1次方)1(1)
1011=1*2的三次方+0*2的2次方+1*2+1*1
二进制转换十进制:
1011=8+0+2+1
1011=11
十进制准换二进制:
1234/2=617, 余数为0
617/2=308, 余数为1
308/2=154, 余数为0
154/2=77, 余数为0
77/2=38, 余数为1
38/2=19, 余数为0
19/2=9, 余数为1
9/2=4, 余数为1
4/2=2, 余数为0
2/2=1, 余数为0
1/2=0, 余数为1
再从下往上数得出1234的二进制:10011010010
位运算符
- & 与运算 两个位都是 1 时,结果才为 1,否则为 0
1 0 0 1 1
& 1 1 0 0 1
————————————————
1 0 0 0 1
- | 或运算 两个位都是 0 时,结果才为 0,否则为 1
1 0 0 1 1
| 1 1 0 0 1
————————————————
1 1 0 1 1
- ^ 异或运算,两个位相同则为 0,不同则为 1
1 0 0 1 1
^ 1 1 0 0 1
————————————————
1 0 1 0 1
- ~ 取反运算,0 则变为 1,1 则变为 0
~ 1 1 0 0 1
————————————————
0 0 1 1 0
- << 左移运算,向左进行移位操作,高位丢弃,低位补 0
9 << 3;
移位前:0000 0000 0000 0000 0000 0000 0000 1001
移位后:0000 0000 0000 0000 0000 0000 0100 1000
- >> 右移运算,向右进行移位操作,对无符号数,高位补 0,对于有符号数,高位补符号位
9 >> 3;
移位前:0000 0000 0000 0000 0000 0000 0000 1001
移位后:0000 0000 0000 0000 0000 0000 0000 0001
四、算法
在HashMap中所有算法均使用的位运算,位运算的效率比运算符更高,除了省下二进制与十进制转换的时间,JVM在底层也对位运算进行了优化。
索引算法
计算索引的代码:
int index=h&(INITIAL_CAPACITY-1);
//假设INITIAL_CAPACITY为16
1001 1011 //155
& 1111 //16-1
——————————
1011 //直接舍弃前面N位,取后4位,特别的高效
将hash值与阈值进行位运算获得数组中的索引,当一个int值a是二的次幂的时候,h跟a-1进行与运算的时候,刚好是h % a,这是也是为什么HashMap的数组大小需要为2的整次幂的原因之一,也是导致hash冲突的原因(可能2个索引在一个位置)。
HashCode算法
在HashMap中采用了下图进行HashCode的运算,这样可以将hashCode的前后16位都充分利用。并且使用了异或运(其他的离散值为3/4,而异或为1/2)算来加大离散度(在哈希计算中,所有的操作都是为了加大离散度)。
h = hashCode ^ (hashCode >>> 16) //扰动函数
- 为什么已经有HashCode了,还要进行一次运算呢?
如果直接用HashCode来进行索引算法的话,那进行运算的无论前面的怎么变,只有后四位参与运算,这样就会产生大量的Hash碰撞
…… 1111 1111 0000 //任意结尾为0000的数与1111进行计算
…… 0111 0000 0000
…… 0000 0000 0000
& 1111
——————————————————
0000
根据上面的十进制转二进制算法,只要任何数,前4次除以2余数为0(例如大数值的偶数),都会碰撞,数组都给你撞烂。如果使用扰动函数进行扰动呢?
1001 0110 1111 1010 //前面缩略,总是就是前半部分和hashcode进行位运算
^ 1001 0110
———————————————————
0110 1100
& 1111
——————————————————
1100
如果还是跟1111与运算的话,一下就有8个数字参与了运算,如果再扩容一次,和11111运算,就有10个数字参与运算,大大减少碰撞几率。
扩容算法
- 初始化容量多少合适?
initialCapacity(需要存的元素个数/0.75)+1,默认为16。
- 为什么扩容大小要求为2的整次幂?
如果length为2的次幂 则length-1 转化为二进制必定是11111……的形式,在与hashCode的二进制与操作效率会非常的快,而且空间不浪费;如果length不是2的次幂,比如length为15,则length-1为14,对应的二进制为1110,在与hashCode与操作, 最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费
- 扩容算法是如何判定一个数是否需要移动到新位置
e.hash & oldCap == 0
//假设oldCap==16,现在有两个数都在同一个索引上
1111 1111
0000 1111
& 1111
—————————
1111 //原本都在1111这个位置上,但是经过一次扩容
1111 1111
0000 1111
& 1 0000 //跟oldCap进行与运算,判断是否该移动
———————————
1 0000 //第一个结果不等于 0 则不需要移动
0 0000 //第二个结果等于0需要移动
- 最大容量为多少
因为计算采用int值,int值最大为2的31次-1,达不到2的31次,所以为2的30次。
- 为什么负载因子是0.75
根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。