HashMap相关问题

本文详细探讨了HashMap的工作原理,包括如何通过哈希算法定位下标,HashMap的结构(数组+链表+红黑树),以及不同版本中插入节点的方式。还分析了HashMap的扩容策略,容量为何取2的指数倍,以及何时进行树化。此外,讨论了HashMap对null的支持,哈希值与对象关系,以及JDK8相对于JDK7的改进,如尾插法和红黑树的使用,提升了查找和插入效率。
摘要由CSDN通过智能技术生成

Q0:HashMap是如何定位下标的?

A:先获取Key,然后对Key进行hash,获取一个hash值,然后用hash值对HashMap的容量进行取余(实际上不是真的取余,而是使用按位与操作,原因参考Q6),最后得到下标。

Q1:HashMap由什么组成?

A:数组+单链表,jdk1.8以后又加了红黑树,当链表节点个数超过8个(m默认值)且 数组长度需要大于64才会变成红黑树,(若知识链表节点个数超过8个会进行resize()操作),开始使用红黑树,使用红黑树一个综合取优的选择,相对于其他数据结构,红黑树的查询和插入效率都比较高。而当红黑树的节点个数小于6个(默认值)以后,又开始使用链表。这两个阈值为什么不相同呢?主要是为了防止出现节点个数频繁在一个相同的数值来回切换,举个极端例子,现在单链表的节点个数是9,开始变成红黑树,然后红黑树节点个数又变成8,就又得变成单链表,然后节点个数又变成9,就又得变成红黑树,这样的情况消耗严重浪费,因此干脆错开两个阈值的大小,使得变成红黑树后“不那么容易”就需要变回单链表,同样,使得变成单链表后,“不那么容易”就需要变回红黑树。

Q2:Java的HashMap为什么不用取余的方式存储数据?

A:实际上HashMap的 index = (n - 1) & hash,而不是%求余。(这里有个硬性要求,容量必须是2的指数倍,原因参考Q6)

Q3:HashMap往链表里插入节点的方式?

A:jdk1.7以前是头插法,jdk1.8以后是尾插法,因为引入红黑树之后,就需要判断单链表的节点个数(超过8个后要转换成红黑树),所以干脆使用尾插法,正好遍历单链表,读取节点个数。也正是因为尾插法,使得HashMap在插入节点时,可以判断是否有重复节点。

Q4:HashMap默认容量和负载因子的大小是多少?

A:默认容量是16,负载因子是0.75。

Q5:HashMap初始化时,如果指定容量大小为10,那么实际大小是多少?

A:16,因为HashMap的初始化函数中规定容量大小要是2的指数倍,即2,4,8,16,所以当指定容量为10时,实际容量为16。

		int n = cap - 1;
		n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

Q6:容量大小为什么要取2的指数倍?

A:两个原因:1,提升计算效率:因为2的指数倍的二进制都是只有一个1,而2的指数倍-1的二进制就都是左全0右全1。那么跟(2n- 1)做按位与运算的话,得到的值就一定在【0,2n- 1】区间内,这样的数就刚合适可以用来作为哈希表的容量大小,因为往哈希表里插入数据,就是要对其容量大小取余,从而得到下标。所以用2n做为容量大小的话,就可以用按位与操作替代取余操作,提升计算效率。2.便于动态扩容后的重新计算哈希位置时能均匀分布元素:因为动态扩容仍然是按照2的指数倍,所以按位与操作的值的变化就是二进制高位+1,比如16扩容到32,二进制变化就是从0000 1111(即15)到0001 1111(即31),那么这种变化就会使得需要扩容的元素的哈希值重新按位与操作之后所得的下标值要么不变,要么+16(即挪动扩容后容量的一半的位置),这样就能使得原本在同一个链表上的元素均匀(相隔扩容后的容量的一半)分布到新的哈希表中。(注意:原因2(也可以理解成优点2),在jdk1.8之后才被发现并使用)

Q7:HashMap满足扩容条件的大小(即扩容阈值)怎么计算?

A:扩容阈值=min(容量负载因子,MAXIMUM_CAPACITY+1),MAXIMUM_CAPACITY非常大,所以一般都是取(容量负载因子)

Q8:HashMap是否支持元素为null?

A:支持。且null对应的hash为0,所以通过index=(table.length - 1) & hash计算出的下表为0,故存储在table的第一个位置。

Q9:HashMap的 hash(Obeject k)方法中为什么在调用 k.hashCode()方法获得hash值后,为什么不直接对这个hash进行取余,而是还要将hash值进行右移和异或运算?

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

A:
如果HashMap容量比较小而hash值比较大的时候,哈希冲突就容易变多。假设容量为16,那么就要对二进制0000 1111(即15)进行按位与操作,那么hash值的二进制的高28位无论是多少,都没意义,因为都会被0&,变成0。所以哈希冲突容易变多。那么hash(Obeject k)方法中在调用 k.hashCode()方法获得hash值后,进行的一步运算:异或运算是利用了特性:同0异1原则,可以使k.hashCode()方法获得的hash值的二进制中高位尽可能多地参与按位与操作,从而减少哈希冲突。右移16为是因为int一共32位。
其实jdk1.8中对此进行了弱化,jdk1.7更为复杂。

