ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

HashMap底层原理(精讲)

2022-07-24 20:33:48  阅读:182  来源: 互联网

标签:hash HashMap int 精讲 hashCode 链表 索引 哈希 底层


这几天专门研究了一下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是如何实现的?

  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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有