文章目录

  • 一、加载因子为什么是0.75
  • 二、为什么要无符号右移16位后做异或运算
  • 三、为什么槽位数必须使用2^n
  • 四、JAVA 8 HashMap改进
  • 五、HashMap性能问题

写在前面:无论在工作中和面试的时候都会遇到关于HashMap问题,这一篇文章我们主要从以下几个方面讲解HashMap。至于分析源码网上已经有很多这样的文章,所以这里就不在以源码为中心讲述。

HashMap数据结构
在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的键值对会被放在同一个位桶里,当桶中元素较多时,通过key值查找的效率较低。

而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8),时,将链表转换为红黑树,这样大大减少了查找时间。

一、加载因子为什么是0.75

先说结果:提高空间利用率和减少查询成本的折中,选择0.75作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择。

加载因子解释:表示Hash表中元素的填满的程度。加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了。反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了。冲突的机会越大,则查找的成本越高。反之,查找的成本越小。因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。

哈希冲突主要与两个因素有关

  • 填装因子,填装因子是指哈希表中已存入的数据元素个数与哈希地址空间的大小的比值,a=n/m ; a越小,冲突的可能性就越小,相反则冲突可能性较大;但是a越小空间利用率也就越小,a越大,空间利用率越高,为了兼顾哈希冲突和存储空间利用率,通常将a控制在0.6-0.9之间。
  • 与所用的哈希函数有关,如果哈希函数得当,就可以使哈希地址尽可能的均匀分布在哈希地址空间上,从而减少冲突的产生,但一个良好的哈希函数的得来很大程度上取决于大量的实践。

如果想继续了解如何解决hash冲突请看这篇文章:解决hash冲突的三个方法

以上是对加载因子为什么是0.75的解释下文将继续讲解为什么是0.75

HashMap有一个初始容量大小,默认是16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

为了减少冲突的概率,当hashMap的数组长度到了一个临界值就会触发扩容,把所有元素rehash再放到扩容后的容器中,这是一个非常耗时的操作。

而这个临界值由【加载因子】和当前容器的容量大小来确定:DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR ,即默认情况下是16x0.75=12时,就会触发扩容操作。

所以使用hash容器时尽量预估自己的数据量来设置初始值。具体代码实现自行去研究HashMap的源码。

基础知识补充完毕,回到正题,为什么加载因子要默认是0.75?

hashmap源码注释里找到了这一段

Ideally, under random hashCodes, the frequency ofnodes 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 resizing
threshold of 0.75, although with a large variance because of
resizing granularity. Ignoring variance, the expected
occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
factorial(k)). The first values are:
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million

注意http://en.wikipedia.org/wiki/Poisson_distribution链接中的关键字:Poisson_distribution 中文翻译为: 泊淞分布

简单翻译一下就是在理想情况下,使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素个数和概率的对照表。

从上面的表中可以看到当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。

重申一下使用hash容器请尽量指定初始容量,且是2的幂次方。

关于泊淞分布的知识请看:泊松分布和指数分布:10分钟教程

二、为什么要无符号右移16位后做异或运算

HashMap中哈希算法的关键代码

//重新计算哈希值
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//key如果是null 新hashcode是0 否则 计算新的hashcode
}//计算数组槽位static int indexFor(int h, int length) {return h & (length-1);
}

这段代码可以看到哈希算法的细节(h = key.hashCode()) ^ (h >>> 16)

^按位异或运算,只要位不同结果为1,不然结果为0;>>> 无符号右移:右边补0

根据上面的说明我们做一个简单演练

h=key.hashcode() 1111 1101 1101 1111 0101 1101 0010 1111
^
h >>> 16         0000 0000 0000 0000 1111 1101 1101 1111
--------------------------------------------------------
h^(h>>>16)       1111 1101 1101 1111 1010 0000 1111 0000
h=key.hashcode() 1111 1101 1101 1111 0101 1101 0010 1111

将h无符号右移16为相当于将高区16位移动到了低区的16位,再与原hashcode做异或运算,可以将高低位二进制特征混合起来

从上文计算可知高区的16位与原hashcode相比没有发生变化,低区的16位发生了变化

我们可知通过上面(h = key.hashCode()) ^ (h >>> 16)进行运算可以把高区与低区的二进制特征混合到低区,那么为什么要这么做呢?

我们都知道重新计算出的新哈希值在后面将会参与hashmap中数组槽位的计算,计算公式:(n - 1) & hash,假如这时数组槽位有16个,则槽位计算如下:

hash             1111 1101 1101 1111 1010 0000 1111 0000
&
16 -1            0000 0000 0000 0000 0000 0000 0000 1111
--------------------------------------------------------
(16 - 1) & hash  0000 0000 0000 0000 0000 0000 0000 0000

如果不了解与运算可以看这篇文章:与运算(&)、或运算(|)、异或运算(^)

