java集合相关之HashMap

来源:互联网 发布:快反部队知乎 编辑:程序博客网 时间:2024/05/17 08:08

HashMap的工作原理是近年来常见的Java面试题。几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道Hashtable和HashMap之间的区别,那么为何这道面试题如此特殊呢?是因为这道题考察的深度很深。这题经常出现在高级或中高级面试中。投资银行更喜欢问这个问题,甚至会要求你实现HashMap来考察你的编程能力。ConcurrentHashMap和其它同步集合的引入让这道题变得更加复杂。让我们开始探索的旅程吧!

先来些简单的问题

“你用过HashMap吗?” “什么是HashMap?你为什么用到它?”

几乎每个人都会回答“是的”,然后回答HashMap的一些特性,譬如HashMap可以接受null键值和值,而Hashtable则不能;HashMap是非synchronized;HashMap很快;以及HashMap储存的是键值对等等。这显示出你已经用过HashMap,而且对它相当的熟悉。但是面试官来个急转直下,从此刻开始问出一些刁钻的问题,关于HashMap的更多基础的细节。面试官可能会问出下面的问题:

“你知道HashMap的工作原理吗?” “你知道HashMap的get()方法的工作原理吗?”

你也许会回答“我没有详查标准的Java API,你可以看看Java源代码或者Open JDK。”“我可以用Google找到答案。”

但一些面试者可能可以给出答案,“HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。”这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Entry。这一点有助于理解获取对象的逻辑。如果你没有意识到这一点,或者错误的认为仅仅只在bucket中存储值的话,你将不会回答如何从HashMap中获取对象的逻辑。这个答案相当的正确,也显示出面试者确实知道hashing以及HashMap的工作原理。但是这仅仅是故事的开始,当面试官加入一些Java程序员每天要碰到的实际场景的时候,错误的答案频现。下个问题可能是关于HashMap中的碰撞探测(collision detection)以及碰撞的解决方法:

“当两个对象的hashcode相同会发生什么?” 从这里开始,真正的困惑开始了,一些面试者会回答因为hashcode相同,所以两个对象是相等的,HashMap将会抛出异常,或者不会存储它们。然后面试官可能会提醒他们有equals()和hashCode()两个方法,并告诉他们两个对象就算hashcode相同,但是它们可能并不相等。一些面试者可能就此放弃,而另外一些还能继续挺进,他们回答“因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。”这个答案非常的合理,虽然有很多种处理碰撞的方法,这种方法是最简单的,也正是HashMap的处理方法。但故事还没有完结,面试官会继续问:

“如果两个键的hashcode相同,你如何获取值对象?” 面试者会回答:当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。面试官提醒他如果有两个值对象储存在同一个bucket,他给出答案:将会遍历链表直到找到值对象。面试官会问因为你并没有值对象去比较,你是如何确定确定找到值对象的?除非面试者直到HashMap在链表中存储的是键值对,否则他们不可能回答出这一题。

其中一些记得这个重要知识点的面试者会说,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。完美的答案!

许多情况下,面试者会在这个环节中出错,因为他们混淆了hashCode()和equals()方法。因为在此之前hashCode()屡屡出现,而equals()方法仅仅在获取值对象的时候才出现。一些优秀的开发者会指出使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。

如果你认为到这里已经完结了,那么听到下面这个问题的时候,你会大吃一惊。“如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?”除非你真正知道HashMap的工作原理,否则你将回答不出这道题。默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

如果你能够回答这道问题,下面的问题来了:“你了解重新调整HashMap大小存在什么问题吗?”你可能回答不上来,这时面试官会提醒你当多线程的情况下,可能产生条件竞争(race condition)。

当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。这个时候,你可以质问面试官,为什么这么奇怪,要在多线程的环境下使用HashMap呢?:)

热心的读者贡献了更多的关于HashMap的问题:

为什么String, Interger这样的wrapper类适合作为键? String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

我们可以使用自定义的对象作为键吗? 这是前一个问题的延伸。当然你可能使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。

我们可以使用CocurrentHashMap来代替Hashtable吗?这是另外一个很热门的面试题,因为ConcurrentHashMap越来越多人用了。我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。看看这篇博客查看Hashtable和ConcurrentHashMap的区别。

