深入理解Java中的HashMap的實現原理

HashMap繼承自抽象類AbstractMap,抽象類AbstractMap實現了Map接口。關係圖如下所示:


Java中的Map<key, value>接口允許我們將一個對象作爲key,也就是可以用一個對象作爲key去查找另一個對象。
在我們探討HashMap的實現原理之前,我們先自己實現了一個SimpleMap類,該類繼承自AbstractMap類。具體實現如下:

[java] view plaincopy
  1. import java.util.*;  
  2.   
  3.   
  4. public class SimpleMap<K,V> extends AbstractMap<K,V> {  
  5.     //keys存儲所有的鍵  
  6.     private List<K> keys = new ArrayList<K>();  
  7.     //values存儲所有的值  
  8.     private List<V> values = new ArrayList<V>();  
  9.       
  10.       
  11.     /** 
  12.      * 該方法獲取Map中所有的鍵值對 
  13.      */  
  14.     @Override  
  15.     public Set entrySet() {  
  16.         Set<Map.Entry<K, V>> set = new SimpleSet<Map.Entry<K,V>>();  
  17.           
  18.         //keys的size和values的size應該一直是一樣大的  
  19.         Iterator<K> keyIterator = keys.iterator();  
  20.         Iterator<V> valueIterator = values.iterator();  
  21.         while(keyIterator.hasNext() && valueIterator.hasNext()){  
  22.             K key = keyIterator.next();  
  23.             V value = valueIterator.next();  
  24.             SimpleEntry<K,V> entry = new SimpleEntry<K,V>(key, value);  
  25.             set.add(entry);  
  26.         }  
  27.           
  28.         return set;  
  29.     }  
  30.   
  31.     @Override  
  32.     public V put(K key, V value) {  
  33.         V oldValue = null;  
  34.         int index = this.keys.indexOf(key);  
  35.         if(index >= 0){  
  36.             //keys中已經存在鍵key,更新key對應的value  
  37.             oldValue = this.values.get(index);  
  38.             this.values.set(index, value);  
  39.         }else{  
  40.             //keys中不存在鍵key,將key和value作爲鍵值對添加進去  
  41.             this.keys.add(key);  
  42.             this.values.add(value);  
  43.         }  
  44.         return oldValue;  
  45.     }  
  46.       
  47.     @Override  
  48.     public V get(Object key) {  
  49.         V value = null;  
  50.         int index = this.keys.indexOf(key);  
  51.         if(index >= 0){  
  52.             value = this.values.get(index);  
  53.         }  
  54.         return value;  
  55.     }  
  56.   
  57.     @Override  
  58.     public V remove(Object key) {  
  59.         V oldValue = null;  
  60.         int index = this.keys.indexOf(key);  
  61.         if(index >= 0){  
  62.             oldValue = this.values.get(index);  
  63.             this.keys.remove(index);  
  64.             this.values.remove(index);  
  65.         }  
  66.         return oldValue;  
  67.     }  
  68.   
  69.     @Override  
  70.     public void clear() {  
  71.         this.keys.clear();  
  72.         this.values.clear();  
  73.     }  
  74.       
  75.     @Override  
  76.     public Set keySet() {  
  77.         Set<K> set = new SimpleSet<K>();  
  78.         Iterator<K> keyIterator = this.keys.iterator();  
  79.         while(keyIterator.hasNext()){  
  80.             set.add(keyIterator.next());  
  81.         }  
  82.         return set;  
  83.     }  
  84.   
  85.     @Override  
  86.     public int size() {  
  87.         return this.keys.size();  
  88.     }  
  89.   
  90.     @Override  
  91.     public boolean containsValue(Object value) {  
  92.         return this.values.contains(value);  
  93.     }  
  94.   
  95.     @Override  
  96.     public boolean containsKey(Object key) {  
  97.         return this.keys.contains(key);  
  98.     }  
  99.   
  100.     @Override  
  101.     public Collection values() {  
  102.         return this.values();  
  103.     }  
  104.   
  105. }  