而在这里采用异或运算而不采用& ,| 运算的原因是,异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向1靠拢,采用|运算计算出来的值会向0靠拢 。
接下来将得到的值与与0xf做&运算 目的是得到后四位的数值,得到后四位的下标在0~15之间 对应的放在哪个桶里面,当然这里存在扩容的问题,根据实际情况确定桶的个数。

三、为什么槽位数必须使用2^n

先说结果:为了让哈希后的结果更加均匀
这个原因我们继续用上面的例子来说明
假如槽位数不是16,而是17,则槽位计算公式变成:(17 - 1) & hash

hash1            1111 1101 1101 1111 1010 0000 1111 0000
&
17 -1            0000 0000 0000 0000 0000 0000 0001 0000
--------------------------------------------------------
(16 - 1) & hash  0000 0000 0000 0000 0000 0000 0001 0000hash1            1111 1101 1101 1111 1010 0000 1111 1010
&
17 -1            0000 0000 0000 0000 0000 0000 0001 0000
--------------------------------------------------------
(16 - 1) & hash  0000 0000 0000 0000 0000 0000 0001 0000hash1            1111 1101 1101 1111 1010 1101 1110 1010
&
17 -1            0000 0000 0000 0000 0000 0000 0001 0000
--------------------------------------------------------
(16 - 1) & hash  0000 0000 0000 0000 0000 0000 0000 0000

从上文可以看出,计算结果将会大大趋同,hashcode参加&运算后被更多位的0屏蔽,计算结果只剩下两种0和16,这对于hashmap来说是一种灾难

2、可以通过位运算e.hash & (newCap - 1)来计算,a % (2^n) 等价于 a & (2^n - 1) ,位运算的运算效率高于算术运算,原因是算术运算还是会被转化为位运算。

四、JAVA 8 HashMap改进

java8和java7最大的区别就是节点可以扩展到TreeNodes,TreeNode是一个红黑树结构,可以存储更多的信息。

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {final int hash; // inherited from Node<K,V>final K key; // inherited from Node<K,V>V value; // inherited from Node<K,V>Node<K,V> next; // inherited from Node<K,V>Entry<K,V> before, after;// inherited from LinkedHashMap.Entry<K,V>TreeNode<K,V> parent;TreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;boolean red;

使用红黑树的主要优点:在许多数据都位于内部表的同一索引(存储桶)中的情况下,在树中进行搜索花费的时间比链表时间短。
缺点:树比链接列表占用了更多的空间

通过继承,内部可以同时包含 Node(链接列表)和 TreeNode(红黑树)。Oracle决定使用以下规则使用这两个数据结构:

  • 如果内部表中给定索引(存储桶)的节点超过8个,则链表转换为一棵红黑树
  • 如果给定索引(存储桶) )内部表中的节点少于6个,树被转换为链表

五、HashMap性能问题

在正常情况下,get()和put()方法的时间复杂度为O(1)。但是,如果key分布不均,可能put()和get()调用非常慢。put()和get()的性能取决于将数据重新分配到内部数组(存储桶)的不同索引中。如果key的哈希函数使用不当,存储数据将会分配不均,调用put()和get()都会很慢,因为需要遍历整个列表。
key分布不均HashMap。

key分布均匀HashMap

在分布均匀的HashMap情况下,获得K将花费3次迭代。两个HashMap都存储相同数量的数据,并且具有相同的内部数组大小。

以下示例,创建了一个哈希函数,该函数将所有数据放入同一存储桶中,然后添加200万个元素。

public class Test {public static void main(String[] args) {class MyKey {Integer i;public MyKey(Integer i){this.i =i;}@Overridepublic int hashCode() {return 1;}@Overridepublic boolean equals(Object obj) {…}}Date begin = new Date();Map <MyKey,String> myMap= new HashMap<>(2_500_000,1);for (int i=0;i<2_000_000;i++){myMap.put( new MyKey(i), "test "+i);}Date end = new Date();System.out.println("Duration (ms) "+ (end.getTime()-begin.getTime()));}
}

在我的核心i5-2500k @ 3.6Ghz上,使用Java 8 需要超过45分钟(我在45分钟后停止了该过程)。

现在,如果我运行相同的代码,但是这次我使用以下哈希函数

  @Overridepublic int hashCode() {int key = 2097152-1;return key+2097152*i;
}

需要46秒

如果我使用以下哈希函数运行相同的代码,则可以提供更好的哈希重新分区

@Override
public int hashCode() {return i;
}

需要2秒钟。

使用HashMap时,尽可能使用合适的key,将key散列到尽可能多的存储桶中。字符串对象是一个很好的key,因为它具有良好的哈希功能。整数也很好,因为它们的哈希码是它们自己的值。

如果需要存储大量数据,则应为HashMap创建初始容量。

Map默认大小16,加载因子为0.75。第一个到第11个put()非常快,但是第12个(16 * 0.75)将重新创建一个新的内部数组(及其关联的链表/树),其新容量为32。第13个到第23个将很快,但是第24个(32 * 0.75)将从新扩容。在数量小的情况下,内部阵列的完全恢复速度很快,但在数量大的情况下,可能需要几秒钟到几分钟。通过设置map的大小,可以避免这些自动扩容带来的代价。

