HashMap面试题
HashMap原理:
- “HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。”
1.“你用过HashMap吗?”
- 答:“HashMap可以接受null键值和值,而Hashtable则不能;HashMap是非synchronized;HashMap很快;以及HashMap储存的是键值对等等。这显示出你已经用过HashMap,而且对它相当的熟悉。”
2.“你知道HashMap的工作原理吗?” “你知道HashMap的get()方法的工作原理吗?”
- 答:HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象(Node 对象)。”
3.当两个对象的hashcode相同会发生什么?”
答:因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。”
- 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。
4.如果两个键的hashcode相同,你如何获取值对象?”
- 答:"当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。"
- HashMap在链表中存储的是键值对。
5.“如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?”
- 答:“默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。”
6.“你了解重新调整HashMap大小存在什么问题吗?”
- 答:“当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。这个时候,你可以质问面试官,为什么这么奇怪,要在多线程的环境下使用HashMap呢?”
7.HashMap中put方法的过程?
- 答:“调用哈希函数获取Key对应的hash值,再计算其数组下标;
- 如果没有出现哈希冲突,则直接放入数组;如果出现哈希冲突,则以链表的方式放在链表后面;
- 如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表;
- 如果结点的key已经存在,则替换其value即可;
- 如果集合中的键值对大于12,调用resize方法进行数组扩容。”
8.哈希函数怎么实现的?
- 调用Key的hashCode方法获取hashCode值,并将该值的高16位和低16位进行异或运算。
9.哈希冲突怎么解决?
- 将新结点添加在链表后面
10.数组扩容的过程?
- 创建一个新的数组,其容量为旧数组的两倍,并重新计算旧数组中结点的存储位置。结点在新数组中的位置只有两种,原下标位置或原下标+旧数组的大小。
11.拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?
- 答:“之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。”
12.说说你对红黑树的见解?
- 1、每个节点非红即黑
- 2、根节点总是黑色的
- 3、如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
- 4、每个叶子节点都是黑色的空节点(NIL节点)
- 5、从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
13.为什么HashMap集合在储存数据的时候要使用哈希算法?
- HaspMap中存储的数据都是不可重复的并且是无序的,那么我们在存储一个新的数据的时候就需要判断这个数据原来是否存在,这个时候就需要通过java中的equals方法来判断两个对象的实例是否相等,如果相等,那么就是重复数据。但是,如果每增加一个元素就检查一次,那么当元素比较多时,添加新元素的时候就需要用equals比较很多次,这个时候存储的效率就非常低。
- 例如:如果一个HashMap集合中已经有10000个元素,这个时候添加一个新元素的时候就需要用equals比较10000次,再添加一个元素,就需要用equals比较10001次,存储的元素越多,效率就会越低。
- 于是,Java便采用哈希算法来提高效率。当向集合中添加新的元素的时候,先将对象通过哈希算法(hashCode方法)计算得到哈希值(正整数),然后将哈希值和集合(数组)长度进行&运算,得到该对象在该数组存放位置的索引值。如果这个位置上没有元素,就可以直接存储在这个位置上,如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就覆盖,不相同的话就发生碰撞,形成链表。
14.jdk8中对HashMap做了哪些改变?
- 在java 1.8中,如果链表的长度超过了8,那么链表将转换为红黑树。(桶的数量必须大于64,小于64的时候只会扩容)
- 发生hash碰撞时,java 1.7 会在链表的头部插入,而java 1.8会在链表的尾部插入
- 在java 1.8中,Entry被Node替代(换了一个马甲)。
15.存储过程
- 在jdk7的基础上,形成了链表之后,当我们查询一个对象的时候,如果这个位置已经形成了链表,那么此时查询的效率就比较低了,因为我们得遍历这个链表,运气不好的话,可能要查找的元素就是在链表的最后一个,那么此时我们就需要把整个链表都遍历一遍,效率比较低。
- 在jdk1.8之后,在数组+链表的基础上,还多了一个红黑树。现在的结构就是数组+链表+红黑树,当碰撞的次数大于8并且总容量大于64的时候,链表就会变为红黑树结构,转为红黑树之后,除了添加以外,其他的效率都比链表高,因为在添加的时候,链表是直接加到链表的末尾,而红黑树添加的时候,需要比较大小,然后再进行添加。转为红黑树之后,集合进行扩容以后,不用重新对元素进行计算,只需要找每个元素的二倍,然后把元素放入位置就可以了,提高了效率。
16.负载因子为什么会影响HashMap性能
- 我们都知道有序数组存储数据,对数据的索引效率都很高,但是插入和删除就会有性能瓶颈(回忆ArrayList),链表存储数据,要一次比较元素来检索出数据,所以索引效率低,但是插入和删除效率高(回忆LinkedList),两者取长补短就产生了哈希散列这种存储方式,也就是HashMap的存储逻辑.而负载因子表示一个散列表的空间的使用程度,有这样一个公式:initailCapacity*loadFactor=HashMap的容量。
- 所以负载因子越大则散列表的装填程度越高,也就是能容纳更多的元素,元素多了,链表大了,所以此时索引效率就会降低。反之,负载因子越小则链表中的数据量就越稀疏,此时会对空间造成烂费,但是此时索引效率高。
17.为什么initailCapacity要设置成2的n次幂
我们可以看到在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。
- 所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样做的:
-
static int indexFor(int h, int length) { return h & (length-1); }
- 左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!
-
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用
遍历某个位置上的链表,这样查询效率也就较高了。