HashMap源码解析

一、前言

HashMap这个数据结构,不管是在实际应用中还是面试中,都是很重要的知识点。例如:在实际应用中,我们需要在O(1)时间内查找到相关的元素,假如遍历数据找其相关元素,那时间复杂度就是O(n)了,不符合我们需要在O(1)时间复杂度里找到其元素,这时我们就想到用HashMap这个数据结构来进行查找,通过匹配key和value值,可以在O(1)时间内快速找到。再比如,在面试中,面试官对HashMap底层原理会进行全方位的考察,如:HashMap 1.7,1.8底层原理有什么区别?扩容机制?线程是否安全?等等。。。

所以,下面我会对HashMap的底层原理,做一个全方位的解析。

二、正文

1.HashMap底层的数据结构是什么样的?

Java7底层的数据结构是:数组+链表,Java8之后底层的数据结构是:数组+链表+红黑树。

数组里面存储的是key-value这样的实例,在Java7中叫Entry,在Java8中叫Node。大概如下:
在这里插入图片描述

因为本身所有的位置都为null,在put插入的时候会根据key的hash算出一个index。

比如:put一个(“小周”,20),就是插入“小周”这个元素,通过hash算出对应的位置,比如是index=1,如下:
在这里插入图片描述

2.那都有数组了,为啥还要用链表呢?

我们知道数组的长度是有限的,在有限的长度里使用hash,就有概率会发生哈希冲突,比如说”小周“和”周小“就有一定概率在hash后,得到相同的index值,这样就会形成链表,如下:

在这里插入图片描述

每一个节点都会保存自身的hash、key、value、以及下一个节点,我们来看下Node的源码:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    ...
}

3.说到链表,那新的Entry节点是怎么插入到链表中的呢?

Java8之前是头插法,也就是新来的节点会取代原有的值,原有的值就顺序推到链表中去,就像上面的例子一样,因为写代码的作者认为后来的值被查找到的概率大一点,提升查找的效率。

但是Java8之后,都用了尾部插入发。

4.为什么要用尾插法?

首先在解释这个问题前,我们需要先了解下HashMap扩容机制,也就是在数组容量到达一定数量时,会进行resize。

5.什么是resize?

有两个因素:

  • Capacity:HashMap当前长度

  • LoadFactor:负载因子,默认值0.75f

在这里插入图片描述

怎么理解呢,就比如当前容量大小为100,当你存入第76个时,判断发现要进行resize,那就进行扩容,但是HashMap扩大容量也不是这么简单的。

6.怎么扩容?

分两步:

  • 扩容:创建一个空的Entry数组,长度为原来数组的2倍
  • reHash:遍历原来的Entry数组,把所有的Entry重新hash到新数组中

7.为什么要重新hash,直接复制过去不行嘛?

因为扩大长度后,Hash规则也发生了变化。

Hash公式:index = hashcode & (length - 1),原来的长度(length)是8,假如得到的index是1,但是长度增大到16,得到的index可就不是原先的值了。

8.回到之前说的,Java8为什么改为尾插法?

Java7在多线程操作hashMap时可能会引起死循环,原因是在扩容转移后,前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。

Java8在同样的条件下不会引起死循环,因为扩容后,前后链表的顺序没有改变,保持了之前节点的引用关系。

9.HashMap初始长度是多少?

从jdk1.8源码可以看到,初始化大小是1 << 4 ,也就是16。

在这里插入图片描述

10.为什么初始化大小是16?

因为16是2的4次幂,也就是是2的n次幂形式。之所以要是2的n次幂的形式,是因为:

(1) 因为扩容公式是:hashcode & (length - 1),其实他就是取模运算,Java使用位运算(&)代替取模运算(%),最主要的是考虑效率问题。

位运算(&)要比取模运算(%)效率高很多,主要因为位运算是对内存操作,不需要转化为十进制,因此处理速度快。

(2) 只要保证长度(length)是2的n次幂,然后通过扩容公式是:hashcode & (length - 1),就可以实现取模运算。

