HashMap笔试面试题汇总解析

来源:互联网 发布:访客网络会影响网速么 编辑:程序博客网 时间:2024/05/16 14:25

面试题: Java中ArrayList和LinkedList的主要区别是什么?

这个问题首先要知道数组和链表的特点

数组的特点:寻址容易,插入和删除困难。

链表的特点是:寻址困难,插入和删除容易。

ArrayList的底层实现就是通过动态数组来实现的,LinkedLIst底层实现就是通过链表来实现的,所以直接答出数组和链表的特点就ok

面试题: hashMap是怎样实现key-value这样键值对的保存?

HashMap中有一个内部类Entry,

[html] view plaincopy在CODE上查看代码片派生到我的代码片
  1.  static class Entry<K,V> implements Map.Entry<K,V> {  
  2.         final K key;  
  3.         V value;  
  4.         Entry<K,V> next;  
  5.         int hash;  
  6.         //.....  
  7. }  
主要有4个属性,key ,hash,value,指向下一个节点的引用next ,看到这个实体类就明白了,在HashMap中存放的key-value实质是通过实体类Entry来保存的

面试题: hashMap的实现原理?

HashMap使用到的数据类型主要就是数组和链表,首先看原理图


在hashMap的原理图中,左边是通过数组来存放链表的第一个节点,看懂这个图这个问题就ok

面试题: hashMap的put过程?

面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。比如说: 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。也就是说数组中存储的是最后插入的元素。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public V put(K key, V value) {  
  2.    if (key == null)  
  3.        return putForNullKey(value);  
  4.    int hash = hash(key);  
  5.    int i = indexFor(hash, table.length);  
  6.    for (Entry<K,V> e = table[i]; e != null; e = e.next) {<span style="color:#ff0000;">//循环判断插入的key是否已经存在,若存在就更新key对应的value</span>  
  7.        Object k;  
  8.        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  9.            V oldValue = e.value;  
  10.            e.value = value;  
  11.            e.recordAccess(this);  
  12.            return oldValue;  
  13.        }  
  14.    }  
  15.   
  16.    modCount++;  
  17.    addEntry(hash, key, value, i)<span style="color:#ff0000;">;//key不存在,那么插入新的key-value</span>  
  18.    return null;  
[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.     if ((size >= threshold) && (null != table[bucketIndex])) {  
  3.         resize(2 * table.length);  
  4.         hash = (null != key) ? hash(key) : 0;  
  5.         bucketIndex = indexFor(hash, table.length);  
  6.     }  
  7.   
  8.     createEntry(hash, key, value, bucketIndex);  
  9. }  
  10.   
  11. void createEntry(int hash, K key, V value, int bucketIndex) {//这个方法就验证了上面说的<strong><span style="color:#ff0000;">数组中存储的是最后插入的元素</span></strong>  
  12.     Entry<K,V> e = table[bucketIndex];  
  13.     table[bucketIndex] = new Entry<>(hash, key, value, e);  
  14.     size++;  
  15. }  
面试题: hashMap的get过程?
这个过程比较简单,直接看代码:
[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public V get(Object key) {  
  2.         if (key == null)  
  3.             return getForNullKey();  
  4.         int hash = hash(key.hashCode());  
  5.         //先定位到数组元素,再遍历该元素处的链表  
  6.         for (Entry<K,V> e = table[indexFor(hash, table.length)];  
  7.              e != null;  
  8.              e = e.next) {  
  9.             Object k;  
  10.             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
  11.                 return e.value;  
  12.         }  
  13.         return null;  
  14. }  
面试题: hashMap存取的时候是如何定位数组下标的?
找到indexFor这个方法,hashMap就是通过这个方法获取数组下标的:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. static int indexFor(int h, int length) {  
  2.         return h & (length-1);  
  3.     }  
通过key的hash值和数组长度求&,这意味着数组下标相同,并不表示hashCode相同。所以在Entry中存有一个hash值,在比较Entry的时候都是想比较hash值
面试题: hashMap中数组的初始化大小过程?

找到hashMap的构造方法:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public HashMap(int initialCapacity, float loadFactor) {  
  2.        .....// Find a power of 2 >= initialCapacity  
  3.        int capacity = 1;  
  4.        while (capacity < initialCapacity)  
  5.            capacity <<= 1;  //相当于capacity = capacity * 2  
  6.        this.loadFactor = loadFactor;  
  7.        threshold = (int)(capacity * loadFactor);  
  8.        table = new Entry[capacity];  
  9.        init();  
  10.    }  
从上面的代码我们可以看出数组的初始大小并不是构造函数中的initialCapacity!!而是2的n次方
面试题: hashMap什么时候开始rehash?

在hashMap中有一个加载因子loadFactor,默认值是0.75,当数组的实际存入值的大小 > 数组的长度×loadFactor 时候就会rehash,重新创建一个新的表,将原表的映射到新表中,这个过程很费时。




HashMap笔试面试题汇总解析



在笔试和面试的过程中,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,23------> m-1 时,称谓线性探测再散列

(2)d=1^2-1^2---->+(-)k^2时,称为二次线性再散列。

(3)d为伪随即序列时,称为伪随即序列再散列。

B .再哈希法 

H=RH(key)RH()为不同的哈希函数,即在地址冲突时计算另一个哈希函数地址,直到不再发生冲突。

C .链地址法

将所有哈希地址冲突的记录存储在同一个线性链表中

公共溢出区法

将所有哈希地址冲突的记录都填入到溢出表中

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);  //和除留余数等价

}

当我们往HashMapput元素的时候,先根据keyhashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾数组中存储的是最后插入的元素 。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

以上是有关HashMap底层实现的说明,几乎所有有关HashMap的题目都是基于上述理论展看的。下面是收集到的一些HashMap相关的笔试面试题的解析:

1.HashMapHashtable的区别:

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.ConcurrentHashMapHashtable的区别

HashtableConcurrentHashMap有什么分别呢?它们都可以用于多线程的环境,但是当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这两种遍历方法是分别对keysetentryset来进行遍历,但是对于keySet其实是遍历了2次,一次是转为iterator,一次就从hashmap中取出key所对于的value。而entryset只是遍历了第一次,它把keyvalue都放到了entry,即键值对,所以就快了。

先总结到这吧,以后遇到有价值的hashmap相关的题目日后再做更新,文章不足之处,反应各位拍砖指正。。。。

 

 

 

 

 

 

 

 

 

 

 


0 0
原创粉丝点击