搞定HashMap

news/2024/9/22 12:01:55/

搞定HashMap

1.Map是个啥?

HashMap隶属于Java中集合这一块,我们知道集合这块有list,set和map,这里的HashMap就是Map的实现类,那么在Map这个大家族中还有哪些重要角色呢?

在这里插入图片描述

上图展示了Map的家族,都是狠角色啊,我们对这些其实都要了解并掌握,这里简单的介绍下这几个狠角色:

TreeMap从名字上就能看出来是与树有关,它是基于树的实现,而HashMap,HashTable和ConcurrentHashMap都是基于hash表的实现,另外这里的HashTable和HashMap在代码实现上,基本上是一样的,还记得之前在讲解ArrayList的时候提到过和Vector的区别嘛?这里他们是很相似的,一般都不怎么用HashTable,会用ConcurrentHashMap来代替,这个也需要好好研究,它比HashTable性能更好,它的锁粒度更小。

简单来说,Map就是一个映射关系的数据集合,就是我们常见的k-v的形式,一个key对应一个value,大致有这样的图示

在这里插入图片描述

2.HashMap是个啥?

建议了解了哈希表之后再学习HashMap,这样很多难懂的也就不那么难理解了,参考哈希表部分

对于HashMap来说,底层也是基于数组实现,只不过这个数组可能和你印象中的数组有些许不同,我们平常整个数组出来,里面会放一些数据,比如基础数据类型或者引用数据类型,数组中的每个元素我们没啥特殊的叫法,在jdk1.7及之前叫做Entry,而在jdk1.8之后人家又改名叫做Node

3.HashMap初始化大小是多少

先来看HashMap的基础用法:

HashMap map = new HashMap();

就这样,我们创建好了一个HashMap,接下来我们看看new之后发生了什么,看看这个无参构造函数吧

public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

解释下新面孔:

  1. loadFactor :负载因子,之前聊哈希表的时候说过这个概念
  2. DEFAULT_LOAD_FACTOR :默认负载因子,看源码知道是0.75

很简单,当你新建一个HashMap的时候,人家就是简单的去初始化一个负载因子,不过我们这里想知道的是底层数组默认是多少嘞,显然我们没有得到我们的答案,我们继续看源码。

在此之前,想一下之前ArrayList的初始化大小,是不是在add的时候才创建默认数组,这里会不会也一样,那我们看看HashMap的添加元素的方法,这里是put

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}

这里大眼一看,有两个方法;

  1. putVal 重点哦
  2. hash

这里需要再明确下,这是我们往HashMap中添加第一个元素的时候,也就是第一次调用这个put方法,可以猜想,现在数据已经过来了,底层是不是要做存储操作,那肯定要弄个数组出来啊,好,离我们想要的结果越来越近了。

先看这个hash方法:

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

记得之前聊哈希表的时候说过,哈希表的数据存储有个很明显的特点,就是根据你的key使用哈希算法计算得出一个下标值
而这里的hash就是根据key得到一个hash值,并没有得到下标值哦,重点要看这个putVal方法,可以看看源码:

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;.......

首先Node<K,V>[] table就是HashMap底层的那个数组,其次这个resize()方法主要作用就是初始化数组大小以及将来数组的扩容,看看resize()方法的源码中,有这么一句

newCap = DEFAULT_INITIAL_CAPACITY;

有这么一个赋值操作,DEFAULT_INITIAL_CAPACITY字面意思理解就是初始化容量啊,是多少呢?

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  

这里是个左移位运算,就是16,现在已经确定默认容量是16了,那具体在哪创建默认的Node数组呢?继续往下看源码,有这么一句

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

ok,到这里我们发现,第一次使用HashMap添加数据的时候底层会创建一个长度为16的默认Node数组。

4.hashmap是如何减少hash冲突的?

HashMap 中对 key 做 hash 处理时,做了什么特殊操作?为什么这么做?首先我们知道 HashMap 在做 put 操作的时候,会先对 key 做 hash 操作,直接定位到源码位置

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

可以看到再对 key 做 hash 操作时,执行了 (h = key.hashCode()) ^ (h >>> 16)

原 hashCode 值: 10110101 01001100 10010101 11011111
右移 16 位后的值: 00000000 00000000 10110101 01001100 
异或后的值:      10110101 01001100 00100000 10010011

这个操作是把 key 的 hashCode 值与 hashCode 值右移 16 位做异或(不同为 1,相同为 0),这样就是把哈希值的高位和低位一起混合计算,这样就能使生成的 hash 值更离散,减少hash碰撞,注意hash(Object key)只是算出hash值

