【Java集合类】HashMap(二)- 设计要点

本章将开始探讨JDK中的HashMap,包括HashMap如何避免和解决上一章所说的散列冲突问题,以及Java 8对HashMap的改进

避免散列冲突- 散列函数设计

String.hashcode()

Object.hashCode()方法用于返回当前对象的散列值。Object类中也约定了,重写了equals也要重写hashcode相同对象必须有相同哈希码。

以常见的String类的hashcode()为例,会有一个固定值31,在for循环中会结合每一位字符的ASCII码计算出最终的hashcode

char val[] = value;
for (int i = 0; i < value.length; i++) {
      h = 31 * h + val[i];
}

这里String用的哈希函数是一个叫做DJBX33A算法的变种

这里的乘子用一个固定值31的好处:

  1. 之所以使用 31, 是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)
  2. 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5)- i, 现代的 VM 可以自动完成这种优化。这个公式可以很简单的推导出来。
  3. 另外,一些实验证明,31是个不大不小的质数,碰撞概率很小

因此,String类hashcode的实现兼顾了散列函数的随机性和性能

扰动函数HashMap.hash()

假如用String作为hashmap的key,计算出hashcode后,并没有直接根据数组长度取模,而是先将key传入扰动函数hash()

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hashmap为什么使用扰动函数对hashcode的上半区和下半区异或:

  • 首先提一下,hashMap的长度必须是2的幂,这样hash%length就会转换为hash&(length-1),计算更加高效,
  • 而Java假设length一般很少超过2^16(65536),因此计算索引:hash&(length-1),hash值参与运算的部分一般只有低16位,导致高16位信息丢失,为了提高最终索引值随机性,让高16位也参与运算,扰动的办法就是hashcode高16位和低16位进行运算
  • 为什么用异或呢,因为用&或者|都会让结果趋向于0或1,反而不平均了
  1. 怎么验证随机性呢??用实验的方式,比如准备10万个单词,分别用加扰动和不加扰动的方式,插入到一个长度为128(2次幂)的hashmap中,看看每个索引下对应的单词数量。如果所有单词。可以画出折线图看看,如果所有单词在每个索引下分配的比较平均,就说明随机性强。随机性强也是为了尽量减少hash碰撞。

避免散列冲突-负载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

在HashMap中,负载因子决定了数据量多少了以后进行扩容。这里要提到上面做的HashMap例子,我们准备了7个元素,但是最后还有3个位置空余,2个位置存放了2个元素 所以可能即使你数据比数组容量大时也是不一定能正正好好的把数组占满的,而是在某些小标位置出现了大量的碰撞,只能在同一个位置用链表存放,那么这样就失去了Map数组的性能

要选择一个合理的大小下进行扩容,默认值0.75就是说当阀值容量占了3/4时赶紧扩容,减少Hash碰撞

解决散列冲突 - 链表 + 红黑树

HashMap解决散列冲突的方法是上一章中提到的拉链法(链表法)

在Java 8之前,HashMap只使用了链表来解决哈希冲突。

但是,由于链表长度增长过长会导致查找效率变低,因此在Java 8中添加了红黑树的支持来解决这个问题。当链表长度超过8时,HashMap将使用红黑树代替链表,这样能够提高HashMap的性能。

如果红黑树中节点的数量降到一定数量以下(Java 8中为6个),它将退化为链表。这是因为红黑树节点包含额外的指针和颜色标记,当节点数较少时,这些额外开销可能会变得更加显著,而链表占用的空间更少,也更容易遍历。

这种自动调整的特性使得HashMap能够在不同负载下保持良好的性能和空间使用率。

Java8对HashMap的修改

这部分将尽可能解释每种修改的背后原因

数据结构的改变

前面也提到过:Java8之前的数据结构数组+链表,而Java8之后是数组+链表/红黑树,显然是对于性能的考虑

更简单的hash()方法

前面我们提到过扰动函数能够让最终的hash值更加随机,但实际上Java8之前的扰动函数hash()更加复杂:

static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);

关于这一点改变我目前也没有在权威资料中查到原因,不过这篇回答中对此进行了一个合理猜测:

https://stackoverflow.com/questions/24673567/change-to-hashmap-hash-function-in-java-8

简单来说就是个tradeoff:更复杂的hash()方法、更随机的散列值 vs 更高效的hash()方法、更好的性能

有可能是官方发现后者带来的好处更大,所以放弃了更为复杂的hash()方法

其实回答里也提到了:假如key的hashcode()方法已经能生成高质量的散列值(例如String),那hash()方法再进行复杂运算其实是在浪费时间

新增节点的插入位置

新增一个节点时,Java8之前采用的是头插法,之后采用的是尾插法

采用头插法最大的问题就是在并发时,扩容可能产生环形链表,导致get()方法产生死循环,采用尾插法可以解决

其实头插法有一个好处:根据局部性原理越晚插入的元素越有可能被访问,因此头部插入有助于后续访问效率的提升
但是,如果是一个容量很大,并且长期存在于硬盘上的数据结构,这种局部性原理的效率提升更为明显。内存中短期使用的小容量数据结构,反而不太能借助局部性原理提升效率。

扩容方法

扩容时最直接影响效率的问题,就是需要把元素迁移到新的桶位中。

首先注意,表中每个节点都会保存一个hash值,这个值是经过扰动函数处理之后、取模之前的值,扩容时会用到这个值重新计算下标

拆分元素的过程中,原jdk1.7中会需要重新计算哈希值:**hash&(length-1),但是到jdk1.8中已经进行优化,不再直接取模计算,提升了拆分的性能,设计的还是非常巧妙的


请添加图片描述

对31取模保留低5位,对15取模保留低4位,两者的差异就在于第5位是否为1,是的话则需要加上增量,为0的话则不需要改变

参考资料

面经手册 · 第3篇《HashMap核心知识,扰动函数、负载因子、扩容链表拆分,深度学习》 | bugstack 虫洞栈

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容