我个人很喜欢这个问题,因为这个问题的深度和广度,也不直接的涉及到不同的概念。让我们再来看看这些问题设计哪些知识点:

hashing的概念

HashMap中解决碰撞的方法

equals()和hashCode()的应用,以及它们在HashMap中的重要性

不可变对象的好处

HashMap多线程的条件竞争

重新调整HashMap的大小

总结

HashMap的工作原理

HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。

当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。

因为HashMap的好处非常多,我曾经在电子商务的应用中使用HashMap作为缓存。因为金融领域非常多的运用Java,也出于性能的考虑,我们会经常用到HashMap和ConcurrentHashMap。


HashMap对HashCode碰撞的处理

先说Java之外的,什么是拉链法?怎么解决冲突的:

拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组t[0..m-1]。凡是散列地址为i的结点,均插入到以t为头指针的单链表中。t中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。

换句话说:HashCode是使用Key通过Hash函数计算出来的,由于不同的Key,通过此Hash函数可能会算的同样的HashCode,所以此时用了拉链法解决冲突,把HashCode相同的Value连成链表. 但是get的时候根据Key又去桶里找,如果是链表说明是冲突的,此时还需要检测Key是否相同





在解释下,Java中HashMap是利用“拉链法”处理HashCode的碰撞问题。在调用HashMap的put方法或get方法时,都会首先调用hashcode方法,去查找相关的key,当有冲突时,再调用equals方法。hashMap基于hasing原理,我们通过put和get方法存取对象。当我们将键值对传递给put方法时,他调用键对象的hashCode()方法来计算hashCode,然后找到bucket(哈希桶)位置来存储对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当碰撞发生了,对象将会存储在链表的下一个节点中。hashMap在每个链表节点存储键值对对象。当两个不同的键却有相同的hashCode时,他们会存储在同一个bucket位置的链表中。键对象的equals()来找到键值对。HashMap的put和get方法源码如下:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. /** 
  2.      * Returns the value to which the specified key is mapped, 
  3.      * or if this map contains no mapping for the key. 
  4.      * 
  5.      * 获取key对应的value 
  6.      */  
  7.     public V get(Object key) {  
  8.         if (key == null)  
  9.             return getForNullKey();  
  10.     //获取key的hash值  
  11.         int hash = hash(key.hashCode());  
  12.     // 在“该hash值对应的链表”上查找“键值等于key”的元素  
  13.         for (Entry<K,V> e = table[indexFor(hash, table.length)];  
  14.              e != null;  
  15.              e = e.next) {  
  16.             Object k;  
  17.             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
  18.                 return e.value;  
  19.         }  
  20.         return null;  
  21.     }  
  22.   
  23.     /** 
  24.      * Offloaded version of get() to look up null keys.  Null keys map 
  25.      * to index 0.   
  26.      * 获取key为null的键值对,HashMap将此键值对存储到table[0]的位置 
  27.      */  
  28.     private V getForNullKey() {  
  29.         for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
  30.             if (e.key == null)  
  31.                 return e.value;  
  32.         }  
  33.         return null;  
  34.     }  
  35.   
  36.     /** 
  37.      * Returns <tt>true</tt> if this map contains a mapping for the 
  38.      * specified key. 
  39.      * 
  40.      * HashMap是否包含key 
  41.      */  
  42.     public boolean containsKey(Object key) {  
  43.         return getEntry(key) != null;  
  44.     }  
  45.   
  46.     /** 
  47.      * Returns the entry associated with the specified key in the 
  48.      * HashMap.   
  49.      * 返回键为key的键值对 
  50.      */  
  51.     final Entry<K,V> getEntry(Object key) {  
  52.         //先获取哈希值。如果key为null,hash = 0;这是因为key为null的键值对存储在table[0]的位置。  
  53.         int hash = (key == null) ? 0 : hash(key.hashCode());  
  54.         //在该哈希值对应的链表上查找键值与key相等的元素。  
  55.         for (Entry<K,V> e = table[indexFor(hash, table.length)];  
  56.              e != null;  
  57.              e = e.next) {  
  58.             Object k;  
  59.             if (e.hash == hash &&  
  60.                 ((k = e.key) == key || (key != null && key.equals(k))))  
  61.                 return e;  
  62.         }  
  63.         return null;  
  64.     }  
  65.   
  66.   
  67.     /** 
  68.      * Associates the specified value with the specified key in this map. 
  69.      * If the map previously contained a mapping for the key, the old 
  70.      * value is replaced. 
  71.      * 
  72.      * 将“key-value”添加到HashMap中,如果hashMap中包含了key,那么原来的值将会被新值取代 
  73.      */  
  74.     public V put(K key, V value) {  
  75.     //如果key是null,那么调用putForNullKey(),将该键值对添加到table[0]中  
  76.         if (key == null)  
  77.             return putForNullKey(value);  
  78.     //如果key不为null,则计算key的哈希值,然后将其添加到哈希值对应的链表中  
  79.         int hash = hash(key.hashCode());  
  80.         int i = indexFor(hash, table.length);  
  81.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  82.             Object k;  
  83.     //如果这个key对应的键值对已经存在,就用新的value代替老的value。  
  84.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  85.                 V oldValue = e.value;  
  86.                 e.value = value;  
  87.                 e.recordAccess(this);  
  88.                 return oldValue;  
  89.             }  
  90.         }  
  91.   
  92.         modCount++;  
  93.         addEntry(hash, key, value, i);  
  94.         return null;  
  95.     }  

