一. 基本概念
首先HashMap是一种常用的数据结构,由数组和链表构成,
数组里面有很多K-V的实例,在java7叫entry,在Java8叫node,
本身所有的位置都为null,在put插入的时候会根据Key的的hash值去计算一个index值
二. 既然HashMap是由数组和链表构成的,那么链表是如何形成的?
不同的Key可能会通过hash函数映射为同一个index值,这样留形成了链表
三. 既然会形成链表,那么新的Entry节点是怎么插入的呢?
Java8之前是头插法,也就是说新来的值会取代原有的值,
因为最开始时认为后来的值被查找的可能性会大一些,可以提升查找效率
但是Java8之后就是尾插法了
四. 为什么Java8之后就改为尾插法了呢?原因涉及到了HashMap的扩容机制,那HashMap的扩容机制是什么样的呢?
首先提到HashMap是由数组和链表组成的,而数组的容量是有限的,因此到达一定的容量,就会扩容
扩容涉及两个因素:
1.Capacity : HashMap的当前长度
2. LoadFactor : 负载因子,默认值是0.75
也就是说,当存储容量达到Capacity*LoadFactor的时候,判断发现需要resize(扩容)
具体实现扩容分为两步;
1.扩容:创建一个新的数组,长度是原来的2倍
2.ReHash: 遍历原来的数组,将元素在此重新hash到新数组中
那么为什么需要ReHash呢?是因为hash函数,与数组长度有关,当长度改变后,相应的存储位置也会改变
五. 说完尾插法会涉及到的扩容机制,那么到底是什么原因改为了尾插法,和扩容机制又有什么关系呢?
场景例子:HashMap的数组大小是2,也就是存了1个元素,就要被扩容,假设在多线程情况下存储ABC
头插法不扩容是这样子的: A->B->C
头插法扩容情况下就变成了这样子:
这个时候就看出了问题,若使用头插法,扩容之前A指B,扩容之后B指A。也就是Infinite Loop(死循环)
那么尾插法为什么不会发生死循环呢?
是因为Java8之后链表有红黑树,除了将原本O(n)的时间复杂度变为了O(logn)外,还保持了节点之间的引用关系
六. 既然Java8之后的HashMap不会造成死循环,那么可以说它是线程安全的嘛?
当然是不可以的,因为源码中的put/get方法没有加锁,这样就会出现同步问题,即无法保证
上一秒put的值下一秒get到原值。
七. 源码的初始化大小是多少呢?是16,那为什么是16呢?
因为阿里巴巴规范插件赋初值最好是2的幂,这样是为了位运算,位与运算比算数计算的效率高了很多,
是为了实现均匀分布
八. 为什么重写equals方法时需要重写hashCode呢?
在Java中,所有的对象都是继承于Object类,这里面有两个方法,就是equals和hashCode,这两个方法都是用来比较两个 对象是否相等的。在未重写equals方法时,比较的是两个对象的地址,在HashMap中一个node下的链表地址是相同的,那 么 久无法保证具体找到某一个元素,办法就是通过重写equals方法和hashCode方法,以保证相同的对象返回相同的hash 值,不同的对象返回不同的hash值,相同的hash值情况下还能保证hashcode的不同
九. 怎么处理HashMap的线程安全的场景?
为保证线程安全一般要使用HashTable或者CurrentHshMap,
HashTable简单粗暴,直接在方法上上锁,并发度很低,最多同时允许一个线程访问,
CurrentHashMap是针对entry上的锁,这样并发度会提高很多
补充:
本篇内容借鉴了公众号: 三太子敖丙 内容诙谐幽默,简洁明白,向大家强烈安利!!
index的计算公式:index = HashCode(Key) & (Length- 1)