其他分享
首页 > 其他分享> > HashMap底层原理(精讲)

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));
}

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是如何实现的?

  1. 先判断是不是第一次put元素,如果是,就先初始化hashmap,负载因子0.75,默认容量16,扩容阈值16*0.75=12。
  2. 如果索引位置为null,就把新元素放入到索引位置。
  3. 如果索引位置不为null,判断新旧元素的地址或者key是否相同,是否是一个元素,如果是,就把新元素的value替换旧元素的value。
  4. 如果索引位置的元素类型是treeNode,就进行红黑树操作。
  5. 如果索引位置的元素是链表,就在元素尾部增加新的节点。
  6. 如果增加的节点数量超过8个,并且数组的长度小于64,进行扩容。
  7. 否则,进行链表转红黑树的操作, 先把单向链表转换为双向链表,再转换为红黑树。

为什么重写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()又是如何实现的?

扩容条件:

  1. map元素大于扩容的阈值
  2. 链表长度大于8并且数组长度小于64的情况下会扩容

扩容实现:

  1. 遍历所有旧数组
  2. 旧数组索引位置如果为null ,啥也不做
  3. 旧数组索引位置如果只有一个元素,重新计算hash值,把这个元素放入到新数组,相应索引的位置
  4. 旧数组索引位置是treeNode类型,就做红黑树的分割操作,红黑树也会分为低位树和高位树,如果树的节点数量低于6个,红黑树会转变为单向链表
  5. 旧数组索引位置是单向链表,把单向链表按照高位和低位重新建立两个链表,把首元素放入到对应的索引位置

为什么不直接将key作为哈希值而是与高16位做异或运算?

扰动一下,可以让所有的数据都参与到hashcode的计算中来,让hashcode分布更加均匀。

为什么是16?为什么必须是2的幂?如果输入值不是2的幂比如10会怎么样?

  1. 在hashMap中,会通过tableSizeFor方法,找到一个大于初始容量的2的倍数作为table容量.
  2. 如果不这么处理,数据分布会不均匀,出现漏位现象. 如果输入的是10 ,那么1.3.5.,,,, 奇数位永远不会被使用.
  3. 并且,使用2的幂次,可以用x&(n-1) 代替取模操作.提高效率

谈一下当两个对象的hashCode相等时会怎么样?

hashcode相同时,就是Hash碰撞.碰撞的hash 会存储在同一个桶位置. 如果数组小于64,链表小于等于8的长度,会使用单向链表结构,否则,使用红黑树结构.

如果两个键的hashcode相同,你如何获取值对象?

  1. 先判断索引处的元素与要获取的元素是不是一个元素,如果是,就返回这个对象
  2. 如果不是,判断索引处的元素的next是否为null
  3. 如果不为null,判断是否为treeNode,如果是就进行红黑树的查找
  4. 如果不是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