Java面试高频:HashMap的面试考点,你真的会么?

1. 说说Java中常见的集合有哪些?

2. HashMap和HashTable的区别

3. HashSet和HashMap的区别

4. 谈一下HashMap的特性?

5. 谈一下HashMap的底层原理是什么?

6. 谈一下hashMap中get是如何实现的?

7. 谈一下hashMap中put是如何实现的?

8. 谈一下hashMap中什么时候需要进行扩容,扩容resize()又是如何实现的?

9. 谈一下HashMap中hash函数是怎么实现的?还有哪些hash函数的实现方式?

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

10.1 为什么是两次扰动呢?

11.为什么哈希数组的默认长度是16?为什么扩容必须是2的幂?如果输入值不是2的幂比如10会怎么样?

12. 谈一下当两个对象的hashCode相等时会怎么样?

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

14. 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

15. 请解释一下HashMap的参数loadFactor,它的作用是什么?

16.传统hashMap的缺点(为什么引入红黑树?)

17. 平时在使用HashMap时一般使用什么类型的元素作为Key?

1. 说说Java中常见的集合有哪些?

答:Map接口和Collection接口是所有集合框架的父接口:

  • Collection接口的子接口包括:Set接口和List接口
  • Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
  • Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
  • List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等


2. HashMap和HashTable的区别

  • 线程安全性区别:HashMap没有考虑同步,是线程不安全的;Hashtable使用了synchronized关键字,是线程安全的;
  • HashMap允许K/V都为null;后者K/V都不允许为null;
  • 父类不同:HashMap继承自AbstractMap类;而Hashtable继承自Dictionary类;
  • 迭代器(Iterator):HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException。
  • 容量的初始值和增加方式都不一样:HashMap默认的容量大小是16;增加容量时,每次将容量变为"原始容量x2"。Hashtable默认的容量大小是11;增加容量时,每次将容量变为"原始容量x2 + 1";
  • 添加key-value时的hash值算法不同:HashMap添加元素时,是使用自定义的哈希算法。Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。

3. HashSet和HashMap的区别

  • HashMap实现了Map接口,HashSet实现了Set接口
  • HashMap储存键值对,HashSet仅仅存储对象
  • HashMap使用put()方法将元素放入map中,HashSet使用add()方法将元素放入set中
  • HashMap中使用键对象来计算hashcode值,HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
  • HashMap比较快,因为是使用唯一的键来获取对象,HashSet较HashMap来说比较慢。


4. 谈一下HashMap的特性?

  • 1.HashMap存储键值对实现快速存取,允许为null。key值不可重复,若key值重复则覆盖。
  • 2.非同步,线程不安全。
  • 3.底层是hash表,不保证有序(比如插入的顺序)

5. 谈一下HashMap的底层原理是什么?

  • 数据结构:基于hashing的原理,jdk8后采用数组+链表+红黑树的数据结构,通过put和get存储和获取对象。
  • 插入:当我们给put()方法传递键和值时,先对键做一个hashCode()的计算来得到它在bucket数组中的位置来存储Entry对象。
  • 读取:当获取对象时,通过get获取到bucket的位置,再通过键对象的equals()方法找到正确的键值对,然后在返回值对象。


6. 谈一下hashMap中get是如何实现的?

  • 对key的hashCode进行hashing,与运算计算下标获取bucket位置,如果在桶的首位上就可以找到就直接返回,否则在树中找或者链表中遍历找,如果有hash冲突,则利用equals方法去遍历链表查找节点。


7. 谈一下hashMap中put是如何实现的?

  • 1. 计算关于key的hashcode值(与Key.hashCode的高16位做异或运算)
  • 2. 如果散列表为空时,调用resize()初始化散列表
  • 3. 如果没有发生碰撞,直接添加元素到散列表中去
  • 4. 如果发生了碰撞(hashCode值相同),进行三种判断:

a. 若key地址相同或者equals后内容相同,则替换旧值

b. 如果是红黑树结构,就调用树的插入方法

c. 链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。

  • 5. 如果桶满了大于阀值,则resize进行扩容


8. 谈一下hashMap中什么时候需要进行扩容,扩容resize()又是如何实现的?

hashMap扩容的触发条件:

1.初始化数组table

2.当数组table的size达到阙值时即++size > load factor * capacity 时,也是在putVal函数中


扩容的详细实现过程:

1. 通过判断旧数组的容量是否大于0来判断数组是否初始化过,否,则进行初始化:

  • 判断是否调用无参构造器,是:使用默认的大小(16)和阙值(0.75)
  • 判断是否调用无参构造器,否:使用构造函数中初始化的容量。当然这个容量是经过tableSizefor计算后的2的次幂数;

2. 已初始化,则进行扩容,扩容成两倍(小于最大值的情况下),之后在进行将元素重新进行与运算复制到新的散列表中。

概括的讲:扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。

9. 谈一下HashMap中hash函数是怎么实现的?还有哪些hash函数的实现方式?

对key的hashCode做hash操作,与高16位做异或运算

还有平方取中法,除留余数法,伪随机数法

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

  • 只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;
  • 如果 length 为 2 的次幂 则 length-1 转化为二进制必定是 11111……的形式,在于 h 的二进制与操作效率会非常的快,而且空间不浪费;如果 length 不是 2 的次幂,比如 length 为 15,则 length - 1 为 14,对应的二进制为 1110,在于 h 与操作,最后一位都为 0 ,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。

10.1 面试官:那为什么是两次扰动呢?

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


11.为什么哈希数组的默认长度是16?为什么扩容必须是2的幂?如果输入值不是2的幂比如10会怎么样?

  • 1.为了数据的均匀分布,减少哈希碰撞。因为确定数组位置是用的位运算,若数据不是2的次幂则会增加哈希碰撞的次数和浪费数组空间。(PS:其实若不考虑效率,求余也可以就不用位运算了也不用长度必需为2的幂次)
  • 2.输入数据若不是2的幂,HashMap通过一通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字

12. 谈一下当两个对象的hashCode相等时会怎么样?

会产生哈希碰撞,若key值相同则替换旧值,不然链接到链表后面,链表长度超过阙值8就转为红黑树存储,如果长度缩减小于阈值6则红黑树转链表存储。

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

HashCode相同,通过equals比较内容获取值对象


14. 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

超过阙值会进行扩容操作,概括的讲就是扩容后的数组大小是原数组的2倍,将原来的元素重新hashing放入到新的散列表中去。

15. 请解释一下HashMap的参数loadFactor,它的作用是什么?

loadFactor表示HashMap的拥挤程度,影响hash操作到同一个数组位置的概率。默认loadFactor等于0.75,当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,在HashMap的构造器中可以定制loadFactor。

16.传统hashMap的缺点(为什么引入红黑树?)

JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。

17. 平时在使用HashMap时一般使用什么类型的元素作为Key?

选择Integer,String这种不可变的类型,像对String的一切操作都是新建一个String对象,对新的对象进行拼接分割等,这些类已经很规范的覆写了hashCode()以及equals()方法。作为不可变类天生是线程安全的。


分享到:


相關文章: