HashMap常见面试题

1、默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂

hash运算的过程其实就是对目标元素的Key进行hashcode,再对Map的容量进行取模,而JDK 的工程师为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂。

HashMap的容量为什么是2的n次幂,和这个(n - 1) & hash的计算方法有着千丝万缕的关系,符号&是按位与的计算,这是位运算,计算机能直接运算,特别高效,按位与&的计算方法是,只有当对应位置的数据都为1时,运算结果也为1,当HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够(充分的散列),使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。

HashMap 的底层数组长度为何总是2的n次方

HashMap根据用户传入的初始化容量,利用无符号右移和按位或运算等方式计算出第一个大于该数的2的幂。

·使数据分布均匀,减少碰撞

·当length为2的n次方时,h&(length - 1) 就等价于 h %length,而且在速度、效率上比直接取模要快得多

2你知道hash的实现吗?为什么要这样实现

JDK1.8中,是通过hashCode()的高16位异或低16位实现的:(h=k.hashCode())^(h>>>16),主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。

计算过程如下所示:

说明:

·key.hashCode();返回散列值也就是hashcode,假设随便生成的一个值。

·n表示数组初始化的长度是16。

·&(按位与运算):运算规则:相同的二进制数位上,都是1的时候,结果为1,否则为零。

·^(按位异或运算):运算规则:相同的二进制数位上,数字相同,结果为0,不同为1。

 

高16bit不变,低16bit和高16bit做了一个异或(得到的hashCode转化为32位二进制,前16位和后16位低16bit和高16bit做了一个异或)

问题:为什么要这样操作呢?

如果当n即数组长度很小,假设是16的话,那么n - 1即为1111 ,这样的值和hashCode直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成哈希冲突了,所以这里把高低位都利用起来,从而解决了这个问题。

 

3为什么要用异或运算符

保证了对象的hashCode的32位值只要有一位发生改变,整个hash()返回值就会改变。尽可能的减少碰撞。

4HashMap的主要参数都有哪

·DEFAULT_INITIAL_CAPACITY:默认的初始化容量,1<<4位运算的结果是16,也就是默认的初始化容量为16。当然如果对要存储的数据有一个估计值,最好在初始化的时候显示的指定容量大小,减少扩容时的数据搬移等带来的效率消耗。同时,容量大小需要是2的整数倍。

·MAXIMUM_CAPACITY:容量的最大值,1 << 30位,2的30次幂。

·DEFAULT_LOAD_FACTOR:默认的加载因子,设计者认为这个数值是基于时间和空间消耗上最好的数值。这个值和容量的乘积是一个很重要的数值,也就是阈值,当达到这个值时候会产生扩容,扩容的大小大约为原来的二倍。

·TREEIFY_THRESHOLD:因为jdk8以后,HashMap底层的存储结构改为了数组+链表+红黑树的存储结构(之前是数组+链表),刚开始存储元素产生碰撞时会在碰撞的数组后面挂上一个链表,当链表长度大于这个参数时,链表就可能会转化为红黑树,为什么是可能后面还有一个参数,需要他们两个都满足的时候才会转化。

·UNTREEIFY_THRESHOLD:介绍上面的参数时,我们知道当长度过大时可能会产生从链表到红黑树的转化,但是,元素不仅仅只能添加还可以删除,或者另一种情况,扩容后该数组槽位置上的元素数据不是很多了,还使用红黑树的结构就会很浪费,所以这时就可以把红黑树结构变回链表结构,什么时候变,就是元素数量等于这个值也就是6的时候变回来(元素数量指的是一个数组槽内的数量,不是HashMap中所有元素的数量)。

·MIN_TREEIFY_CAPACITY:链表树化的一个标准,前面说过当数组槽内的元素数量大于8时可能会转化为红黑树,之所以说是可能就是因为这个值,当数组的长度小于这个值的时候,会先去进行扩容,扩容之后就有很大的可能让数组槽内的数据可以更分散一些了,也就不用转化数组槽后的存储结构了。当然,长度大于这个值并且槽内数据大于8时,那就转化为红黑树吧。

5哈希冲突及解决方法

      如果两个不同对象的hashCode相同,这种现象称为hash冲突。有以下的方式可以解决哈希冲突:

1、开放定址法 开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

2、链地址法链地址法 将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。

3、再哈希法 当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。

4、建立公共溢出区 将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。

6HashMap如何有效减少碰撞

1、扰动函数:促使元素位置分布均匀,减少碰撞几率

2、使用final对象,并采用合适的equals()和hashCode()方法

7HashMap可以实现同步吗

HashMap可以通过下面的语句进行同步:

Map m = Collections.synchronizeMap(hashMap);

8为啥我们重写equals方法的时候需要重写hashCode方法呢

hashmap中value的查找是通过 key 的 hashcode 来查找,所以对自己的对象必须重写 hashcode 方法通过 hashcode 找到对象地址后会用 equals 比较你传入的对象和 hashmap 中的 key 对象是否相同,因此还要重写 equals。

