HashMap 在 JDK1.7 和 JDK1.8 的区别

JDK1.8的HashMap引入了红黑树优化查询,当链表长度超过8时转换为红黑树,避免了链表过长导致的查询效率低下。1.7使用数组+链表结构,扩容时会颠倒链表顺序,而1.8保持原有顺序。1.8在计算哈希值时更加均匀,且扩容策略更为优化。

遇到的一个问题,之前没有好好思考过这个问题,现在研究一下

区别

  1. 最重要的一点是底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构;

  2. jdk1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;而1.8则是直接调用resize()扩容;

  3. 插入键值对的put方法的区别,1.8中会将节点插入到链表尾部,而1.7中是采用头插;
    1.7采用头插法,会引发环形链表死循环;1.8采用尾插法;

  4. jdk1.7中的hash函数对哈希值的计算直接使用key的hashCode值,而1.8中则是采用key的hashCode异或上key的hashCode进行无符号右移16位的结果,避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀;

  5. 扩容时1.8会保持原链表的顺序,而1.7会颠倒链表的顺序;而且1.8是在元素插入后检测是否需要扩容,1.7则是在元素插入前;

  6. jdk1.8是扩容时通过hash&cap==0将链表分散,无需改变hash值,而1.7是通过更新hashSeed来修改hash值达到分散的目的;

  7. 扩容策略:1.7中是只要不小于阈值就直接扩容2倍;而1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数不小于7就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容。

关于红黑树相关知识:
https://www.jianshu.com/p/e136ec79235c

JDK8中HashMap链表转红黑树的阈值为什么选8?为什么用红黑树做优化?

为什么会引入红黑树做查询优化呢?

在平常我们用HashMap的时候,HashMap里面存储的key是具有良好的hash算法的key(比如String、Integer等包装类),冲突几率自然微乎其微,此时链表几乎不会转化为红黑树,但是当key为我们自定义的对象时,我们可能采用了不好的hash算法,使HashMap中key的冲突率极高,但是这时 HashMap为了保证高速的查找效率,就引入了红黑树来优化查询了。

为什么树化的临界值为8?

源码中的介绍如下:

通过源码我们得知 HashMap源码作者通过泊松分布算出,当桶中结点个数为8时,出现的几率是亿分之6的,因此常见的情况是桶中个数小于8的情况,此时链表的查询性能和红黑树相差不多,因为转化为树还需要时间和空间,所以此时没有转化成树的必要。

既然个数为8时发生的几率这么低,我们为什么还要当链表个数大于8时来树化来优化这几乎不会发生的场景呢?

首先我们要知道亿分之6这个几乎不可能的概率是建立在什么情况下的 答案是:建立在良好的hash算法情况下,例如String,Integer等包装类的hash算法、如果一旦发生桶中元素大于8,说明是不正常情况,可能采用了冲突较大的hash算法,此时桶中个数出现超过8的概率是非常大的,可能有n个key冲突在同一个桶中,此时再看链表的平均查询复杂度和红黑树的时间复杂度,就知道为什么要引入红黑树了,

举个例子,若hash算法写的不好,一个桶中冲突1024个key,使用链表平均需要查询512次,但是红黑树仅仅10次, 红黑树的引入保证了在大量hash冲突的情况下,HashMap还具有良好的查询性能

### HashMapJDK1.7 JDK1.8 之间的实现差异 #### 数据结构 在 JDK1.7 中,`HashMap` 的底层数据结构是由数组加链表组成的。当发生哈希冲突时,所有的键值对会被存放在同一个桶中形成一条单向链表[^1]。 而在 JDK1.8 中,除了保留原有的数组加链表的数据结构外,还引入了红黑树的概念。如果某个桶中的链表长度超过了一个阈值(默认为 8),并且当前 `HashMap` 的大小超过了一定界限,则该链表会转换成红黑树,从而减少查找时间复杂度从 O(n) 到平均情况下的 O(log n)[^2]。 #### 初始化过程 对于初始化部分,在 JDK1.7 中,`table` 数组是在声明时被初始化为空数组,并且首次插入元素之前调用 `inflateTable()` 方法完成实际分配工作[^3]。然而到了 JDK1.8 版本里,“懒加载”的概念得到了应用——即只有当真正需要存储第一个键值对时才会触发真正的初始化操作;另外值得注意的是,默认情况下初始容量调整为了最接近指定容量的一个 2 的幂次方数值[^4]。 #### 扩容机制 关于扩容方面也有明显改变: - **JDK1.7**: 使用头插法来进行迁移旧有节点到新位置的过程当中可能出现循环链表的情况,进而导致死循环问题的发生概率增加。而且它的判断依据较为严格,需满足两个前提条件才能执行扩容动作:一是达到负载因子限制;二是目标索引处已有其他映射项存在。 - **JDK1.8**: 改进了这一点采用了尾插法避免上述风险的同时简化逻辑只依赖单一条件即可触发改动 —— 即每当新增后的总数量超出临界点便会立即启动扩展流程。此外它利用位运算技巧快速定位每一个条目应该迁移到的新地址[(e.hash & oldCap)==0][^3]。 #### Hash 计算方式 两代产品间另一个重要差别体现在如何计算散列码之上: - **JDK1.7** 定义了一种相对复杂的扰动函数用来进一步打乱原始hashCode分布特性以期获得更加均匀的结果集[^4]: ```java static final int hash(int h){ h ^= (h >>> 20) ^ (h >>>12); return h^(h >>> 7) ^ (h >>> 4); } ``` - **JDK1.8** 对此进行了优化精简版本如下所示不仅减少了不必要的移位次数同时也兼顾效率与效果平衡考虑特别针对null关键字做了特殊处理直接返回零值作为对应hashcode: ```java static final int hash(Object key){ int h; return (key == null) ? 0 : (h = key.hashCode())^(h >>> 16); } ``` #### Null 键的支持 无论是哪个版本都允许最多只有一个Null Key 存在于整个Map实例之中不过具体表现形式略有出入: - 在早期设计(JDK1.7)里面如果遇到这样的特殊情况则专门开辟首个槽位留给它们单独使用而不参与常规路径匹配流程. - 后续更新至最新标准后(NKD1.8),尽管依旧保持相同行为模式但内部实现细节有所调整现在即使NULL也可以像普通对象那样经历完整的HASHING步骤只不过最终得出固定常量ZERO而已因此理论上讲二者并无本质区别仅限表达手法上的转变罢了[^3]. ```python # 示例代码展示两种不同版本下 NULL KEY 插入情形对比模拟演示片段 map_17.put(None,"value") # JDK1.7 方式可能直接放置 index=0 处理 map_18.put(None,"another value") # JDK1.8 经过 HASH 函数作用同样指向 index=0 结果一致 ``` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值