这里需要我解释下,通过前面的介绍,我们知道数组的容量范围是 [0,2^30],这个数还是比较大的,平时使用的数组容量还是比较小的,比如默认的大小 16,假设三个不同的 key 生成 1`的 hashCode 值如下所示:

19305951   00000001 00100110 10010101 11011111128357855  00000111 10100110 10010101 1101111138367      00000000 00000000 10010101 11011111

他们三个有个共同点是低 16 位完全一样,但高 16 位不同,当计算他们在数组中所在的下标时,如果直接通过 (n-1)&hash,这里 n 是 16,n-1=15,15 的二进制表示为

00000000 00000000 00000000 00001111

用 19305951、128357855、38367 都与 15 进行 & 运算,结果如下

在这里插入图片描述

通过计算后发现他们的结果一样,也就是说他们会被放到同一个下标下的链表或红黑树中,显然不符合我们的预期

所以对 hash 与其右移 16 位后的值进行异或操作,然后与 15 做与运算,看 hash 冲突情况

在这里插入图片描述

可见经过右移 16位后再进行异或操作,然后计算其对应的数组下标后,就被分到了不同的桶中,解决了哈希碰撞问题,思想就是把高位和低位混合进行计算,提高分散性

5.出现哈希冲突怎么解决

对于HashMap而言,存放的是键值对,所以做数据添加操作的时候会根据你传入的key值做hash运算,从而得到一个下标值,也就是以这个下标值来确定你的这个value值应该存放在底层Node数组的哪个位置。

那么这里一定会出现的问题就是,不同的key会被计算得出同一个位置,那么这样就冲突啦,位置已经被占了,那么怎么办嘞?

首先就是冲突了,我们要想办法看看后来的数据应该放在哪里,就是给它找个新位置,这是常规方法,除此之外,我们是不是也可以聚焦到hash算法这块,就是尽量减少冲突,让得到的下标值能够均匀分布。但是在添加数据的时候尽管为实现下标值的均匀分布做了很多努力,但是势必还是会存在冲突的情况,那么该怎么解决冲突呢?

在jdk1.7中采用数组+链表
在jdk1.8中采用数组+链表+红黑树

6.新的节点在插入到链表的时候,是怎么插入的?

Java8之前是头插法,啥意思嘞,就是放在之前Node的前面,为啥要这样,这是之前开发者觉得后面插入的数据会先用到,因为要使用这些Node是要遍历这个链表,在前面的遍历的会更快

在这里插入图片描述

在这里插入图片描述

7.为什么使用尾插法?

但是在Java8及之后都使用尾插法了,这里主要是一个链表成环的问题,啥意思嘞,想一下,使用头插法是不是会改变链表的顺序,你后来的就应该在后面嘛,如果扩容的话,由于原本链表顺序有所改变,扩容之后重新hash,可能导致的情况就是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。

这样的话在多线程操作下就会出现链表死循环,而使用尾插法,在相同的前提下就不会出现这样的问题,因为扩容前后链表顺序是不变的,他们之间的引用关系也是不变的

对应源码:HashMap的resize()方法中的for循环和do while循环

也就是说:如果在并发环境下不扩容的话,采用头插法和尾插法都可以

8.HashMap扩容机制?

下面我们继续说HashMap的扩容, HashMap 的最大容量为 2^30,经过上面的分析,我们知道第一次使用HashMap是创建一个默认长度为16的底层Node数组,如果满了怎么办,那就需要进行扩容了,也就是之前谈及的resize方法,这个方法主要就是初始化和增加表的大小,关于扩容要知道这两个概念:

  1. Capacity:HashMap当前长度。
  2. LoadFactor:负载因子,默认值0.75f。

这里怎么扩容的呢?首先是达到一个条件之后会发生扩容,什么条件呢?就是这个负载因子,比如HashMap的容量是100,负载因子是0.75,乘以100就是75,所以当你增加第76个的时候就需要扩容了,那扩容又是怎么样步骤呢?

首先是创建一个新的数组,容量是原来的二倍,之所以要为原来的二倍(也就是2 的整数次幂)目的是减少更多的hash冲突,然后会经过重新hash,把原来的数据放到新的数组上,至于为啥要重新hash,那必须啊,你容量变了,相应的hash算法规则也就变了,得到的结果自然不一样了

9.关于链表转红黑树

在Java8之前是没有红黑树的实现的,在jdk1.8中加入了红黑树,就是当链表长度为8时会将链表转换为红黑树,为6时又会转换成链表,这样是为了提高了性能,因为在链表的长度没有超过6时,链表由于使用尾插法,在新增和删除的时候快,查询效率也高,当长度为8时,链表的插入和删除操作比红黑树还是要快,因为红黑树要不断的维护树的平衡和节点的自旋操作,但是查询效率没有红黑树高(如:一个元素在链表的最末端,用链表的话要一一个个的找,用红黑树的话效率很高),为了综合性能,故在长度超过8之后,就用红黑树,不用链表

在这里插入图片描述

因为经过计算,在 hash 函数设计合理的情况下,发生 hash 碰撞 8 次的几率为百万分之 6,概率说话。因为 8 够用了,至于为什么转回来是 6,因为如果 hash 碰撞次数在 8 附近徘徊,会一直发生链表和红黑树的转化,为了预防这种情况的发生,所以为6

10.HashMap增加新元素的主要步骤

下面我们分析一下HashMap增加新元素的时候都会做哪些步骤:

1、判断数组是否为空,为空进行初始化;
2、不为空,计算 k 的 hash 值,....,然后通过(n - 1) & hash计算应当存放在数组中的下标 index;
3、看 table[index] 是否存在数据,没有数据就构造一个 Node 节点存放在 table[index] 中;
4、存在数据,说明发生了 hash 冲突(存在二个节点 key 的 hash 值一样), 继续判断 key 是否相等,相等,用新的 value 替换原数据(onlyIfAbsent 为 false);
5、如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;
6、如果不是树型节点,创建普通 Node 加入链表中;判断链表长度是否大于 8, 大于的话链表转换为红黑树;
7、插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍

在这里插入图片描述

所以啊,这里面的重点就是判断放入HashMap中的元素要不要替换当前节点的元素,那怎么判断呢?总结起来只要满足以下两点即可替换:

1、hash值相等。2、==或equals的结果为true。

11.谈谈jdk1.8 对HashMap三点主要的优化

1、数组+链表改成了—数组+链表或红黑树;
2、链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7 将新元素放到数组中,原始节点作为节点的后继节点,1.8 遍历链表,将元素放置到链表的最后;
3、在插入时,1.7 先判断是否需要扩容,再插入,1.8 先进行插入,插入完成再判断是否需要扩容

12. HashMap 是线程安全的吗

不是,在多线程环境下,1.7 会产生死循环、数据丢失、数据覆盖的问题,1.8 中会有数据覆盖的问题,以 1.8 为例,当 A 线程判断 index 位置为空后正好挂起,B 线程开始往 index 位置的写入节点数据,这时 A 线程恢复现场,执行赋值操作,就把 A 线程的数据给覆盖了;还有++size 这个地方也会造成多线程同时扩容等问题

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)  //多线程执行到这里tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode) // 这里很重要e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold) // 多个线程走到这,可能重复 resize()resize();afterNodeInsertion(evict);return null;

13. 如何解决HashMap线程不安全的问题?

Java 中有 HashTable、Collections.synchronizedMap、以及 ConcurrentHashMap 可以实现线程安全的 Map

HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个数组,粒度比较大,Collections.synchronizedMap 是使用 Collections 集合工具的内部类,通过传入 Map 封装出一个 对象,内部定义了一个对象锁,方法内通过对象锁实现;ConcurrentHashMap 使用分段锁,降低了锁粒度,让并发度大大提高

14.ConcurrentHashMap 的分段锁原理?

ConcurrentHashMap 成员变量使用 volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用 CAS 操作和 synchronized 结合实现赋值操作,多线程操作只会锁住当前操作索引的节点

如下图,线程 A 锁住 A 节点所在链表,线程 B 锁住 B 节点所在链表,操作互不干涉。

在这里插入图片描述

15.HashMap 内部节点是有序的吗

是无序的,根据 hash 值随机插入

16.有没有有序的 Map?

LinkedHashMap 和 TreeMap

17. LinkedHashMap 怎么实现有序的?

在这里插入图片描述

如上图,我们依次插入A、B、C、D、E五个Entry,而每次插入时,我们都按照插入顺序维持一个双向链表。我们从head指针开始,顺着after指针走(也就是图中的红色箭头),就可以还原我们的插入顺序

在这里插入图片描述

LinkedHashMap继承HashMap, LinkedHashMap 节点 Entry 内部除了继承 HashMap 的 Node 属性,还有 LinkedHashMap.Entry<K,V> head 和LinkedHashMap.Entry<K,V> tail 用于标识前置节点和后置节点。LinkedHashMap底层是数组 + 单项链表 + 双向链表。也就是说LinkedHashMap = HashMap+双向链表,数组 + 单向链表就是HashMap的结构,记录数据用;双向链表是用来给节点排序的,默认情况下LinkedHashMap是按照插入元素的顺序给元素排序的(accessOrder = false),当然我们也可以按照访问元素的方式 给链表排序(accessOrder = true)

案例:

public static void main(String[] args) {Map<String, String> map = new LinkedHashMap<>();map.put("1", "小孔明");map.put("2", "的");map.put("3", "博客");for (Map.Entry<String, String> item : map.entrySet()) {System.out.println(item.getKey() + ":" + item.getValue());}
}

18.TreeMap 怎么实现有序的?

TreeMap 是按照 Key 的自然顺序或者 Comprator 的顺序进行排序,内部是通过红黑树来实现。所以要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了 Comparator 接口的比较器,传给 TreeMap 用户 key 的比较

@Data
@AllArgsConstructor
@NoArgsConstructor
class Student  {private String name;private int age;
}
class StuNameComparator implements Comparator<Student> {@Overridepublic int compare(Student s1, Student s2) {int num = s1.getName().compareTo(s2.getName());if(num == 0){return new Integer(s1.getAge()).compareTo(new Integer(s2.getAge()));}return num;}
}
public class MyTest {public static void main(String[] args) {TreeMap<Student,String> map = new TreeMap<Student,String>(new StuNameComparator());map.put(new Student("lisi",21),"beijing");map.put(new Student("lisi",21),"tianji");map.put(new Student("lisi",22),"shanghai");map.put(new Student("lisi",23),"wuhan");map.put(new Student("lisi",24),"nanjing");Set<Map.Entry<Student, String>> entries = map.entrySet();entries.forEach(item-> System.out.println(item.getKey().getAge()));}
}

附:hashmap整体存储结构说明

在这里插入图片描述

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

http://www.ppmy.cn/news/735301.html

相关文章

SpringCloud学习1

SpringCloud是分布式微服务架构的 一些列解决方案的集合&#xff0c;SpringBoot是一门单独的技术 ​​​​​​

阿迪斯发得瑟得瑟阿迪斯

阿迪斯发啊手动阀阿迪斯发啊手动阀阿斯蒂发士大夫阿斯蒂f

AI赋能未来,贾维斯不再是梦。

Time: 2023年4月2日07:21:10 By : MemoryErHero Dream: 从不怀疑未来会有多精彩&#xff0c;努力过好生活的每一天。 1. 莓用ai https://ai.usesless.com/scene 2. 博弈ai https://ai.bo-e.com/ 3. Open Prompt https://openprompt.co/ 4. zcc https://zcc.app/ 5.po…

大地女神该亚与第一代天神乌拉诺斯

大地之神该亚是从混沌女神欧律诺墨分离出来的&#xff0c;一出生&#xff0c;就谁在奥林匹斯山上的一个石头上&#xff0c;一阵暖风盘旋后&#xff0c;大地女神该亚怀孕了&#xff0c;十月后生下了天神乌拉诺斯、老海神蓬托斯、时序女神。后天神乌拉诺斯看到了大地女神该亚&…

最新案例 | 昇思MindSpore携手梦蝶幻像打造濒危鸟类目标识别和实时追踪系统

2022年6月&#xff0c;重庆梦蝶幻像科技有限公司&#xff08;以下简称“梦蝶幻像”&#xff09;的濒危鸟类目标识别和实时追踪系统完成了与全场景AI框架昇思MindSpore的兼容性测试。该系统基于昇腾AI基础软硬件平台&#xff0c;可以实现对鸟类种类和姿态的准确识别&#xff0c;…

【das】

DAS DAS (database as a service)模型是最近出现的一种新的 数据管理 模型&#xff0c;它把用户的数据存放在. 数据库服务提供端 (database service provider&#xff0c; DSP )并让它们通过网络使用 数据库管理系统 &#xff0c;因此这种. 模型对外购数据库的安全性提出了更高…

双目立体感知「走下神坛」?鼻祖斯巴鲁启用单目摄像头

一直以来&#xff0c;双目立体感知技术是辅助驾驶赛道的小众路线。 支持者认为&#xff0c;立体感知接近人眼仿生概念&#xff0c;基于三角测量原理&#xff0c;利用成像设备从不同的位置获取被测物体的两幅图像&#xff0c;通过计算图像对应点间的位置偏差&#xff0c;来获取…

男人怎么读 萨瓦迪卡!还是萨瓦迪卡不!

泰国旅游中问候语你好是十分常见的,很早就听闻男同胞说萨瓦迪卡是不正确的.结果百度的结果是这样的,通篇并没有说正确的读音 修改关键词吧终于在知道里面找到想要的 สวัสดี 是梵文&#xff0c;สวัส-sawat 表示祝福、好运。ดี-dee表示好,สวัสดี -sawatdee 表示…