从HashMap的put()和get方法实现中可以与拉链法解决hashCode冲突解决方法相互印证。并且从put方法中可以看出HashMap是使用Entry<K,V>来存储数据。数据节点Entry的数据结构如下:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // Entry是单向链表。  
  2.    // 它是 “HashMap链式存储法”对应的链表。  
  3.    // 它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数  
  4.    static class Entry<K,V> implements Map.Entry<K,V> {  
  5.        final K key;  
  6.        V value;  
  7. //指向下一个节点  
  8.        Entry<K,V> next;  
  9.        final int hash;  
  10.   
  11.        /** 
  12.         * Creates new entry. 
  13. * 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)" 
  14.         */  
  15.        Entry(int h, K k, V v, Entry<K,V> n) {  
  16.            value = v;  
  17.            next = n;  
  18.            key = k;  
  19.            hash = h;  
  20.        }  
  21.   
  22.        public final K getKey() {  
  23.            return key;  
  24.        }  
  25.   
  26.        public final V getValue() {  
  27.            return value;  
  28.        }  
  29.   
  30.        public final V setValue(V newValue) {  
  31.     V oldValue = value;  
  32.            value = newValue;  
  33.            return oldValue;  
  34.        }  
  35.       
  36.        // 判断两个Entry是否相等  
  37.        // 若两个Entry的“key”和“value”都相等,则返回true。  
  38.        // 否则,返回false  
  39.        public final boolean equals(Object o) {  
  40.            if (!(o instanceof Map.Entry))  
  41.                return false;  
  42.            Map.Entry e = (Map.Entry)o;  
  43.            Object k1 = getKey();  
  44.            Object k2 = e.getKey();  
  45.            if (k1 == k2 || (k1 != null && k1.equals(k2))) {  
  46.                Object v1 = getValue();  
  47.                Object v2 = e.getValue();  
  48.                if (v1 == v2 || (v1 != null && v1.equals(v2)))  
  49.                    return true;  
  50.            }  
  51.            return false;  
  52.        }  
  53.   
  54.        public final int hashCode() {  
  55.            return (key==null   ? 0 : key.hashCode()) ^  
  56.                   (value==null ? 0 : value.hashCode());  
  57.        }  
  58.   
  59.        public final String toString() {  
  60.            return getKey() + "=" + getValue();  
  61.        }  
  62.   
  63.        /** 
  64.         * This method is invoked whenever the value in an entry is 
  65.         * overwritten by an invocation of put(k,v) for a key k that's already 
  66.         * in the HashMap. 
  67.         */  
  68.        void recordAccess(HashMap<K,V> m) {  
  69.        }  
  70.   
  71.        /** 
  72.         * This method is invoked whenever the entry is 
  73.         * removed from the table. 
  74.         */  
  75.        void recordRemoval(HashMap<K,V> m) {  
  76.        }  
  77.    }  

0 0
原创粉丝点击