HashMap原理与底层理解
背景
又一次去面试,还是在java上栽了些跟头。还好我心态好,面试官也挺好,大家就是交流学习嘛,每一次的面试都是个交流学习的机会,就当作他给我上课了,我能进步一点点也是好的,把每一次的面试都当作进步和查漏补缺的机会,逐步试错,逐步学习,做好了准备机会自然就到了。
HashMap的基本原理
概念
提到HashMap的概念和认识,HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null键和null值,因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。HashMap是线程不安全的。(当然这些都是最基本的,只要用过都能答到这个水平,博主就是答到的这个水平,手动苦笑~)
数据存储结构
在java7中HashMap由数组和链表来实现对数据的存储,HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。
数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;
链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。
capacity:当前数组容量,初始默认容量为16,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。(为什么必须为2的次幂?当计算索引值index = h % length 由于计算机的取余操作速度很慢,而计算机的按位取余 & 的操作非常快,又因为 h%length = h & (length-1) (需要满足length = 2^n) 因此规定了length = 2^n 加快index的计算速度 )
loadFactor:负载因子,默认为 0.75。 (loadFactor负载因子是一个非常重要的参数,因为他能够反映HashMap桶数组的使用情况, 这样的话,HashMap的时间复杂度就会出现不同的改变。当这个负载因子属于低负载因子的时候,HashMap所能够容纳的键值对数量就是偏少的,扩容后,重新将键值对 存储在桶(bucket)数组中,键与键之间产生的碰撞会下降,链表的长度也会随之变短。)
threshold:扩容的阈值,等于 capacity * loadFactor。
在java8中,利用了红黑树,所以在JAVA 8 是由数组+链表+红黑树组成。当链表中的元素超过8个之后,会将链表转换为红黑树,在这些位置进行查找的时候可降低时间复杂度 为O(logn)。
扩容(Resize)
当hashmap中的size > loadFactory * capacity即会发生扩容,扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。经过resize之后,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
添加方法 —— put()
添加键值对时,首先进行table是否初始化的判断,如果没有进行初始化(分配空间,Entry[]数组的长度)。然后进行key是否为null的判断,如果key==null ,放置在Entry[]的0号位置。计算在Entry[]数组的存储位置,判断该位置上是否已有元素,如果已经有元素存在,则遍历该Entry[]数组位置上的单链表。判断key是否存在,如果key已经存在,则用新的value值,替换点旧的value值,并将旧的value值返回。如果key不存在于HashMap中,程序继续向下执行。将key-vlaue, 生成Entry实体,添加到HashMap中的Entry[]数组中。
获取方法 —— get()
首先计算hash值,然后调用indexFor()方法得到该key在table中的存储位置,得到该位置的单链表,遍历列表找到key和指定key内容相等的Entry,返回entry.value值。
面试中的问题
1.当两个对象的hashcode相同会发生什么?
答:因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。
2.如果两个键的hashcode相同,你如何获取值对象?
答:调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。如果有两个值对象储存在同一个bucket,将会遍历LinkedList直到找到值对象。找到bucket位置之后,会调用keys.equals()方法去找到LinkedList中正确的节点,最终找到要找的值对象。
3.为什么String, Interger这样的wrapper类适合作为键?
答:String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。
4.hashCode和equals区别?
答:hashCode实际上返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。equals它的作用是判断两个对象是否相等,如果对象重写了equals()方法,比较两个对象的内容是否相等;如果没有重写,比较两个对象的地址是否相同,价于“==”。如果两个对象相等,那么它们的hashCode()值一定相同。这里的相等是指,通过equals()比较两个对象时返回true。如果两个对象hashCode()相等,它们并不一定相等。因为在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等,此时就出现所谓的哈希冲突场景。
总结
这次面试我就被HashMap的hashCode一致的话会怎么办问到了,在此记录,鞭策自己,保持进步。