Java开发面试官终结者!HashMap高频面试题总结,必拿下!

222 阅读7分钟

HashMap 的工作原理

首先 HashMap 是基于 hashing 的原理,我们知道 HashMap 有两个常用的方法 put()get(),将键值对传递给 put() 方法时,它调用键对象的hashCode()方法来计算 hashcode,然后找到 bucket 位置来储存值对象。当获取对象时,通过键对象的equals() 方法找到正确的键值对,然后返回值对象。

一般情况下,肯定会问如果不同的键对象的 hashcode 值相等会出现什么样的情况。他们肯定是存储在同一个位置的链表中的,使用键对象的 equals() 方法找到键值对。 在 Java1.8 以后,HashMap 在数组、链表的基础上又增加了红黑树的数据结构。在一个数组位置的链表长度大于 8 时数据结构转换为红黑树的结构。

HashMap 和 HashTable 的区别有什么?

HashMap 和 HashTable 都实现了 Map 接口,主要的区别集中在线程、同步、速度方面的差别。

  • HashMap 是非同步的,并且 HashMap 可以存储键值为 null 的对象,允许最多只有一个键可以为 null 对象、允许可以有多个值为 null。
  • HashMap 是线程不安全的,HashTable 是线程安全的。
  • 因为 HashTable 是多线程的,所以在单线程的情况下,HashMap 的速度要快一些。
  • HashMap 不能保证存储顺序是不变的。

HashMap 和 HashSet 的区别有什么?

  • 两者实现的接口不一样,HashMap 实现的是 Map 接口、HashSet 则实现的是 Set 接口。
  • HashMap 存储的是键值对、HashSet 存储的是对象。
  • HashSet 的速度比 HashMap 的要慢一些。
  • 计算 hashcode 值的方式不一样,HashMap 使用键对象来计算、HashSet 使用它本身的对象元素来计算。

当两个对象的 HashCode 相同会发生什么?

因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。

如果两个键的 HashCode 相同,你如何获取值对象?

当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

多线程情况下,调整 HashMap 的大小会有什么问题?

由于线程不安全的原因,在多线程条件下调整 HashMap 的大小时会存在多个 HashMap 对象的竞争关系,不知道要给哪一个调整大小。如此一来,多线程情况调整 HashMap 的大小就会陷入死循环的情况,在 Java1.5 以后就增加了 ConcurrentHashMap 的对象解决多线程等问题。

JDK8中的HashMap有哪些改动?

  1. JDK7中的底层实现是数组+链表,JDK8中使用的是数组+链表+红黑树。
  2. JDK7中扩容时有可能出现死锁,JDK8中通过算法优化不会出现死锁了。
  3. JDK8中对算哈希值的哈希算法进行了简化以提高运算效率

JDK8中为什么要使用红黑树?

因为JDK7中是用数组+链表来作为底层的数据结构的,但是如果数据量较多,或者hash算法的散列性不够,可能导致链表上的数据太多,导致链表过长,考虑一种极端情况:如果hash算法很差,所有的元素都在同一个链表上。那么在查询数据的时候的时间复杂度和链表查询的时间复杂度差不多是一样的,我们知道链表的一个优点是插入快,但是查询慢,所以如果HashMap中出现了很长的链表结构会影响整个HashMap的查询效率,我们使用HashMap时插入和查询的效率是都要具备的,而红黑树的插入和查询效率处于完全平衡二叉树和链表之间,所以使用红黑树是比较合适的。

HashMap扩容机制是怎么样的,JDK7 与JDK8有什么不同吗?

首先,我们需要知道HashMap为什么需要扩容,道理很简单,HashMap底层是用数组+链表实现的,而数组是预先就已经分配好内存的,如果需要对数组进行扩容,需要重新开辟一个新的数组再将旧数组上的元素进行转移,如果不进行扩容,那么会导致HashMap的链表过长,查询效率降低,所以需要对数组进行扩容。

在JDK7中,HashMap扩容的条件是 (size >= threshold) && (null !=table[bucketIndex]) , size 为HashMap当前的容量, threshold 初始化值为12, table[bucketIndex] 代表所put进来的key所对应的数组上的元素,所以在JDK7中扩容条件是当当Put操操作传入的 作传入的Key值所对应的数组位置上不为空时并且当前容量大于等于了扩容的阈值时才进行扩容 值所对应的数组位置上不为空时并且当前容量大于等于了扩容的阈值时才进行扩容,JDK7中的扩容思路是:开辟一个新的数组,数组大小为原数组的两倍,然后再将数组上的链表与元素转移到新数组上,此过程可能会出现死锁。

JDK8中的扩容条件比JDK7中要少,只有当前容量大于等于了扩容的阈值时才进行扩容 当前容量大于等于了扩容的阈值时才进行扩容,并且扩容的思路也发生了变化,思路比较复杂。

为什么重写对象的Equals方法时,要重写HashCode方法,跟HashMap有关系吗?为什么?

跟HashMap有关系,或者说因为HashMap中用到了对象的hashcode方法所以会有关系,因为我们如果在设计两个对象相等的逻辑时,如果只重写Equals方法,那么一个类有两个对象A1,A2,他们的A1.equals(A2)为true,A1.hashcode和A2.hashcode不一样,当将A1和A2都作为HashMap的key时,HashMap会认为它两不相等,因为 HashMap在判断key值相不相等时会判断key的hashcode是不是一样,hashcode一样相等,所以在这种场景下会出现我们认为这两个对象相等,但是hashmap不这么认为,所以会有问题。

在使用HashMap的过程中我们应该注意些什么问题?

  1. HashMap的扩容机制是很影响效率的,所以如果事先能确定有多少个元素需要存储,那么建议在初始化HashMap时对数组的容量也进行初始化,防止扩容。
  2. HashMap中使用了对象的hashcode方法,而且很关键,所以再重写对象的equals时建议一定要重写hashcode方法。
  3. 如果是用对象作为HashMap的key,那么请将对象设置为final,以防止对象被重新赋值,因为一旦重新赋值其实就代表了一个新对象作为了key,因为两个对象的hashcode可能不同。

HashMap和Hashtable的区别

  1. HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized修饰
  2. 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它
  3. HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在HashTable 中put 进的键值只要有一个 null,直接抛出 NullPointerException。
  4. JDK1.8 以后的 HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable没有这样的机制。 最后:祝大家都能在秋招中找到自己合适的工作,飞黄腾达!!