當子類繼承自AbstractMap類時,我們只需要實現AbstractMap類中的entrySet方法和put方法即可,entrySet方法是用來返回該Map所有鍵值對的一個Set,put方法是實現將一個鍵值對放入到該Map中。
大家可以看到,我們上面的代碼不僅除了實現entrySet和put方法外,我們還重寫了get、remove、clear、keySet、values等諸多方法。其實我們只要重寫entrySet和put方法,該類就可以正確運行,那我們爲什麼還要重寫剩餘的那些方法呢?AbstractMap這個方法做了很多處理操作,Map中的很多方法在AbstractMap都實現了,而且很多方法都依賴於entrySet方法,舉個例子,Map接口中的values方法是讓我們返回該Map中所有的值的Collection。我們可以看一下AbstractMap中對values方法的實現:
[java] view plaincopy
  1. public Collection<V> values() {  
  2.         if (values == null) {  
  3.             values = new AbstractCollection<V>() {  
  4.                 public Iterator<V> iterator() {  
  5.                     return new Iterator<V>() {  
  6.                         private Iterator<Entry<K,V>> i = entrySet().iterator();  
  7.   
  8.                         public boolean hasNext() {  
  9.                             return i.hasNext();  
  10.                         }  
  11.   
  12.                         public V next() {  
  13.                             return i.next().getValue();  
  14.                         }  
  15.   
  16.                         public void remove() {  
  17.                             i.remove();  
  18.                         }  
  19.                     };  
  20.                 }  
  21.   
  22.                 public int size() {  
  23.                     return AbstractMap.this.size();  
  24.                 }  
  25.   
  26.                 public boolean isEmpty() {  
  27.                     return AbstractMap.this.isEmpty();  
  28.                 }  
  29.   
  30.                 public void clear() {  
  31.                     AbstractMap.this.clear();  
  32.                 }  
  33.   
  34.                 public boolean contains(Object v) {  
  35.                     return AbstractMap.this.containsValue(v);  
  36.                 }  
  37.             };  
  38.         }  
  39.         return values;  
  40.     }  

大家可以看到,代碼不少,基本的思路是先通過entrySet生成包含所有鍵值對的Set,然後通過迭代獲取其中的value值。其中生成包含所有鍵值對的Set肯定需要開銷,所以我們在自己的實現裏面重寫了values方法,就一句話,return this.values,直接返回我們的values字段。所以我們重寫大部分方法的目的都是讓方法的實現更快更簡潔。

大家還需要注意一下,我們在重寫entrySet方法時,需要返回一個包含當前Map所有鍵值對的Set。首先鍵值對時一種類型,所有的鍵值對類都要實現Map.Entry<K,V>這個接口。其次,由於entrySet要讓我們返回一個Set,這裏我們沒有使用Java中已有的Set類型(比如HashSet、TreeSet),有兩方面的原因:
1. Java中HashSet這個類內部其實用HashMap實現的,本博客的目的就是要研究HashMap,所以我們不用此類;
2. Java中Set的實現也不是很麻煩,自己實現一下AbstractSet,加深一下對Set的理解。

以下是我們自己實現的鍵值對類SimpleEntry,實現了Map.Entry<K,V>接口,代碼如下:

[java] view plaincopy
  1. import java.util.Map;  
  2.   
  3. //Map中存儲的鍵值對,鍵值對需要實現Map.Entry這個接口  
  4. public class SimpleEntry<K,V> implements Map.Entry<K, V>{  
  5.       
  6.     private K key = null;//鍵  
  7.       
  8.     private V value = null;//值  
  9.       
  10.     public SimpleEntry(K k, V v){  
  11.         this.key = k;  
  12.         this.value = v;  
  13.     }  
  14.   
  15.     @Override  
  16.     public K getKey() {  
  17.         return this.key;  
  18.     }  
  19.   
  20.     @Override  
  21.     public V getValue() {  
  22.         return this.value;  
  23.     }  
  24.   
  25.     @Override  
  26.     public V setValue(V v) {  
  27.         V oldValue = this.value;  
  28.         this.value = v;  
  29.         return oldValue;  
  30.     }  
  31.       
  32. }  

以下是我們自己實現的集合類SimpleSet,繼承自抽象類AbstractSet<K,V>,代碼如下:

