HashMap的那些事儿

一、疑问

为什么要学HashMap?HashMap 的底层原理?HashSet为何不能有重复的元素(HashSet底层就是HashMap)?如果你也有上述疑问,相信此文能给你一点帮助。

二、为什么要学HashMap

HashMap说到底也是一个存储数据的东西。对数据操作无非就是增删改查,对于增删需求大的业务我们可以利用链表存储数据,对于查改需求大的业务我们可以利用数组。这两种数据结构已经可以互补了,为什么还要一个HashMap呢。HashMap存储的是k-v集合,根据key就可以获得相应的value,HashMap与前两者不同。使用起来很方便,也很常用,各语言都有自己实现k-v的形式,比如java用的HashMap,redis用string存储k-v。也就是从结果导向来看,不学HashMap就out啦。

三、HashMap的底层原理

3.1 存储结构

首先我们看看HashMap是如何存储数据的。在jdk8之前的HashMap利用数组+链表存储,jdk8开始利用数组+链表+红黑树存储。如图

image.png

其中每一个框都是一个Node结点。

3.2 存储逻辑

put一个k-v时,HashMap会先利用k的hashcode计算这个k-v应该放在数组的哪一个位置。如果该位置有了元素,就比较新加入的元素和已存在的是否相同(如何判断相同,稍微复杂,后面看源码),相同则更新value值,不同则挂到后面形成链表。如图

image.png

其实HashMap一般情况下是Node数组中的元素大于链表中的元素,因为每次添加都是通过key的哈希值确认数据存放的位置。每次哈希算法的结果一般不同。而链表的存在就是解决哈希碰撞。即如果两个不同对象计算的哈希相同了,那么就把新添加的对象挂在后面,也就是拉链法。而红黑树的加入是jdk8为了改进HashMap查询效率,在链表长度>=8且数组长度>=64时,对该链进行树化。红黑树比单纯的链表查询效率要更快不少。

3.3 debug验证

3.3.1先模拟正常情况,向map中添加3个猫,和对应的年龄

public class HashMapTest {
    public static void main(String[] args) {
        Cat cat01 = new Cat("咪咪");
        Cat cat02 = new Cat("喵喵");
        Cat cat03 = new Cat("呱呱");
        Map<Cat,Integer> map = new HashMap<>();
        map.put(cat01,1);
        map.put(cat02,2);
        map.put(cat03,3);
        System.out.println(map);

    }
}
class Cat{
    private String name;
    public Cat(String name) {
        this.name = name;
    }
}
复制代码

image.png 这里补充一个概念:桶,其实就是在数组中的位置,可以理解为索引。但是HashMap中叫桶bucket,更加形象,因为这个位置后面可以挂链表,就感觉这几个元素都在一个桶里。

image.png

3.3.2 模拟存在Hash冲突

class Cat{
    private String name;

    public Cat(String name) {
        this.name = name;
    }
    //我们只要重写一下Cat的hashCode方法返回一个固定的值,这时HashMap每次调用key的hashCode返回的就都是同一个值了。达到哈希冲突的效果
    @Override
    public int hashCode() {
        return 100;
    }
}
复制代码

调试查看结果:发现三只猫的hash值都是100,而且都在4号桶中。

image.png 到这我们验证了:当存在hash冲突时,确实会把元素挂在已存在元素的后面。而且因为上面例子中的三只猫都不同,所以三只猫都成功添加了。

3.3.3 key的比较

上面三只猫分别是咪咪,喵喵和呱呱。那么如果我把 呱呱 也改成 咪咪。能否添加成功呢?验证:

image.png 发现三只猫还是进来了。眼尖的小伙伴可能已经发现了,cat01cat03本来就不是同一个对象,指向的内存空间不一样,当判断是否相同时,默认调用的是Object的equals,它默认是比较两个对象的内存地址。所以HashMap会认为cat01,cat03是不同的猫。明白这一点,我们只要重写equals方法就可以实现,当两只猫的名字相同时,就认为两只猫是同一只猫了。

class Cat{
    private String name;

    public Cat(String name) {
        this.name = name;
    }
    @Override
    public int hashCode() {
        return 100;
    }

    //重写equals方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Cat cat = (Cat) o;
        return Objects.equals(name, cat.name);
    }
}
复制代码

验证:

image.png 重写equals方法后,我们发现只添加了两只猫;而且原本cat01的value是1,现在变成cat03的value了。因为HashMap认为这是同一只猫,所以对cat01的value值进行了更新。至此我们验证了HashMap的存储逻辑,接下来我们看看HashMap是怎么实现的.

3.4 源码分析

3.4.1 第一次添加的流程

image.png 这里可以发现put()底层调用的是putVal(),可知putVal()才是真正添加元素的方法;这里还调用了hash()方法,求key的hash值,并传给putVal() image.png 接下来看putVal()方法,由于是第一次添加,满足前两个if,而else中的语句不会执行。先分析第一个if,table是HashMap中的成员属性,第一次添加它是null。putVal()中定义了一个临时数组tab也指向了table,第一次为空,length也=0。故满足第一个if,那么进入resize()扩容(这个方法后面再讲,这里知道它是扩容就行)。然后进入第二个if判断,p=tab[i = (n-1)&hash]拆开看就是i=(n-1)&hash,p = tab[i],i=(n-1)&hash就是利用hash值,查到新增的元素应该放到哪个桶中(为什么要这么找桶,后面再讲,这里知道p指向的是table数组中桶的位置就行),第二if就是判断p指向的这个桶是不是空桶,是则把新增的元素放到这个空桶里。由于我们是第一次添加,所有的桶都是空的没有数据。到此第一个元素添加成功。

image.png

3.4.2 第二次添加

put()方法就不看了,它的作用就是给putVal()传key对应的hash值。

image.png 第二次由于我们模拟了hash碰撞,因此会进入到else中。进入else后首先要判断新增的元素和桶中的元素是不是相等。然后判断这个桶里的元素是不是树化了。都不是的话就进入循环遍历桶中的每一个元素,如果都不同的话,那么就把这个新增的元素插入到最后面。

image.png

3.4.3 第三次添加

这次添加,由于我们重写equals方法,cat03和cat01的名字相同,HashMap就会认为这是同一只猫。故不会把cat03做为新元素添加进来。

image.png 然后进入个判断,如果是相同的key,那么更新value值,并返回旧值。 image.png

猜你喜欢

转载自juejin.im/post/7107887621308694536