在工作了一段时间之后,上课的时候基础已经忘得七七八八了,今天突然看到群里的朋友问了一些关于HashMap的面试题,也想着自己整理一下对HashMap的一些理解
使用过HashMap么?他有什么用处?
只要学习过Java的同学想必都用过HashMap,HashMap对于日常的工作中可以方便的存储一些我们需要存放的对象,最基础的便是get/put方法,对于HashMap的一些特性,比如HashMap是可以储存null键和null值得,而与之对应的HashTable则不行,HashMap是非线程安全的,因此它的速度会相对来说快一些,HashMap储存的是键值对等等,到此,说明在日常工作中,对HashMap的理解还是比较到位的。
你知道HashMap的工作原理吗?
- 针对第一个问题,其实HashMap最基础的就是基于Hashing的原理,
- 当我们给put方法传递<K,V>时,会首先调用hashCode方法对K进行哈希值的计算,得到哈希值后,去bucket中寻找对应的储存地址,并且把<K,V>储存进当前的储存地址。
- 当调用get方法的时候,同样是寻找相对应的hashCode对应的bucket储存地址,并取出相对应的键值对。
特别要指出的是,当K为null时,HashMap默认是储存在第一个位置当中的。
HashMap的数据结构
HashMap是综合了链表和数组的产物,先来了解一些链表和数组,这两个相对来说是两个极端的储存方式。
- 数据是有序存放数据的,占用的内存集中,因此有插件简单快速,但是插入和删除的代价大
- 链表是随机存放的,占用的内存随机,通过节点标识来形成链状结构,因此查询不易,但是插入和删除比较方便
而HashMap在处理hashCode时采用的是数组,而每个数组都相当于是链表的header。因此可以想到,在一定的条件下,HashMap和数组/List一样,是需要扩容的,会有一个reHashing的操作,然后把之前的数据全都存放到新的HashMap中。默认的负载因子是0.75.
当两个对象的hashcode相同时,调用put/get会发生什么
在刚才我们提到过,put时调用hashCode方法会找到bucket中的位置,然后再该位置进行存放,如果两个对象的hashCode相同时,会进行什么样的‘碰撞’?
有些同学可能会觉得就是直接覆盖,或者因为hashCode相同,会抛出异常,不进行储存。其实上一段说的数据结构,在这里便产生了结果。在bucket中的某一位置,对应的实际是一个链表结构,因此,put方法只是在其中又加了一节链表而已。
- 那么问题来了,这个时候我们调用get方法,获取到的其实是一个链表,而不是一个对象,难道只是简单的取最新的一个么?
- 其实不是的 ,因为在HashMap中不仅是用hashCode做判断,因为我们还会有equals方法,在链表中,我们又会采用equals方法找值相等的,就会得到唯一的一个对象。
在到达了负载因子的情况下,扩容会发生什么问题?
在工作中还是比较容易想到的,就是并发问题,在多线程的情况下,若两个线程都达到了复杂因子,他们同时产生扩容操作,便会产生条件竞争,在扩容时元素的排列顺序便会反序存放,在条件竞争时便会产生死循环。
- 解决方法:并不应该局限于如何解决这个问题。因为HashMap本身就是非线程安全的,完全可以采用并发包下的CocurrentHashMap
为什么使用String,Integer等等wapper比较合适作为K?
因为在生产环境中,这些是最常用的,并且在这些类中的hashCode和equals方法都经过了重写,并且是属于final的不可变的,因此作为K值可以有效提高查询效率。
我们可以使用CocurrentHashMap来代替Hashtable么?
这就需要看实际的业务场景了,在大多数情况下,我认为是可行的。
这两个类相较于HashMap的区别想必大家都清楚,就是这两个类都是线程安全的,但是两者之间,HashTable的线程安全等级更高一些,但是同样导致了性能的丢失。
- 具体体现在,当HashTable的大小到达一定的程度时,扩容操作就会显得非常慢,此时该Map是被上锁的,因此导致它的即时性能急剧下降,同时也就意味着,即便两个线程操作不同的数据,也会因整体上锁而需要等待。
- 而CocurrentHashMap采用了分割的概念,也就是分块治理的,因此不管整体的数据量有多大,只需要扩容部分数据,而其他部分则不会受到影响,同样的,可以理解不同线程操作不同的数据时,可以给不同的区块上锁,相当于在一定程度下允许了同时有多线程操作,有效提升了他的性能。