java面試基礎-Java中HashMap的實現

HashMap可謂JDK的一大實用工具,把各個Object映射起來,實現了“鍵--值”對應的快速存取。但實際裏面做了些什麼呢?

  在這之前,先介紹一下負載因子和容量的屬性。大家都知道其實一個 HashMap 的實際容量就 因子*容量,其默認值是 16×0.75=12; 這個很重要,對效率很一定影響!當存入HashMap的對象超過這個容量時,HashMap 就會重新構造存取表。這就是一個大問題,我後面慢慢介紹,反正,如果你已經知道你大概要存放多少個對象,最好設爲該實際容量的能接受的數字。

兩個關鍵的方法,put和get:

  先有這樣一個概念,HashMap是聲明瞭 Map,Cloneable, Serializable 接口,和繼承了 AbstractMap 類,裏面的 Iterator 其實主要都是其內部類HashIterator 和其他幾個 iterator 類實現,當然還有一個很重要的繼承了Map.Entry 的 Entry 內部類,由於大家都有源代碼,大家有興趣可以看看這部分,我主要想說明的是 Entry 內部類。它包含了hash,value,key 和next 這四個屬性,很重要。put的源碼如下
  
Java代碼

 public Object put(Object key, Object value) {   
      Object k = maskNull(key);   

  這個就是判斷鍵值是否爲空,並不很深奧,其實如果爲空,它會返回一個static Object 作爲鍵值,這就是爲什麼HashMap允許空鍵值的原因。

 
Java代碼

  int hash = hash(k);   
    int i = indexFor(hash, table.length);   

  這連續的兩步就是 HashMap 最牛的地方!研究完我都汗顏了,其中 hash 就是通過 key 這個Object的 hashcode 進行 hash,然後通過 indexFor 獲得在Object table的索引值。

  table???不要驚訝,其實HashMap也神不到哪裏去,它就是用 table 來放的。最牛的就是用 hash 能正確的返回索引。其中的hash算法,我跟JDK的作者 Doug 聯繫過,他建議我看看《The art of programing vol3》可恨的是,我之前就一直在找,我都找不到,他這樣一提,我就更加急了,可惜口袋空空啊!!!

  不知道大家有沒有留意 put 其實是一個有返回的方法,它會把相同鍵值的 put 覆蓋掉並返回舊的值!如下方法徹底說明了 HashMap 的結構,其實就是一個表加上在相應位置的Entry的鏈表:

 
Java代碼

 for (Entry e = table[i]; e != null; e = e.next) {   
     if (e.hash == hash && eq(k, e.key)) {   
     Object oldvalue = e.value;   
     e.value = value; //把新的值賦予給對應鍵值。   
     e.recordAccess(this); //空方法,留待實現   
     return oldvalue; //返回相同鍵值的對應的舊的值。   
     }   
     }   
     modCount++; //結構性更改的次數   
     addEntry(hash, k, value, i); //添加新元素,關鍵所在!   
     return null; //沒有相同的鍵值返回   
     }   

  我們把關鍵的方法拿出來分析:

  