[java] view plaincopy
  1. import java.util.AbstractSet;  
  2. import java.util.ArrayList;  
  3. import java.util.Iterator;  
  4.   
  5. public class SimpleSet<E> extends AbstractSet<E> {  
  6.       
  7.     private ArrayList<E> list = new ArrayList<E>();  
  8.   
  9.     @Override  
  10.     public Iterator<E> iterator() {  
  11.         return this.list.iterator();  
  12.     }  
  13.   
  14.     @Override  
  15.     public int size() {  
  16.         return this.list.size();  
  17.     }  
  18.   
  19.     @Override  
  20.     public boolean contains(Object o) {  
  21.         return this.list.contains(o);  
  22.     }  
  23.   
  24.     @Override  
  25.     public boolean add(E e) {  
  26.         boolean isChanged = false;  
  27.         if(!this.list.contains(e)){  
  28.             this.list.add(e);  
  29.             isChanged = true;  
  30.         }  
  31.         return isChanged;  
  32.     }  
  33.   
  34.     @Override  
  35.     public boolean remove(Object o) {  
  36.         return this.list.remove(o);  
  37.     }  
  38.   
  39.     @Override  
  40.     public void clear() {  
  41.         this.list.clear();  
  42.     }  
  43.   
  44. }  

我們測試下我們寫的SimpleMap這個類,測試包括兩部分,一部分是測試我們寫的SimpleMap是不是正確,第二部分測試性能如何,測試代碼如下:

[java] view plaincopy
  1. import java.util.HashMap;  
  2. import java.util.HashSet;  
  3. import java.util.Map;  
  4.   
  5.   
  6. public class Test {  
  7.   
  8.     public static void main(String[] args) {  
  9.         //測試SimpleMap的正確性  
  10.         SimpleMap<String, String> map = new SimpleMap<String, String>();  
  11.         map.put("iSpring""27");  
  12.         System.out.println(map);  
  13.         System.out.println(map.get("iSpring"));  
  14.         System.out.println("-----------------------------");  
  15.           
  16.         map.put("iSpring""28");  
  17.         System.out.println(map);  
  18.         System.out.println(map.get("iSpring"));  
  19.         System.out.println("-----------------------------");  
  20.           
  21.         map.remove("iSpring");  
  22.         System.out.println(map);  
  23.         System.out.println(map.get("iSpring"));  
  24.         System.out.println("-----------------------------");  
  25.           
  26.         //測試性能如何  
  27.         testPerformance(map);  
  28.     }  
  29.       
  30.     public static void testPerformance(Map<String, String> map){  
  31.         map.clear();  
  32.           
  33.         for(int i = 0; i < 10000; i++){  
  34.             String key = "key" + i;  
  35.             String value = "value" + i;  
  36.             map.put(key, value);  
  37.         }  
  38.           
  39.         long startTime = System.currentTimeMillis();  
  40.           
  41.         for(int i = 0; i < 10000; i++){  
  42.             String key = "key" + i;  
  43.             map.get(key);  
  44.         }  
  45.           
  46.         long endTime = System.currentTimeMillis();  
  47.           
  48.         long time = endTime - startTime;  
  49.           
  50.         System.out.println("遍歷時間:" + time + "毫秒");  
  51.     }  
  52.       
  53. }  

輸出結果如下:
{iSpring=27}
27
-----------------------------
{iSpring=28}
28
-----------------------------
{}
null
-----------------------------
遍歷時間:956毫秒

從結果裏面我們看到輸出結果是正確的,也就是我們寫的SimpleMap基本實現都是對的。我們往Map中插入了10000個鍵值對,我們測試的是從Map中取出這10000條鍵值對的性能開銷,也就是測試Map的遍歷的性能開銷,結果是956毫秒。

沒有對比就不知性能強弱,我們測試下HashMap讀取這10000條鍵值對的時間開銷,測試方法完全一樣,只是我們傳入的是HashMap的實例,測試代碼如下:

[java] view plaincopy
  1. //創建HashMap的實例  
  2.         HashMap<String, String> map = new HashMap<String, String>();  
  3.           
  4.         //測試性能如何  
  5.         testPerformance(map);  