但是有一个缺点:如果您将数组大小设置得很高,例如2 ^ 28,而在数组中仅使用2 ^ 26个存储桶,则会浪费大量内存。

HashMap之三问为什么及性能问题相关推荐

  1. JVM&NIO&HashMap简单问

    JVM&NIO&HashMap简单问 背景:前几天在网上看到关于JVM&NIO&HashMap的一些连环炮的面试题,整理下以备不时之需. 一.JVM Java的虚拟机的 ...

  2. 时势下的HMS和GMS前世今生——今生篇之三问突破口

      谢HMS Core总体组运营邀请,分享个人的一些观点.   关于HMS突破口的讨论已有多名技术.营销专家.同事在内部掀起热烈讨论,虽然HMS和GMS的前世今生-- 前世篇整合了一些思考,但构建移动 ...

  3. 拜托别在问我 MySQL 性能优化了!

    来自:业余草 今天微信群里一位网友发了一个问题:"mysql 根据时间进行过滤,查询速度特别慢,需要 30 多秒".然后我问她,数据库中总数据量大概是多少,她告诉我 explain ...

  4. HashMap 21 问!

    [公众号回复"1024",送你一个特别推送] 9:20约会,真爱,请"星标" 1:HashMap 的数据结构? A:哈希表结构(链表散列:数组+链表)实现,结合 ...

  5. 数据分析师求职之三问三答

    互联网时代什么最值钱?有的人可能会回答人才.可是小编不完全赞同,小编认为人才加上数据分析的组合才是最有价值的.互联网时代是大数据的时代,数据分析师就是与数据打交道的,他们通过对数据采集.清洗.分析之后 ...

  6. C# JSon解析之三个库的性能对比

    Swifter.Json.System.Text.Json.NewtonJson(各自的简介和使用自行百度或看官方文档,)解析Json的性能对比(.Net5): 解析一个拥有59个属性的对象,Coun ...

  7. 拜托,别再问我数据库性能优化了!

    一.前言 在谈论数据库的时候,经常能够听到"QPS"."TPS"等词汇,其实吞吐量不过是数据库性能的呈现,对于数据库性能的本质,我更倾向于将其描述为响应时间量, ...

  8. [车联网安全自学篇] Android安全之三问为什么?APP加固原理以及加固技术基础知识解惑

    也许每个人出生的时候都以为这世界都是为他一个人而存在的,当他发现自己错的时候,他便开始长大 少走了弯路,也就错过了风景,无论如何,感谢经历 0x01 前言 Android APP的开发除了部分功能采用 ...

  9. 【转】HashMap,ArrayMap,SparseArray源码分析及性能对比

    HashMap,ArrayMap,SparseArray源码分析及性能对比 jjlanbupt 关注 2016.06.03 20:19* 字数 2165 阅读 7967评论 13喜欢 43 Array ...

最新文章

  1. Python两个字典键同值相加的几种方法
  2. 关于cocos2d-js中使用 ClippingNode 以及 BlendFunc 来实现遮罩
  3. Confluence 6 嵌套用户组的示例
  4. AJAX 必用的情况(待选........)
  5. 软件工程之图书管理系统总体设计
  6. 云台山风景美如画,四大网红打卡景点等你来!
  7. thinkphp mysql操作数据库_thinkPHP数据库操作
  8. 圆梦微软 — 旅游和入职体验
  9. 网易云音乐APP(基于APICloud平台)
  10. eclipse android模拟器 慢,android模拟器太慢怎么办?
  11. 20175208 实验一 Java开发环境的熟悉
  12. 一建报名条件是什么?
  13. 计算机给文件重命名快捷键,批量重命名文件 一个F2快捷键即可全部搞定
  14. shell脚本实践:自动清理文件,以时间方式形成路径的图片或者是Excel、pdf等文件
  15. 记一次云服务器配置mysql 远程连接失败的解决方案
  16. 数据权限设计:从RBAC到ABAC的演变
  17. 当前目录./和父目录../辨析
  18. Ubuntu16.04安装系统监控器System Monitor
  19. Centos执行shell命令返回127错误
  20. java并发-JUC

热门文章

  1. kaidi中 install cuda
  2. 重磅嘉宾公布,第四范式AI新品发布会进入报名倒计时
  3. 【NLP】完全解析!Bert Transformer 阅读理解源码详解
  4. 线性代数回顾.pptx
  5. 【励志】公子龙:我的工作状态和存款进度
  6. 【学术相关】IEEE TBD, 这个Trans刚被SCI收录,预计首个IF4
  7. 【深度学习】RetinaNet 代码完全解析
  8. ​【Python基础】告别枯燥,60 秒学会一个 Python 小例子(文末下载)
  9. 跟优秀的人一起进步:四月组队学习
  10. Github标星24.9k!适合初学者的有趣、入门级的开源项目