Java代碼

 void addEntry(int hash, Object key, Object value, int bucketIndex) {   
      table[bucketIndex] = new Entry(hash, key, value, table[bucketIndex]);   

  因爲 hash 的算法有可能令不同的鍵值有相同的hash碼並有相同的table索引,如:key=“33”和key=Object g的hash都是-8901334,那它經過indexfor之後的索引一定都爲i,這樣在new的時候這個Entry的next就會指向這個原本的table[i],再有下一個也如此,形成一個鏈表,和put的循環對定e.next獲得舊的值。到這裏,HashMap的結構,大家也十分明白了吧?
Java代碼

 if (size++ >= threshold) //這個threshold就是能實際容納的量   
     resize(2 * table.length); //超出這個容量就會將Object table重構   

  所謂的重構也不神,就是建一個兩倍大的table(我在別的論壇上看到有人說是兩倍加1,把我騙了),然後再一個個indexfor進去!注意!!這就是效率!!如果你能讓你的HashMap不需要重構那麼多次,效率會大大提高!

總結

HashMap中我們最長用的就是put(K, V)和get(K)。我們都知道,HashMap的K值是唯一的,那如何保證唯一性呢?我們首先想到的是用equals比較,沒錯,這樣可以實現,但隨着內部元素的增多,put和get的效率將越來越低,這裏的時間複雜度是O(n),假如有1000個元素,put時需要比較1000次。實際上,HashMap很少會用到equals方法,因爲其內通過一個哈希表管理所有元素,哈希是通過hash單詞音譯過來的,也可以稱爲散列表,哈希算法可以快速的存取元素,當我們調用put存值時,HashMap首先會調用K的hashCode方法,獲取哈希碼,通過哈希碼快速找到某個存放位置,這個位置可以被稱之爲bucketIndex,通過上面所述hashCode的協定可以知道,如果hashCode不同,equals一定爲false,如果hashCode相同,equals不一定爲true。所以理論上,hashCode可能存在衝突的情況,有個專業名詞叫碰撞,當碰撞發生時,計算出的bucketIndex也是相同的,這時會取到bucketIndex位置已存儲的元素,最終通過equals來比較,equals方法就是哈希碼碰撞時纔會執行的方法,所以前面說HashMap很少會用到equals。HashMap通過hashCode和equals最終判斷出K是否已存在,如果已存在,則使用新V值替換舊V值,並返回舊V值,如果不存在 ,則存放新的鍵值對

如圖

這裏寫圖片描述

現在我們知道,執行put方法後,最終HashMap的存儲結構會有這三種情況,情形3是最少發生的,哈希碼發生碰撞屬於小概率事件。到目前爲止,我們瞭解了兩件事:

HashMap通過鍵的hashCode來快速的存取元素。
當不同的對象hashCode發生碰撞時,HashMap通過單鏈表來解決,將新元素加入鏈表表頭,通過next指向原有的元素。單鏈表在Java中的實現就是對象的引用(複合)。

   來鑑賞一下HashMap中put方法源碼:
    public V put(K key, V value) {  
        // 處理key爲null,HashMap允許key和value爲null  
        if (key == null)  
            return putForNullKey(value);  
        // 得到key的哈希碼  
        int hash = hash(key);  
        // 通過哈希碼計算出bucketIndex  
        int i = indexFor(hash, table.length);  
        // 取出bucketIndex位置上的元素,並循環單鏈表,判斷key是否已存在  
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
            Object k;  
            // 哈希碼相同並且對象相同時  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
                // 新值替換舊值,並返回舊值  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }  

        // key不存在時,加入新元素  
        modCount++;  
        addEntry(hash, key, value, i);  
        return null;  
    }  

  說到這裏也差不多了,get比put簡單得多,大家,瞭解put,get也差不了多少了。對於collections我是認爲,它是適合廣泛的,當不完全適合特有的,如果大家的程序需要特殊的用途,自己寫吧,其實很簡單。(作者是這樣跟我說的,他還建議我用LinkedHashMap,我看了源碼以後發現,LinkHashMap其實就是繼承HashMap的,然後override相應的方法,有興趣的同人,自己looklook)建個 Object table,寫相應的算法,就ok啦。

  舉個例子吧,像 Vector,list 啊什麼的其實都很簡單,最多就多了的同步的聲明,其實如果要實現像Vector那種,插入,刪除不多的,可以用一個Object table來實現,按索引存取,添加等。

  如果插入,刪除比較多的,可以建兩個Object table,然後每個元素用含有next結構的,一個table存,如果要插入到i,但是i已經有元素,用next連起來,然後size++,並在另一個table記錄其位置。
   到這裏,我們瞭解了HashMap工作原理的一部分,那還有另一部分,如,加載因子及rehash,HashMap通常的使用規則,多線程併發時HashMap存在的問題等等,

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