前言
HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
一、说一下 HashMap 的实现原理?
HashMap的数据结构: 在Java编程语言中, 基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。HasMap基于Hash算法实现的。 1、当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标 2、存储时,如果出现hash值相同的key,此时有两种情况。 (1)如果key相同,则覆盖原始值; (2)如果key不同(出现冲突),则将当前的key-value放入链表中。 3、获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。 4、解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个 之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)
二、HashMap 底层数据结构
HashMap 底层是数组 + 链表 + 红黑树的数据结构,数组的主要作用是方便快速查找,时间复杂度是 O(1),默认大小是 16,数组的下标索引是通过 key 的 hashcode 计算出来的,数组元素叫做 Node,当多个 key 的 hashcode 一致,但 key 值不同时,单个 Node 就会转化成链表,链表的查询复杂度是 O(n),当链表的长度大于等于 8 并且数组的大小超过 64 时,链表就会转化成红黑树,红黑树的查询复杂度是 O(log(n)),简单来说,最坏的查询次数相当于红黑树的最大深度。
三、 HashMap 是如何扩容的?
1、put 时,发现数组为空,进行初始化扩容,默认扩容大小为 16; 2、put 成功后,发现现有数组大小大于扩容的门阀值时,进行扩容,扩容为老数组大小的 2 倍; 扩容的门阀是 threshold,每次扩容时 threshold 都会被重新计算,门阀值等于数组的大小 * 影响因子(0.75)。
新数组初始化之后,需要将老数组的值拷贝到新数组上,链表和红黑树都有自己拷贝的方法。
四、为什么链表个数大于等于 8 时,链表要转化成红黑树了?
当链表个数太多了,遍历可能比较耗时,转化成红黑树,可以使遍历的时间复杂度降低,但转化成红黑树,有空间和转化耗时的成本,我们通过泊松分布公式计算,正常情况下,链表个数出现 8 的概念不到千万分之一,所以说正常情况下,链表都不会转化成红黑树,这样设计的目的,是为了防止非正常情况下,比如 hash 算法出了问题时,导致链表个数轻易大于等于 8 时,仍然能够快速遍历。 延伸问题:红黑树什么时候转变成链表。
答:当节点的个数小于等于 6 时,红黑树会自动转化成链表,主要还是考虑红黑树的空间成本问题,当节点个数小于等于 6 时,遍历链表也很快,所以红黑树会重新变成链表。
五、HashMap 在 put 时,如果数组中已经有了这个 key,我不想把 value 覆盖怎么办?取值时,如果得到的 value 是空时,想返回默认值怎么办?
如果数组有了 key,但不想覆盖 value ,可以选择 putIfAbsent 方法,这个方法有个内置变量 onlyIfAbsent,内置是 true ,就不会覆盖,我们平时使用的 put 方法,内置 onlyIfAbsent 为 false,是允许覆盖的。
取值时,如果为空,想返回默认值,可以使用 getOrDefault 方法,方法第一参数为 key,第二个参数为你想返回的默认值,如 map.getOrDefault(“2”,“0”),当 map 中没有 key 为 2 的值时,会默认返回 0,而不是空。
|