HashMap底层的深入分析

·  阅读 677
HashMap底层的深入分析

你知道的越多,你不知道的越多


点赞再看,养成习惯


欢迎实名diss建议,希望我们一起有点东西

复制代码

前言


相信hashMap对每个同行人来说,都不陌生,工作中都会有用到。

知识体系的广度决定你能走多远,而深度决定你能走多高。

如果你不了解底层原理!如果你想面试大厂!如果你想深度学习!

请往下看:
复制代码

HashMap的历史演变:

  • 在Java7的时候hashMap的底层采用的是数组+链表实现的。

  • java8则采用的是数组+链表+红黑树实现。


从而引入一个BATJ的面试问题,hashMap的实现原理?


hashMap源码里有两个很重要的属性,一个是初始容量(默认16),一个是负载因子(默认0.75),
复制代码

我们在new一个hashMap对象的时候,实际上底层会默认分配一个大小为16的静态数组arr,我们调用put(key,value)方法的时候,会使用^(异或)运算计算出key的hash,然后通过hash与arr-1进行与运算(hash & (arr.length-1))得到要放入的数组的下标index,如果arr[index]!=null,此时,单向链表就出现了,java7采用的是头插法,java8采用的是尾插法。


什么是头插法?什么是位插法?


java7会采用头插法插入链表中,而java8采用的是尾插法,在并发的时候,多个线程同时调用put方法,头插法则会出现环形链表,造成程序死循环,从而导致堆空间内存溢出,抛出异常。

所谓头插法,简单的说其实就是后面插入的结点作为链表的头结点。尾插法则是后面插入的元素作为链表的尾部结点。

对于hashMap而言,当数据量达到负载容量,会进行扩容操作,而头插法则会导致新的链表和旧的链表是反向的
复制代码

hashMap的扩容机制


因为hashMap的数组容量是有限的,所以不可避免的存在扩容机制,也就是resize()方法。

当数组中已有数据结点的容量>=初始容量*负载因子的时候,hashMap会自动扩容。
默认是将数组的容量扩大为原来的两倍,遍历原来的数组,重新进行hash运算,然后通过得到的新的hash,插入到新的数组中。

HashMap在JDK1.8及以后的版本中引入了红黑树结构,若桶中链表元素个数大于等于8时,链表转换成树结构;
若桶中链表元素个数小于等于6时,树结构还原成链表。
复制代码


什么是hash碰撞呢?


所谓hash碰撞,其实就是通过hash算法计算出的hash值相等


hash碰撞有什么坏处?


就HashMap而言,如果发生hash碰撞的次数过多,在java7的时候会导致某一条单链表上数据量过多,
导致查询的效率很低,java8的时候其实情况相对来说好很多,
因为使用了红黑树,二分法查询的效率会快很多,不管怎样,多多少少还是会降低查询的效率。
复制代码

hashMap的作者为什么选择16作为初始容量?为什么选择0.75作为负载因子?


假设h=0000 0001
如果初始容量为15,  length-1=14, 转换成二进制为:0000 1110index = 0000 0000

如果初始容量为16,  length-1=15, 转换成二进制为:0000 1111index = 0000 0001

结论:
为了减少hash碰撞,我们希望hash均匀分布,即hashCode(key)均匀分布,当length为2的等幂次方的时候,转换成二进制之后后几位都是1,这样就可以让index的结果取决于hashcode了,只要输入的hashCode本身就是均匀的,index就是均匀的

根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。
复制代码


hashMap是线程安全的吗?如果不是,多线程情况下有何解决方案?


通过源码,我们可以知道,hashMap没有任何锁的实现,所以hashMap不是线程安全的。如果想要实现线程安全,有以下解决方案:

1.hashTable替代hashMap,hashTable的底层是有加syschronized关键字的,即上锁,只允许单个线程访问。(效率较低)

2.采用collections.syschronizedHashMap(),(效率较高)


hashMap为什么要重写equals()方法?


因为在java中,所有的对象都是继承于Object类。Ojbect类中有两个方法equals、hashCode,这两个方法都是用来比较两个对象是否相等的。 在未重写equals方法我们是继承了equals方法,那里的 equals是比较两个对象的内存地址,显然我们new了2个对象内存地址肯定不一样。 然而hashMap通过比较key是否相等,是通过hash值+key去比较的,所以,需要去重写equals。


能否手写hashMap()?能否手写红黑树?(对!你没看错!手写红黑树!)


由于近期时间的原因,实在抽不出时间写了,网上有很多资料,包括AVL树,红黑树,b树,b+树,b*树... 下一篇文章会一一实现各种树。

总结

在大厂面试中,HashMap的底层绝对是要烂熟于心的

通过一个hashMap,我们可以衍生出hashTable,TreeMap,concurrentHashMap等等一系列底层实现。

知其然且知其所以然,让我们一起有点东西。复制代码
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改