1. 你用过HashMap吗, 什么是HashMap? 为什么用到它?
用过, 然后可以接着回答一些HashMap的特性, 比如HashMap可以接收null键值和值, 而HashTable则不能, HashMap是非synchronized的; HashMap很快, 以及HashMap存储的是键值对等.
2. 你知道HashMap的工作原理吗? 你知道HashMap的get() 方法的工作原理吗?
HashMap是基于hashing的原理, 我们使用put(key, value) 存储对象到HashMap中, 使用get(key) 从HashMap中获取对象, 当我们给put() 方法传键和值时, 我们先对键调用hashCode()方法, 返回的hashCode用于找到bucket的位置来存储entry对象.
这里关键点在于指出, HashMap是在bucket中存储键对象和值对象, 作为Map.Entry 这一点有助于理解获取对象的逻辑. 如果你没有意识到这一点, 或者错误的认为仅仅只在bucket中存储值的话, 你将不会回答如何从HashMap中获取对象的逻辑.
3. 当两个对象的hashCode相同会发生什么?
这个问题一般是分界点了. 一般同学都会回答, 因为hashCode相同, 所以两个对象是相等的, HashMap将会抛出异常, 或者不会存储它们.
这个时候, 如果面试官对你之前的回答比较满意, 可能会提醒你他们有equals()和hashCode(0两个方法, 并告诉他们两个对象就算hashCode相同, 但是他们可能并不相等. 如果掌握的不太好, 可能这个问题就会不了了之.
而这个题的答案是: 因为hashCode相同, 所以他们的bucket位置相同, 碰撞会发生, 因为HashMap使用链表存储对象, 这个Entry(包含有键值对的Map.Entry)会存储在链表中, 这个时候要根据hashCode来划分数组, 如果数组的坐标相同, 则进入链表这个数据结构中了, 一般的添加都在最前面, 也就是和数组下标直接相连的地方, 链表长度到达8的时候, JDK1.8上升为红黑树. 这样回答基本就ok了, 要不要接着往下问, 就看面试官了.
4. 如果两个键的hashCode相同, 你如何获取值对象?
同学们一般会说 : 当我们调用get(0 方法, HashMap会使用键对象的hashcode找到bucket位置, 然后获取值对象, 面试官会提醒, 如果有两个值对象存储在同一个bucket, 面试者会说, 将会遍历链表直到找到值对象. 面试官会问, 你没有值对象去比较, 怎么确定找到值对象的. 这个题的重点是, 找到bucket位置后, 调用keys.value() 方法去找链表中正确的结点, 最终找到要找的值对象.
正确的完整回答应该是这样 :
当我们调用get() 方法, HashMap会使用键对象的hashCode找到bucket位置, 然后获取值对象. 如果有两个值对象存储在同一个bucket, 将会遍历LinkedList知道找到值对象, 找到bucket位置之后, 会调用keys.equals() 方法找到LinkedList中正确的结点, 最终找到要找的值对象. ( 当程序通过key取到对应的value时, 系统只要先计算出该key的hashCode() 返回值, 在根据该hashCode返回值找出该key在table数组中的索引, 然后取出该索引除的Entry, 最后返回该key对应的value即可)
5. 如果HashMap的大小超过了负载因子(load factor)定义的容量, 该怎么办 ?
默认的负载因子是0.75, 也就是说, 当一个map填满了75%的bucket时, 和其他集合类一样, 就会创建原来HashMap大小的两倍的bucket数组, 来重新调整map的大小, 并将原来的对象放入新的bucket数组中, 这个过程叫rehashing, 因为他调用hash方法找到新的bucket位置.
6. 你了解重新调整HashMap大小存在什么问题吗?
如果这个时候答不出来, 面试官可能会提醒你当多线程的情况下, 可能会产生条件竞争
当重新调整HahMap时, 确实存在条件竞争, 因为如果两个线程都发现HashMap需要重新调整大小了, 他们会试着同时调整大小, 在调整大小的过程中, 存储在链表中的元素的次序会反过来, 因为移动到新的bucket位置的时候, HashMap并不会将元素放在链表的尾部, 而是放在头部, 这是为了避免尾部遍历. 如果条件竞争发生了, 那么就死循环了. 这个时候, 你可以质问面试官, 问什么这么奇怪, 要在多线程的环境下使用HashMap
7. 为什么String, Integer这样的wrapper类适合作为键?
两者都非常适合做键, 而且String更常用. 因为String是不可变的, 也是final的. 而且已经重写了equals()和hashcode()方法, 其他的wrapper类也有这个特点. 不可变性是非常重要的, 因为为了要计算hashCode(), 就要防止键值变化, 如果键值放入时和获取时返回不同的hashcode的话, 就不能从HashMap中找到你想要的对象, 不可变性还有其他的优点, 如线程安全. 如果你可以仅仅通过将某个field声明成final就能保证hashCode时不可变的, 那就这么做. 因为获取对象的时候要用到equals()和hashCode() 方法, 那么键对象正确的重写这两个方法时非常重要的, 如果两个不相等的对象返回不同的hashCode的话, 那么碰撞的几率就会小一些, 这样就能提高HashMap的性能.
8. 我们可以使用自定义的对象作为键吗?
当然可以使用任何对象作为键, 只要它遵守了equals() 和hashCode() 方法的定义规则, 并且当对象插入到Map中后就不能再改变了. 如果这个自定义对象是不可变的, 那么它已经满足了作为键的条件, 因为当它创建之后就已经不可能改变了.
9. 我们可以使用ConcurrentHashMap来代替HashMap吗?
我们知道HashTable是synchronized的, 但是ConcurrentHashMap同步性能更好, 因为它仅仅根据同步级别堆map的一部分进行上锁. ConcurrentHashMap当然可以代替HashTable, 但是HashTable提供更强的线程安全性
以上问题涉及到的知识点 :
HashMap的工作原理
HashMap基于hashing原理, 通过put() 和get()方法存储和获取对象, 当我们将键值对传递给put() 方法时, 它调用键对象的hashCode(0 方法来计算hashCode, 然后找到bucket的位置来存储对象, 当获取对象时, 通过对象的equals() 方法找到正确的键值对, 然后返回值对象. HashMap使用链表来解决碰撞问题, 当发生碰撞了, 对象将会存储再链表的下一个结点中, HashMap在每个链表结点中存储键值对象
当两个不同的键对象的hashCode相同时, 他们会存储在同一个bucket位置的链表中, 键对象的equals() 方法用来找到键值对.