9HashMap什么时候进行扩容它是怎么扩容的呢

HashMap进行扩容取决于以下两个元素:

Capacity:HashMap当前长度。

LoadFactor:负载因子,默认值0.75f。

当Map中的元素个数(包括数组,链表和红黑树中)超过了16*0.75=12之后开始扩容。

具体怎么进行扩容呢?将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing ,因为它将会调用hash方法找到新的bucket位置。

10JDK1.7扩容的时候为什么要重新Hash呢,为什么不直接复制过去

是因为长度扩大以后,Hash的规则也随之改变。比如原来长度(Length)是8,你位运算出来的值是2 ,新的长度是16你位运算出来的值明显不一样了。

11HashMap和Hashtable的区别是什么

①、HashMap是线程不安全的,HashTable是线程安全的;

②、由于线程安全,所以HashTable的效率比不上HashMap;

③、HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而HashTable不允许;

④、HashMap默认初始化数组的大小为16,HashTable为11,前者扩容时,扩大两倍,后者扩大两倍+1;

⑤、HashMap需要重新计算hash值,而HashTable直接使用对象的hashCode;

12什么是Java集合中的快速失败(fast-fail)机制

     快速失败是Java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast。

     举个例子:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就可能会抛出 ConcurrentModificationException异常,从而产生fast-fail快速失败。

那么快速失败机制底层是怎么实现的呢

     迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedModCount值,是的话就返回遍历;否则抛出异常,终止遍历。

     看异常ConcurrentModificationException,JDK中是这么介绍该异常的:当检测到一个并发的修改,就可能会抛出该异常,一些迭代器的实现会抛出该异常,以便可以快速失败。但是你不可以为了便捷而依赖该异常,而应该仅仅作为一个程序的侦测。

13HashTable一定是线程安全吗?它会有快速失败的时候吗?

Hashtable是线程安全的,它的每个方法中都加入了Synchronize方法。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步

14为什么String, Interger这样的wrapper类适合作为键

String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。

     因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

15HashMap的工作原理

HashMap底层是hash数组和单向链表实现,数组中的每个元素都是链表,由Node内部类(实现Map.Entry<K,V>接口)实现,HashMap通过put&get方法存储和获取。

存储对象时,将K/V键值传给put()方法:

①、调用hash(K)方法计算K的hash值,然后结合数组长度,计算得数组下标;

②、调整数组大小(当容器中的元素个数大于capacity*loadfactor时,容器会进行扩容resize为2n);

i.如果K的hash值在HashMap中不存在,则执行插入,若存在,则发生碰撞;

ii.如果K的hash值在HashMap中存在,且它们两者equals返回true,则更新键值对;

iii.如果K的hash值在HashMap中存在,且它们两者equals返回false,则插入链表的尾部(尾插法)或者红黑树中(树的添加方式)。

(JDK1.7之前使用头插法、JDK1.8使用尾插法)

(注意:当碰撞导致链表大于TREEIFY_THRESHOLD=8时,就把链表转换成红黑树)

获取对象时,将K传给get()方法:

①、调用hash(K)方法(计算K的hash值)从而获取该键值所在链表的数组下标;

②、顺序遍历链表,equals()方法查找相同Node链表中K值对应的V值。

hashCode是定位的,存储位置;equals是定性的,比较两者是否相等。

16当两个对象的hashCode相同会发生什么

因为hashCode相同,不一定就是相等的(equals方法比较),所以两个对象所在数组的下标相同,”碰撞”就此发生。又因为HashMap使用链表存储对象,这个Node会存储到链表中。

17HashMap的table的容量如何确定loadFactor是什么该容量如何变化这种变化会带来什么问题

①、table数组大小是由capacity这个参数确定的,默认是16,也可以构造时传入,最大限制是1<<30;

②、loadFactor是装载因子,主要目的是用来确认table数组是否需要动态扩展,默认值是0.75,比如table数组大小为16,装载因子为0.75时,threshold就是12,当table的实际大小超过12时,table就需要动态扩容;

③、扩容时,调用resize()方法,将table长度变为原来的两倍(注意是table长度,而不是threshold)

④、如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。

18HashMap中put方法的过程

·调用哈希函数获取Key对应的hash值,再计算其数组下标;

·如果没有出现哈希冲突,则直接放入数组;如果出现哈希冲突,则以链表的方式放在链表后面;

·如果链表长度超过阀值(TREEIFYTHRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表;

·如果结点的key已经存在,则替换其value即可;

·如果集合中的键值对大于12,调用resize方法进行数组扩容。

19数组扩容的过程

创建一个新的数组,其容量为旧数组的两倍,并重新计算旧数组中结点的存储位置。结点在新数组中的位置只有两种,原下标位置或原下标+旧数组的大小。

什么时候才需要扩容

当HashMap中的元素个数超过数组大小(数组长度)*loadFactor(负载因子)时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)是0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中的元素个数超过16×0.75=12(这个值就是阈值或者边界值threshold值)的时候,就把数组的大小扩展为2×16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预知元素的个数能够有效的提高HashMap的性能。

