在笔试和面试的过程中,Java集合框架毫无疑问是考察的重点,貌似面试官对这都情有独钟,而有关HashMap的考察更是重中之重和难点,一个小小的HashMap不仅能反应出你对Java集合的掌握程度,更能反映出面试者对数据结构的熟悉情况和设计数据结构的思维能力,很容易在这个问题上尴尬地看着面试官发呆,搞不好就拜拜了。根据个人对这个问题的理解和一些网上的资料在这里做个汇总解析,与各位共享。废话不多说,要想搞透彻HashMap,那咱们就先从HashMap的原理说起。
1.HashMap与哈希表
HashMap从本质上说就是哈希表,其底层实现就是围绕哈希表展看的,搞明白了这个很多问题就很容易理解了。那哈希表又是什么东东呢?
哈希表的核心思想就是让记录的关键字和存储位置建立一一映射关系,这样我们就可以通过Key直接获得相对应的Value,好比我们通过索引可以直接获得数组对应的某个值一样,而这种一一映射关系要通过某个数学函数来构造出来,这个函数就是所谓的哈希函数。
而哈希函数有五种实现方式:
A. 直接定址法:取关键字的线性函数值作为哈希地址。
B. 数字分析法:取关键字的中的若干位作为哈希地址。
C. 平方取中法:取关键字平方后的中间几位作为哈希地址。
D. 折叠法:将关键字分割成位数相同的几部分(最后一部分可以不同),然后取这几部分的叠加和作为哈希地址。
E. 除留余数法:H(key) = key MOD p ,p<=m ,m为不大于哈希表的数。
F. 随机函数法
上述五中实现方式中最常用的是除留余数法,而通过哈希函数寻址的过程可能出现“冲突”------即若干个不同的key却对应相同的哈希地址。解决哈希冲突有如下的方法:
A. 开放地址法:H=(H(kyt)+d) MOD m ,m为哈希表表长。
(1)d=1,2,3------> m-1 时,称谓线性探测再散列
(2)d=1^2,-1^2---->+(-)k^2时,称为二次线性再散列。
(3)d为伪随即序列时,称为伪随即序列再散列。
B .再哈希法
H=RH(key),RH()为不同的哈希函数,即在地址冲突时计算另一个哈希函数地址,直到不再发生冲突。
C .链地址法
将所有哈希地址冲突的记录存储在同一个线性链表中
D 公共溢出区法
将所有哈希地址冲突的记录都填入到溢出表中
而HashMap的实现与哈希函数的选择和哈希地址冲突的解决方案密切相关,详情继续看下文。
2.HashMap的具体实现
HashMap的实现采用了除留余数法形式的哈希函数和链地址法解决哈希地址冲突的方案。这样就涉及到两种基本的数据结构:数组和链表。数组的索引就是对应的哈希地址,存放的是链表的头结点即插入链表中的最后一个元素,链表存放的是哈希地址冲突的不同记录。
链表的结点设计如下:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
}
next作为引用指向下一个记录。在HashMap中设计了一个Entry类型的数组用来存放Entry的实例即链表结点。
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table;
除留余数法形式的哈希函数:
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1); //和除留余数等价
}
当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾,数组中存储的是最后插入的元素 。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
以上是有关HashMap底层实现的说明,几乎所有有关HashMap的题目都是基于上述理论展看的。下面是收集到的一些HashMap相关的笔试面试题的解析:
1.HashMap与Hashtable的区别:
HashMap可以接受null键值和值,而Hashtable则不能。
Hashtable是线程安全的,通过synchronized实现线程同步。而HashMap是非线程安全的,但是速度比Hashtable快。
2.HashMap的原理
参见上文中的HashMap的具体实现
3.当两个对象的hashcode相同怎么办
当哈希地址冲突时,HashMap采用了链地址法的解决方式,将所有哈希地址冲突的记录存储在同一个线性链表中。具体来说就是根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。
4.如果两个键的hashcode相同,你如何获取值对象
HashMap在链表中存储的是键值对,找到哈希地址位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象
5.如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办
HashMap默认的负载因子大小为0.75,也就是说,当一个map填满了75%的空间的时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的数组,来重新调整map的大小,并将原来的对象放入新的数组中。
6.为什么String, Interger这样的wrapper类适合作为键?
String, Interger这样的wrapper类是final类型的,具有不可变性,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。
7.ConcurrentHashMap和Hashtable的区别
Hashtable和ConcurrentHashMap有什么分别呢?它们都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。
8.HashMap的遍历
第一种:
Map map = new HashMap();
Iterator iter = map.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Object key = entry.getKey();
Object val = entry.getValue();
}
效率高,以后一定要使用此种方式!
第二种:
Map map = new HashMap();
Iterator iter = map.keySet().iterator();
while (iter.hasNext()) {
Object key = iter.next();
Object val = map.get(key);
}
效率低,以后尽量少使用!
可是为什么第一种比第二种方法效率更高呢?
HashMap这两种遍历方法是分别对keyset及entryset来进行遍历,但是对于keySet其实是遍历了2次,一次是转为iterator,一次就从hashmap中取出key所对于的value。而entryset只是遍历了第一次,它把key和value都放到了entry中,即键值对,所以就快了。
先总结到这吧,以后遇到有价值的hashmap相关的题目日后再做更新,文章不足之处,反应各位拍砖指正。。。。
更多内容欢迎关注个人微信公众号,一起成长!