hashmap原理_HashMap的实现原理

11d9d1b533189f03c7cf360fffa95c0e.png

参考链接:

HashMap 的实现原理 - Java 集合学习指南 - 极客学院Wiki​wiki.jikexueyuan.com
85c85db39cc54198fb14ba52c9326a38.png

上面的链接介绍得非常清楚。建议看完本文后可以去看一下。

概述

HashMap是Java中对Map接口的实现类。是其最常用的实现类中之一。主要有以下几个特性:

  • HashMap中的key和value都允许为null,但最多只能拥有一个null的key。
  • HashMap不保证顺序。
  • HashMap非线程安全。

HashMap的数据结构

HashMap内部是以数组+链表的方式储存的数据。可以参考下图:

2f338df0a9bcbe24c3fe8d5e02fd7d7d.png

(图片来自上文提到的参考链接)

HashMap的数组中,每个元素称之为“”。需要注意的是,这个“桶”并不等同于“键值对(Entity)”。至于它是什么请往下看。

初始化

HashMap在初始化时会创建一个Entity的数组。其个数为16。其源码类似下面的代码:

static 

其中key和value不必多说,不过它还包含了一个next属性,这说明它可以组织成一个链表结构。

put()方法

put方法被调用时,HashMap会根据key计算出对应的hashcode,然后根据hashcode确定该Entity应该存放在数组的哪个位置(应该放在哪个桶里)。

这种设定有一个问题:实际引用中有可能会发生hash碰撞(即两个数据虽然内容不同,但其hashcode有可能是相同的)!因此,HashMap如果发现hashcode已经存在,则会对key进行euqals对比:

  • equals结果是true,则认为确实是同一个key,然后将新的value覆盖旧的value(此时put方法将会返回旧value值)。
  • equals结果是false,则认为是hash碰撞,此时会将之前的Entity作为新Entity的next,此时形成一个链表,新Entity则处在链表的首位。

因此,所谓的“桶”就是数组每个位置放置的“链表”

get()方法

如果理解了上述的put逻辑,则get方法就很容易理解。主要有以下几个步骤:

  • 根据key计算hashcode,然后得出其数组下标(位置)。
  • 去对应位置获取桶(链表)。
  • 从头到尾遍历链表的每一个Entity,通过equals方法找到对应的Entity。

上述的过程中有一个点未详细说明:如何根据key的hashcode计算出对应的数组坐标呢?

HashMap的内部实现用了一个非常巧妙的方法。HashMap的初始容量被定为16,且每次增长都是2的倍数。这样设计的目的是要保证存入map中的元素尽量分散,尽量避免出现桶中出现链表,这可以有效降低数据查询时的处理速度。

key是这么一步步转化成数组下标的:

第一步:Object Key --> int hash

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

如果key是null,则其hash为0;否则便将 hashcode hashcode的高位 异或运算。这是为了尽量避免“低位不变,高位变化”时造成的hash冲突。

第二步:hash --> i

上一步计算出的hash是个长度较长的二进制数字,而通常情况下HashMap的底层数组长度(length)较小,因此如果我们进行 hash % length 计算,则一定能得到一个下标,且相对比较分散。而在源码中使用了性能更高的算法:

i = (length - 1) & hash

这个公式对hash和length进行了按位与运算,等价于取余。

这时就能说清楚另外一件事情:为什么底层数组的长度总要是2的n次方呢?

下图是个示例:

fb3e7d4f4dfe8a4cb2212c1530737c0d.png

(图片来自文章开头提到的参考链接)

可以看到,如果数组长度是2的n次方,那么length-1的二进制表示中,一定所有位都是1,此时取&运算则可以完整保留hash响应位置的二进制数据。相反的,如果数组长度不是2的n次方,则出现hash碰撞的可能性大大提高。


JDK8中HashMap的改进

上文曾提到“”的概念。其实这个概念在JDK8中才是真正有意义的。因为JDK7中,原始数组的每个元素都一定是个链表(链表的节点一个或者多个),而到了JDK8的时候就不一定是链表了。

JDK8对存储方式进行改进的原因很简单:如果在一个HashMap中,有很多Key发生了碰撞的时候,就会产生一个超级长的链表。那么在数据查询的时候就会花费O(n)的时间。所以,JDK8中HashMap采用了“+链表/红黑树”数据存储方式:如果链表的长度大于等于8时,其内部便会将链表转化为红黑树的结构。红黑树的查询时间是O(log n)。

由于数据存储方式发生变化,因此列表扩容时也会发生一些变化。具体细节请看下一篇HashMap的扩容。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值