补充:

当HashMap中的其中一个链表的对象个数如果达到了8个,此时如果数组长度没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链表会变成红黑树,结点类型由Node变成TreeNode类型。当然,如果映射关系被移除后,下次执行resize方法时判断树的结点个数低于6,也会再把树转换为链表。

HashMap的扩容是什么

进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。

HashMap在进行扩容时,使用的rehash方式非常巧妙,因为每次扩容都是翻倍,与原来计算的 (n-1)&hash的结果相比,只是多了一个bit位,所以结点要么就在原来的位置,要么就被分配到”原位置+旧容量”这个位置。

  

20拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树为什么不一直使用红黑树

之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持”平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

21jdk8中对HashMap做了哪些改变

·在java1.8中,如果链表的长度超过了8,那么链表将转换为红黑树。(桶的数量必须大于64,小于64的时候只会扩容)

·发生hash碰撞时,java1.7会在链表的头部插入,而java1.8会在链表的尾部插入

·在java1.8中,Entry被Node替代(换了一个马甲)。

22HashMap,LinkedHashMap,TreeMap有什么区别

·LinkedHashMap保存了记录的插入顺序,在用Iterator遍历时,先取到的记录肯定是先插入的;遍历比HashMap慢;

·TreeMap实现SortMap接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)

23HashMap&TreeMap&LinkedHashMap使用场景

一般情况下,使用最多的是HashMap。

HashMap: 在Map中插入、删除和定位元素时;

TreeMap: 在需要按自然顺序或自定义顺序遍历键的情况下;

LinkedHashMap: 在需要输出的顺序和输入的顺序相同的情况下。

24HashMap线程安全方面会出现什么问题

扩容死链(JDK1.7)

数据错乱(JDK1.7/8)

25为什么HashMap的底层数组长度为何总是2的n次方

1、HashMap的长度是2的次幂的话,可以让数据更散列更均匀的分布,更充分的利用数组的空间

2、HashMap的长度一定是2的次幂,在扩容迁移的时候不需要再重新通过哈希定位新的位置了。扩容后,元素新的位置,要么在原脚标位,要么在原脚标位+扩容长度这么一个位置。

26jdk1.8中做了哪些优化优化

1、数组+链表改成了数组+链表或红黑树

2、链表的插入方式从头插法改成了尾插法

3、扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小

4、在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容

27HashMap在JDK1.7和JDK1.8中有哪些不同

JDK1.8主要解决或优化了一下问题:

1、resize 扩容优化

2、引入了红黑树,目的是避免单条链表过长而影响查询效率

3、解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题

不同

JDK 1.7

JDK 1.8

存储结构

数组 + 链表

数组 +链表 +红黑树

初始化方式

单独函数: inflateTab le0

直接集成到了扩容 函数 resize0)中

hash值计算方式

扰动处理 = 9次扰动 = 4次位运算+ 5次异或运算

扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算

存放数据的规则

无冲突时,存放 数组;冲 突时存放链表

无冲突时,存放 数组;冲 突&链表 长度<8:存放单链表:冲 突& 链表长度>8:树化并存放红黑树

插入数据方式

头插法(先讲原 位置的数 据移到后1 位,再插入数据到 该位置)

尾插法(直接插 入到链表 尾部/红黑 树

扩容后存储位置的计算方式

全部按照 原来方法 进行计算(即hashCode ->> 扰动 函数->>(h&lengt h-1))

按照扩容 后的规律 计算 (即 扩容后的 位置=原位置 or 原位置 + 日容 量)

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: HashMapJava中的一个常用数据结构,它的底层是由hash数组和单向链表实现的。每个数组元素都是一个链表,通过Node内部类实现了Map.Entry接口来存储键值对。HashMap通过put和get方法来存储和获取数据。\[1\] 在重写equals方法时,我们需要同时重写hashCode方法。这是因为在HashMap中,查找value是通过key的hashCode来进行的。当找到对应的hashCode后,会使用equals方法来比较传入的对象和HashMap中的key对象是否相同。因此,为了保证正确的查找和比较,我们需要同时重写equals和hashCode方法。\[2\]\[3\] HashMap在什么时候进行扩容呢?当HashMap中的元素数量超过了负载因子(默认为0.75)与当前容量的乘积时,就会进行扩容。扩容是为了保持HashMap的性能,因为当元素数量过多时,链表的长度会变长,查找效率会下降。扩容的过程是创建一个新的数组,将原数组中的元素重新分配到新数组中,然后将新数组替换为原数组。\[3\] #### 引用[.reference_title] - *1* *2* *3* [史上最全Hashmap面试总结,51道附带答案,持续更新中...](https://blog.csdn.net/androidstarjack/article/details/124507171)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值