測試結果如下:
遍歷時間:32毫秒

我去,不比不知道,一比嚇一跳啊,HashMap比我們自己實現的SimpleMap快的那不是一點半點啊。爲什麼我們的SimpleMap性能這麼差?而HashMap的性能如此高呢?我們分別研究。
首先分析SimpleMap性能爲什麼這麼差。
我們的SimpleMap是用ArrayList來存儲keys和values的,ArrayList本質是用數組實現的,我們的SimpleMap的get方法是這樣實現的:

[java] view plaincopy
  1. @Override  
  2.     public V put(K key, V value) {  
  3.         V oldValue = null;  
  4.         int index = this.keys.indexOf(key);  
  5.         if(index >= 0){  
  6.             //keys中已經存在鍵key,更新key對應的value  
  7.             oldValue = this.values.get(index);  
  8.             this.values.set(index, value);  
  9.         }else{  
  10.             //keys中不存在鍵key,將key和value作爲鍵值對添加進去  
  11.             this.keys.add(key);  
  12.             this.values.add(value);  
  13.         }  
  14.         return oldValue;  
  15.     }  

需要性能開銷的主要是this.keys.indexOf(key)這句代碼,這句代碼從ArrayList中查找指定元素的索引,本質就是從數組開頭走,往後找,直至數組的末尾。如下圖所示:


這樣從頭開始查找,並且每次在遍歷元素的時候,都需要調用元素的equals方法,所以從頭開始查找就會導致調用很多次equals方法,這就造成了SimpleMap效率低下。比如我們將全國的車輛放入到SimpleMap中時,我們是依次將車輛放到ArrayList的最後面,依次往後插入值,車牌號就相當於key,車輛就好比是value,所以SimpleMap中有兩個長度很長的ArrayList,分別存儲keys和values,如果要在該SimpleMap中查找一輛車,車牌是"魯E.DE829",那如果用ArrayList查找的話就要從全國的的所有車輛中去查找了,這樣太慢。

那麼HashMap爲何效率如此高呢?
HashMap比較聰明,大家可以看看HashMash.java的源碼,HashMap把裏面的元素分類放置了,還拿上面根據車牌號查找車輛的例子來說,當把我們把車輛往HashMap裏面放的時候,HashMap將它們分類處理了,首先來一輛車的時候,先看其車牌號,比如車牌號是"魯E.DE829",一看是魯,就知道是山東的車輛,那麼HashMap就開闢了一塊空間,專門放山東的車,就把這輛車放到這塊山東專屬的區間了,下次又要向HashMap放入一輛車牌號爲“浙A.GX588",HashMap一看是浙江的車,就將這輛車放入到浙江的專屬區間了,依次類推。說的再通俗點,假設我們有一種很大的桶,該桶就是相應的區間,可以裝下很多車,如下圖所示:

當我們從HashMap中根據車牌號查找指定的車輛時,比如查找車牌號爲爲"魯E.DE829"的車,當調用HashMap的get方法時,HashMap一看車牌號是魯,那麼HashMap就去標爲魯的那個大桶,也就是山東區間去找這輛車了。這樣就沒有必要從全國的車輛中挨個找這輛車了,這就大大縮短了查找空間,提高了效率。

我們可以看看HashMap.java中具體的源碼實現,HashMap中用一個名爲table的字段存儲着一個Entry數組,table存儲着HashMap裏面的所有鍵值對,每個鍵值對都是一個Entry對象。每個Entry對象都存儲着一個key和value,除此之外每個Entry內部還存着一個next字段,next也是Entry類型。數組table的默認長度是DEFAULT_INITIAL_CAPACITY,即初始長度爲16,當容器需要更多的空間存取Entry時,它會自動擴容。
以下是HashMap的put方法的源碼實現:
[java] view plaincopy
  1. public V put(K key, V value) {  
  2.         if (key == null)  
  3.             return putForNullKey(value);  
  4.         int hash = hash(key.hashCode());  
  5.         int i = indexFor(hash, table.length);  
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  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);  
  18.         return null;  
  19.     }  