11.为什么在重写equals的时候,要重写hashcode? 能用hashMap举个栗子嘛?

因为在Java中,所有的对象都是继承Object类,Object类有两个方法equals和hashcode,这个两个方法用来比较两个对象是否相等。

在未重写equals方法时,我们继承的是Object的equals方法,那里的equals比较的是两个对象的内存地址,显然new了两个对象的内存地址是不一样的。

  • 对于值对象,==比较的是两个对象的值
  • 对于引用对象,比较的是两个对象的地址

大家还记得我之前说的hashMap是通过key的hashcode去寻找index的,那index就形成一个链表,也就是说”小周“和”周小“的index都可能是2,在一个链表上。

当我们去get的时候,他就根据key的hash计算出index,那我们如何确定是”小周“还是”周小“呢?

equals!是的,所以如果对equals方法重写,建议一定要重写hashcode,以保证相同的对象返回相同的hash值,不同的对象返回不同的hash值。

12.那如何重写equals和hashcode方法呢?

这里举个栗子:

比如有一个Article类,其中包含一个url属性。

下面重写url的equals()和hashcode()方法:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    
    Article article = (Article)o;
    return url != null ? url.equals(article.url) : article.url == null;
}
@Override
public int hashCode() {
    return url != null ? url.hashCode() : 0;
}

13.hashMap是否线程安全?

线程不安全。通常我们会使用HashTable或者CurrentHashMap,但是前者由于并发度问题,基本没有什么使用场景,所以在线程不安全的场景中我们会使用CurrentHashMap。

HashTable我看过它的源码,很简单粗暴,直接在其方法中加锁,并发度很低,最多同时允许一个线程访问,currentHashMap就好很多了,1.7和1.8有较大的不同,不过并发度比前者好很多,currentHashMap源码分析会在之后介绍。

在这里插入图片描述

14.HashMap 1.8什么时候会把链表转为红黑树?

当链表的长度大于等于8时,会自动转为红黑树。为什么?

因为这是基于统计的结果(源码中有描述),根据泊松分布,在负载因子默认为0.75时,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转化,小于等于6的时候就转化为链表。

15.为什么一开始不使用红黑树?反而要经历一个转化的过程?

通过查看源码可以发现,默认链表长度到达8的时候就转为红黑树,而当长度降到6时,转会链表,这体现了时间和空间平衡的思想。最开始使用链表的时候,由于链表比较短,空间占用比较少,所以查询时间没有太大问题。当链表越来越长的时候,用红黑树可以保证查询效率。通过源码可以,这个阀门值是8,当大于等于8的时候转为红黑树,小于等于6的时候转会链表。

16.HashMap、TreeMap、LinkedHashMap区别?

在对这三个集合遍历时:

(1)HashMap里面存入的键值在取出的时候是随机的,它根据键的hashcode值存储数据,根据键可以指直接获取它的值,具有很快的访问速度。在Map中插入、删除和定位元素,HashMap是最好的选择。

(2)TreeMap取出的是按自然顺序(比如:a、b、c…)排序后的键值对。

(3)LinkedHashMap是HashMap的一个子类,取出的顺序和输入的顺序是相同的。

三、总结:

HashMap不管是在实际开发中还是在面试的时候,都是很常用的数据结构,我们不能光只是会调用API,而是应该知道其底层原理,应知其然知其所以然,学习其源码的设计思想,这会潜移默化的改变我们以后设计代码的思路,让我设计出更好更高效的代码,并且在遇到问题的时候快速定位问题所在。不要只做CRUD boy or girl啊,这样又如何提升我们自己呢。

最后引用我很佩服的一个人经常说的话:你知道的越多,你不知道的越多!

文章参考:

https://mp.weixin.qq.com/s/0Gf2DzuzgEx0i3mHVvhKNQ

https://mp.weixin.qq.com/s/ktre8-C-cP_2HZxVW5fomQ

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值