java面试小知识Day002(HashMap)附加详细解析

为什么HashMap中String、Integer这样的包装类适合作为K?

String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率:

  1. 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
  2. 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况

解析:
这个问题的回答涉及到了HashMap的核心原理。HashMap的效率和正确性很大程度上依赖于key的不可变性和hash值的稳定性。String和Integer作为不可变类,一旦创建就不能被修改,这保证了它们作为key时,其hash值在整个生命周期内保持不变。同时,这些类都精心实现了hashCode()和equals()方法,确保了hash值的分布均匀性和比较的准确性,这对于HashMap的性能和正确性至关重要。

如果使用Object作为HashMap的Key,应该怎么办呢?

答:重写hashCode()和equals()方法

  1. 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
  2. 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性

解析:
使用自定义对象作为HashMap的key时,正确重写hashCode()和equals()方法是至关重要的。这是因为HashMap使用这两个方法来确定key的唯一性和存储位置。hashCode()方法用于计算key的哈希值,进而确定其在内部数组中的存储位置。equals()方法则用于在发生哈希冲突时,确定两个key是否真的相等。如果这两个方法实现不当,可能会导致无法正确存储和检索值,甚至可能破坏HashMap的内部结构。重写这两个方法时要遵循一定的规则,以确保HashMap的正确性和效率。

HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

答:hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

解析:
这个问题涉及到了HashMap的内部实现细节。直接使用hashCode()的返回值作为数组下标是不可行的,原因有二:首先,hashCode()返回的是一个int值,其范围远远超过了HashMap实际的容量大小,直接使用会导致数组越界;其次,hashCode()的返回值可能为负,而数组索引不能为负。因此,HashMap需要将hashCode()的返回值转化为一个合适的非负整数,作为数组的索引。

那怎么解决呢?

  1. HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
  2. 在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了"哈希值与数组大小范围不匹配"的问题

解析:
HashMap解决这个问题的方法体现了其设计的巧妙之处。通过自定义的hash()方法,HashMap首先对hashCode()的结果进行了再散列,这样可以减少哈希冲突,使得哈希值分布更均匀。然后,通过位运算(h & (length - 1))来获取数组下标,这不仅比取模运算更高效,而且巧妙地将任意整数映射到了[0, length-1]的范围内。这种方法要求数组长度必须是2的幂,这也是HashMap容量总是2的幂的原因之一。

HashMap 的长度为什么是2的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。
这个算法应该如何设计呢?我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

解析:
HashMap 长度为2的幂次方这个设计决策体现了效率与均匀分布的平衡。首先,当长度为2的幂时,h & (length - 1) 等价于 h % length,但位运算比取模运算更快。其次,这种设计能保证 length - 1 的二进制表示全为1(例如,16-1 = 15,二进制为 1111),这样可以保证 hash & (length - 1) 的结果能够充分利用 hash 值的所有位,从而达到更好的散列效果。最后,这种设计便于扩容操作,每次扩容只需将长度翻倍,并重新分配元素即可。

那为什么是两次扰动呢?

答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的。

解析:
两次扰动的设计目的是为了进一步减少哈希冲突。第一次扰动是将 hashCode 的高16位与低16位进行异或操作,这样可以让高位也参与到最后的取模运算中,增加了随机性。第二次扰动是在取模运算中,通过与(length-1)进行与运算,进一步打乱了原有的分布。两次扰动已经能够在效率和均匀性之间取得很好的平衡,再增加扰动次数可能会影响性能而收效不大。这种设计充分体现了 HashMap 在性能优化上的深思熟虑。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值