在put方法中,,調用了對象的hashCode方法,該方法返回一個int類型的值,是個初始的哈希值,這個值就相當於車牌號,例如"魯E.DE829",HashMap中有個hash方法,該hash方法將我們得到的初始的哈希值做進一步處理,得到最終的哈希值,就好比我們將車牌號傳入hash方法,然後返回該存放車輛的大桶,即返回"魯",這樣HashMap就把這輛車放到標有“魯”的大桶裏面了。上面說到的hash方法叫做哈希函數,專門負責根據傳入的值返回指定的最終哈希值,具體實現如下:
[java] view plaincopy
  1. static int hash(int h) {  
  2.         // This function ensures that hashCodes that differ only by  
  3.         // constant multiples at each bit position have a bounded  
  4.         // number of collisions (approximately 8 at default load factor).  
  5.         h ^= (h >>> 20) ^ (h >>> 12);  
  6.         return h ^ (h >>> 7) ^ (h >>> 4);  
  7.     }  

可以看出來,HashMap中主要是通過位操作符實現哈希函數的。這裏簡單說一下哈希函數,哈希函數有多種實現方式,比如最簡單的就是取餘法,比如對i%10取餘,然後按照餘數創建不同的區塊或桶。比如有100個數,分別是從1到100,那麼分別對10取餘,那麼就可以把這100個數放到10個桶子裏面了,這就是所謂的哈希函數。只不過HashMap中的hash函數看起來比較複雜,進行的是位操作,但是其作用與簡單的取餘哈希法的作用是等價的,就是把元素分類放置。
具體將鍵值對放入到HashMap中的方法是addEntry,代碼如下:
[java] view plaincopy
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.         Entry<K,V> e = table[bucketIndex];  
  3.         table[bucketIndex] = new Entry<>(hash, key, value, e);  
  4.         if (size++ >= threshold)  
  5.             resize(2 * table.length);  
  6.     }  

鍵值對都是Map.Entry<K,V>對象,並且Map.Entry具有next字段,也就是桶裏面的元素都是通過單向鏈表的形式將Map.Entry串連起來的,這樣我們就可以從桶上的第一個元素通過next依次遍歷完桶裏面所有的元素。比如桶中有如下鍵值對:
桶-->e1-->e2-->e3-->e4-->e5-->e6-->e7-->e8-->e9-->...
addEntry代碼首先取出桶裏面的第一個鍵值對e1,然後將新的鍵值對e置於桶中第一個元素的位置,然後將鍵值對e1放置於新鍵值對e後面,放置完之後,桶中新的鍵值對如下:
桶-->e-->e1-->e2-->e3-->e4-->e5-->e6-->e7-->e8-->e9-->...
這樣就把新的鍵值對放到了桶中了,也就將鍵值對放到HashMap中了。

那麼當我們從HashMap中查找某個鍵值對時,怎麼查找呢?原理與我們將鍵值對放入HashMap相似,以下是HashMap的get方法的源碼實現:

[java] view plaincopy
  1. public V get(Object key) {  
  2.         if (key == null)  
  3.             return getForNullKey();  
  4.         int hash = hash(key.hashCode());  
  5.         for (Entry<K,V> e = table[indexFor(hash, table.length)];  
  6.              e != null;  
  7.              e = e.next) {  
  8.             Object k;  
  9.             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
  10.                 return e.value;  
  11.         }  
  12.         return null;  
  13.     }  

在get方法中,也是先調用了對象的hashCode方法,就相當於車牌號,然後再將該值讓hash函數處理得到最終的哈希值,也就是桶的索引。然後我們再去這個標有“魯”的桶裏面去找我們的鍵值對,首先先取出桶裏面第一個鍵值對,比對一下是不是我們要找的元素,如果是就直接返回了,如果不是就通過鍵值對的next順藤摸瓜通過單向鏈表繼續找下去,直至找到。  如下圖所示:

下面我們再寫一個Car類,該類有一個字段String類型的字段num,並且我們重寫了Car的equals方法,我們認爲只要車牌號相等就認爲這是同一輛車。代碼如下所示:
[java] view plaincopy
  1. import java.util.HashMap;  
  2.   
  3. public class Car {  
  4.       
  5.     private final String num;//車牌號  
  6.       
  7.     public Car(String n){  
  8.         this.num = n;  
  9.     }  
  10.       
  11.     public String getNum(){  
  12.         return this.num;  
  13.     }  
  14.   
  15.     @Override  
  16.     public boolean equals(Object obj) {  
  17.         if(obj == null){  
  18.             return false;  
  19.         }  
  20.         if(obj instanceof Car){  
  21.             Car car = (Car)obj;  
  22.             return this.num.equals(car.num);  
  23.         }  
  24.         return false;  
  25.     }  
  26.       
  27.   
  28.     public static void main(String[] args){  
  29.         HashMap<Car, String> map = new HashMap<Car, String>();  
  30.         String num = "魯E.DE829";  
  31.         Car car1 = new Car(num);  
  32.         Car car2 = new Car(num);  
  33.         System.out.println("Car1 hash code: " + car1.hashCode());  
  34.         System.out.println("Car2 hash code: " + car2.hashCode());  
  35.         System.out.println("Car1 equals Car2: " + car1.equals(car2));  
  36.         map.put(car1, new String("Car1"));  
  37.         map.put(car2, new String("Car2"));  
  38.         System.out.println("map.size(): " + map.size());  
  39.     }  
  40.   
  41. }  
我們在main函數中寫了一些測試代碼,我們創建了一個HashMap,該HashMap的用Car作爲鍵,用字符串作爲值。我們用同一個字符串實例化了兩個Car,分別爲car1和car2,然後將這兩個car都放入到HashMap中,輸出結果如下:
Car1 hash code: 404267176
Car2 hash code: 2027651571
Car1 equals Car2: true
map.size(): 2

從結果可以看出來,Car1和Car2是相等的,既然二者是相等的,也就是兩者作爲鍵來說是相等的鍵,所以HashMap裏面只能放其中一個作爲鍵,但是實際結果中map的長度卻是2個,爲什麼會這樣呢?關鍵在於Car的hashCode方法,準確的說是Object的hashCode方法,Object的hashCode方法默認情況下返回的是對象內存地址,因爲內存地址是唯一的。

我們沒有重寫Car的hashCode方法,所以car1的hashCode返回的值和car2的hashCode返回的值肯定不同。通過我們前面研究可知,如果是兩個元素相等,那麼這兩個元素應該放到同一個HashMap的桶裏。但是由於我們的car1和car2的hashCode不同,所以HashMap將car1和car2分別放到不同的桶子裏面了,這就出問題了。相等(equals)的兩個元素(car1和car2)如果hashCode返回值不同,那麼這兩個元素就會放到HashMap不同的區間裏面。所以我們寫代碼的時候要保證相互equals的兩個對象的哈希值必定要相等,即必須保證hashCode的返回值相等。那如何解決這個問題?我們只需要重寫hashCode方法即可,代碼如下:
[java] view plaincopy
  1. @Override  
  2.     public int hashCode() {  
  3.         return this.num.hashCode();  
  4.     }  
重新運行main中的測試代碼,輸出結果如下:
Car1 hash code: 607836628
Car2 hash code: 607836628
Car1 equals Car2: true
map.size(): 1

之前我們說了,相互equals的對象必須返回相同的哈希值,相同哈希值的對象都在一個桶裏面,但是反過來,具有相同哈希值的對象(也就是在同一個桶裏面的對象)不必相互equals。

總結:
1. HashMap爲了提高查找的效率使用了分塊查找的原理,對象的hashCode返回的哈希值進行進一步處理,這樣就有規律的把不同的元素放到了不同的區塊或桶中。下次查找該對象的時候,還是計算其哈希值,根據哈希值確定區塊或桶,然後在這個小範圍內查找元素,這樣就快多了。
2. 如果重寫了equals方法,那麼必須重寫hashCode方法,保證如果兩個對象相互equals,那麼二者的hashCode的返回值必定相等。
3. 如果兩個對象的hashCode返回值相等,這兩個對象不必是equals的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章