HashMap的底层是通过数组+链表(即哈希表)的结构来实现的。
HashMap的实例有两个参数影响其性能:初始容量 和 加载因子。
初始容量只是哈希表在创建时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目超出了加载因子与当前容量的乘积时,通过调用rehash方法将容量翻倍。
简单说下HashMap的工作原理:
HashMap基于hashing原理,我们通过put()和get()方法存储和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashCode,然后找到bucket位置来存储值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发送碰撞了,对象将会存储在链表的下一个节点中。HashMap在每个链表的节点中存储键值对对象。
面试中一些常见的问题:
“你知道HashMap的工作原理吗?” “你知道HashMap的get()方法的工作原理吗?”
答:HashMap是基于hashing的原理,我们使用put(key,value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来存储Entry对象。当获取对象时,首先根据键对象的hashCode定位到桶,然后通过键对象的equals()方法找到正确的键值对,然后返回对象。
“当两个对象的hashCode相同会发生什么?”
答:因为hashCode相同,所以它们的bucket位置相同,“碰撞”会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。
“如果两个键的hashCode相同,你如何获取值对象?”
答:首先通过键对象的hashCode定位到桶,如何调用键对象的equals()方法去找到链表中正确的节点,最终找到要找的值对象。
“如果HashMap的大小超过了负载因子定义的容量,怎么办?”
答:默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其他集合类一样(如ArrayList等),将会创建大原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫做rehashing,因为它调用了hash方法找到新的bucket位置。
“你了解重新调整HashMap大小存在什么问题吗?”
答:当重新调整HashMap大小的时候,在多线程的情况下存在竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就死循环了。
“为什么String,Integer这样的包装类适合作为主键?”
答:因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的包装类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键本身的值改变,如果键值在放入和获取时返回不同的hashCode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他优点如线程安全。
“我们可以使用自定义的对象作为键吗?”
答:当然你可以使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入Map中之后将不会再改变了。