HashMap底层原理(精讲)
作者:互联网
这几天专门研究了一下HashMap 整理一下
位运算
讲HashMap之前先复习一下位运算
名称 | 符合 | 规则 |
---|---|---|
与 | & | 全1为1 其余为0 |
或 | | | 有1为1 其余为0 |
异或 | ^ | 不同为1 相同为0 |
左移 | << | 各二进位全部左移若干位,高位丢弃,低位补0 |
右移 | >> | 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移) |
存储需求
设计一个写字楼通讯录,存放所有公司的通讯信息
座机号码作为 key(假设座机号码最长是 8 位),公司详情(名称、地址等)作为 value
设计表如下
号码 | 公司详情 |
---|---|
1 | 公司1 |
12345678 | 公司2 |
88888888 | 公司3 |
99999999 | 公司4 |
那么这样会产生很严重的问题
在1和99999999中间需要建立非常大的空间 并且最后实际用到的空间只有几个
1.空间复杂度非常大
2.空间使用率极低,非常浪费内存空间
解决方案(哈希表)
哈希表 也称散列表
通过哈希函数 hash(key) 将key值转换成int类型的数据 存储在数组中 0~15
哈希碰撞(哈希冲突)
2个不同的key,经过哈希函数计算后得出相同的结果
key1 ≠ key2 hash(key1) = hash(key2)
解决哈希碰撞
jdk1.8之前的HashMap是用链地址法解决 存储结构也就是 数组+链表
jdk1.8之后的存储结构是 数据+链表+红黑树,因为用链表法解决哈希冲突会导致链接过长的时候查询速度非常慢,非常影响性能。所以当数组超过64且链表长度超过8后会将链表转换成红黑树。
当链表长度超过8之后,并不会直接将链表转换成红黑树,而是先判断数组的长度是否大于64,如果数组容量小于64,会先进行数组扩容操作,并分隔链表。
而当删除元素时,是链表长度小于6后将红黑树转换成链表。为什么这么做呢?因为如果超过8变成红黑树,小于8变成链表,会导致如果元素正好处于两者之间,不断的进行转换会导致资源消耗非常严重,产生哈希震荡。
注意是链表数大于8,也就是当链表加入第9个元素的时候才会树化
生成哈希值的方式(源码)
生成的思路:
先生成 key 的哈希值(必须是整数),再让key的哈希值跟数组的大小进行相关运算,生成一个索引值。
public int hash(Object key){
return hash_code(key) % table.length;
}
因为要得到一个(0~15)之间的索引 只需要将值模除16即可
但是CPU在处理取模运算时是很耗费性能的 所以考虑下面方案
使用&位运算取代%运算
前提:将数组的长度设计为2的幂
public int hash(Object key){
return hash_code(key) & (table.length-1);
}
容量大小可以不定义为2的幂吗? 可以 因为得到的索引也会处于数值之间
但是这样会产生很激烈的哈希冲突,空间利用率低,而且查询速度慢
如何生成哈希值
1. int类型
数值当做哈希值
比如10的哈希值就是10
public static int hashCode(int value){
return value;
}
2. 浮点数类型
将存储的二进制格式转成整数值
public static int hashCode(float value){
return floatToIntBits(value);
}
3. Long和Double类型
因为Long和Double是8个字节 占64位 而最终得到的hash值是int类型是4个字节 只有32位。所以要进行处理 将64位变成32位 如果不处理,最终值只是截取后32位 容易产生冲突。
public static int hashCode(long value){
return (int)(value^(value>>>32));
}
public static int hashCode(double value){
long bits = doubleToLongBits(value);
return (int)(bits ^ *(bits>>>32));
}
-
>>> 和 >> 的区别
Long的值>>>32 带符号位右移
Long的值 >>32 不带符号位右移
-
>> 和 ^ 的作用是?
高32bit和低32bit混合计算出32bit的哈希值
充分利用所有信息计算出哈希值
4. 字符串类型
首先考虑整数5489是如何计算出来的?
5*10^3 + 4*10^2 + 8*10^1 + 9*10^0
而字符串是由若干个字符组成的
比如字符串jack由j、a、c、k四个字符串组成(字符的本质就是一个整数)
因此jack的哈希值可以表示为
j*n^3 + a*n^2 + c*n^1 + k*n^0 等价 [(j*n+a)*n+c]*n+k
在JDK中,乘数n为31,为什么使用31呢?
31是奇素数,统计表明使用这个奇素数能让哈希分布更均匀,JVM会将31*i优化成(1<<5)-1
String string = "jack";
int hashCode = 0;
int len = string.length();
for(int i=0;i<len;i++){
char c = string.charAt(i);
hashCode = 31*hashCode+c;
}
System.out.println(hashCode);
System.out.println(string.hashCode());
5. 自定义对象
注意自定义对象一定要让所有的属性都参与到hash值的运算里面,尽量减少冲突。
public class Person{
private int age;
private float height;
private String name;
public Person(int age, float height, String name) {
this.age = age;
this.height = height;
this.name = name;
}
@Override
/**
* 用来比较2个对象是否相等
*/
public boolean equals(Object obj) {
// 内存地址
if (this == obj) return true;
if (obj == null || obj.getClass() != getClass()) return false;
// 比较成员变量
Person person = (Person) obj;
return person.age == age
&& person.height == height
&& (person.name == null ? name == null : person.name.equals(name));
}
//所有字段都需要参与到计算中来,参考String
@Override
public int hashCode() {
int hashCode = Integer.hashCode(age);
hashCode = hashCode * 31 + Float.hashCode(height);
hashCode = hashCode * 31 + (name != null ? name.hashCode() : 0);
return hashCode;
}
}
关于HashMap常见面试题 总结
谈一下hashMap中put是如何实现的?
- 先判断是不是第一次put元素,如果是,就先初始化hashmap,负载因子0.75,默认容量16,扩容阈值16*0.75=12。
- 如果索引位置为null,就把新元素放入到索引位置。
- 如果索引位置不为null,判断新旧元素的地址或者key是否相同,是否是一个元素,如果是,就把新元素的value替换旧元素的value。
- 如果索引位置的元素类型是treeNode,就进行红黑树操作。
- 如果索引位置的元素是链表,就在元素尾部增加新的节点。
- 如果增加的节点数量超过8个,并且数组的长度小于64,进行扩容。
- 否则,进行链表转红黑树的操作, 先把单向链表转换为双向链表,再转换为红黑树。
为什么重写equals,必须重写hashCode方法?
hashcode是依据内存地址存放元素的,如果不重写hashCode ,代码不稳定。
如果:
Persion p1=new Persion(18,"mao"); // hash 18 index 2
Persion p2=new Persion(18,"mao"); // hash 19 index 3
相同对象,存放的位置不是一个索引,也可能一个索引存放不同的对象。
如果:
Persion p1=new Persion(18,"mao"); // hash 18 index 2
Persion p2=new Persion(***,"mao"); // hash 18 index 2
size 大小, 可能等于1 也可能等于2
因此,如果改写了equals(),而不改写hashcode的话,Object内默认hashcode()方法必定不同的(new 出对象的地址一定不同),这样hashmap存储的2个对象,都在不同的链上,这样无法进行equals()比较。
谈一下hashMap中什么时候需要进行扩容,扩容resize()又是如何实现的?
扩容条件:
- map元素大于扩容的阈值
- 链表长度大于8并且数组长度小于64的情况下会扩容
扩容实现:
- 遍历所有旧数组
- 旧数组索引位置如果为null ,啥也不做
- 旧数组索引位置如果只有一个元素,重新计算hash值,把这个元素放入到新数组,相应索引的位置
- 旧数组索引位置是treeNode类型,就做红黑树的分割操作,红黑树也会分为低位树和高位树,如果树的节点数量低于6个,红黑树会转变为单向链表
- 旧数组索引位置是单向链表,把单向链表按照高位和低位重新建立两个链表,把首元素放入到对应的索引位置
为什么不直接将key作为哈希值而是与高16位做异或运算?
扰动一下,可以让所有的数据都参与到hashcode的计算中来,让hashcode分布更加均匀。
为什么是16?为什么必须是2的幂?如果输入值不是2的幂比如10会怎么样?
- 在hashMap中,会通过tableSizeFor方法,找到一个大于初始容量的2的倍数作为table容量.
- 如果不这么处理,数据分布会不均匀,出现漏位现象. 如果输入的是10 ,那么1.3.5.,,,, 奇数位永远不会被使用.
- 并且,使用2的幂次,可以用x&(n-1) 代替取模操作.提高效率
谈一下当两个对象的hashCode相等时会怎么样?
hashcode相同时,就是Hash碰撞.碰撞的hash 会存储在同一个桶位置. 如果数组小于64,链表小于等于8的长度,会使用单向链表结构,否则,使用红黑树结构.
如果两个键的hashcode相同,你如何获取值对象?
- 先判断索引处的元素与要获取的元素是不是一个元素,如果是,就返回这个对象
- 如果不是,判断索引处的元素的next是否为null
- 如果不为null,判断是否为treeNode,如果是就进行红黑树的查找
- 如果不是treeNode,那么就是链表,循环链表查找元素,返回元素.
如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
如果数组长度小于64,会触发扩容操作,超过64,会触发链表转换为红黑树的操作。但是注意并不是在数组中的元素超过负载因子才会扩容,而且所有元素加一起超过就会扩容,包括链表中的元素。
请解释一下HashMap的参数loadFactor,它的作用是什么?
loadFactor 是负载因子,表示节点数/hash表桶数,表示hashmap的拥挤程度.默认是0.75.
传统HashMap的缺点(为什么引入红黑树?)
红黑树的访问路径要短于双向链表. 效率比较高. 空间换时间.
单向链表的时间复杂度为O(n),而红黑树的时间复杂度O(logN),索引红黑树的效率更高一些.
标签:hash,HashMap,int,精讲,hashCode,链表,索引,哈希,底层 来源: https://www.cnblogs.com/Cloong/p/16515340.html