Q10:哈希值相同,对象一定相同吗?对象相同,哈希值一定相同吗?

A:不一定。一定。哈希值相同是对象相同的必要不充分条件。

Q11:HashMap的扩容与插入元素的顺序关系?

A:jdk1.7以前是先扩容再插入,jdk1.8以后是先插入再扩容。

Q12:HashMap扩容的原因?

A:提升HashMap的get、put等方法的效率,因为如果不扩容,链表就会越来越长,导致插入和查询效率都会变低。

Q13:jdk1.8引入红黑树后,如果单链表节点个数超过8个,是否一定会树化?

 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();

A:不一定,它会先去判断是否需要扩容,如果满足扩容条件,直接扩容,不会树化,因为扩容不仅能增加容量,还能缩短单链表的节点数,一举两得。

Q14:JDK8中的HashMap与JDK7的HashMap有什么不一样?

  1. JDK8中新增了红黑树,JDK8是通过数组+链表+红黑树来实现的
  2. JDK7中链表的插入是用的头插法,而JDK8中则改为了尾插法
  3. JDK8中的因为使用了红黑树保证了插入和查询了效率,所以实际上JDK8中
    的Hash算法实现的复杂度降低了
  4. JDK8中数组扩容的条件也发了变化,只会判断是否当前元素个数是否查过了
    阈值,而不再判断当前put进来的元素对应的数组下标位置是否有值。
  5. JDK7中是先扩容再添加新元素,JDK8中是先添加新元素然后再扩容

Q15:HashMap中PUT方法的流程?

  1. 通过key计算出一个hashcode
  2. 通过hashcode与“与操作”计算出一个数组下标
  3. 判断数组下标对应的位置,是不是空,如果是空则直接newNode 把key,value存在该数组位置
  4. 如果该下标对应的位置不为空,则判断当前节点是Node还是TreeNode,若为TreeNode则插入到红黑树中,否则插入到链表中
  5. 插入过程中还需要判断是否存在相同的key,如果存在,则更新value
  6. 如果是JDK7,则使用头插法
  7. 如果是JDK8,如果当前节点为Node,则会遍历链表,并且在遍历链表的过程中,统计当前链表的元素个数,如果超过8个,则先把元素插入到链表中,再链表转变为红黑树
	if ((e = p.next) == null) {
       p.next = newNode(hash, key, value, null);
           if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
               treeifyBin(tab, hash);
           break;
	}

Q16:JDK8中链表转变为红黑树的条件?

  1. 链表中的元素的个数为8个或超过8个
  2. 同时,还要满足当前数组的长度大于或等于64才会把链表转变为红黑树。为什么?因为链表转变为红黑树的目的是为了解决链表过长,导致查询和插入效率慢的问题,而如果要解决这个问题,也可以通过数组扩容,把链表缩短也可以解决这个问题。所以在数组长度还不太长的情况,可以先通过数组扩容来解决链表过长的问题。

Q17:HashMap扩容流程是怎样的?

  1. HashMap的扩容指的就是数组的扩容, 因为数组占用的是连续内存空间,所以数组的扩容其实只能新开一个新的数组,然后把老数组上的元素转移到新数组上来,这样才是数组的扩容
  2. 在HashMap中也是一样,先新建一个2倍数组大小的数组
  3. 然后遍历老数组上的每一个位置,如果这个位置上是一个链表,就把这个链表上的元素转移到新数组上去
  4. 在这个过程中就需要遍历链表,当然jdk7,和jdk8在这个实现时是有不一样的,jdk7就是简单的遍历链表上的每一个元素,然后按每个元素的hashcode结合新数组的长度重新计算得出一个下标,而重新得到的这个数组下标很可能和之前的数组下标是不一样的,这样子就达到了一种效果,就是扩容之后,某个链表会变短,这也就达到了扩容的目的,缩短链表长度,提高了查询效率
  5. 而在jdk8中,因为涉及到红黑树,这个其实比较复杂,jdk8中其实还会用到一个双向链表来维护红黑树中的元素,所以jdk8中在转移某个位置上的元素时,会去判断如果这个位置是一个红黑树,那么会遍历该位置的双向链表,遍历双向链表统计哪些元素在扩容完之后还是原位置,哪些元素在扩容之后在新位置,这样遍历完双向链表后,就会得到两个子链表,一个放在原下标位置,一个放在新下标位置,如果原下标位置或新下标位置没有元素,则红黑树不用拆分,否则判断这两个子链表的长度,如果超过八,则转成红黑树放到对应的位置,否则把单向链表放到对应的位置。【此处只会出现新位置与旧的位置,为什么只有两个位置见Q6】
  6. 元素转移完了之后,在把新数组对象赋值给HashMap的table属性,老数组会被回收到。

神图:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我橘子超酸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值