探究HashMap的工作原理

GeorgiaToma 9年前發布 | 7K 次閱讀 鏈表 Java開發

摘要:經過大學四年,我也剛剛畢業了3個月了,學習android也有一兩年了,可是在大學的生活中,磨練出來的解決實際問題的能力是有的,完成開發任務還是可以的,但是總覺得技術遇到了瓶頸期,沒有辦法進步很多。因此想靜下心來看JDK的源碼,看看這些人是怎么設計出來的?為什么要這么設計?我能從中體會到什么?對我今后的職業生涯有什么幫助?跟著優秀的人一起成長吧。

我們用HaspMap最關心的兩個方法和幾個變量是:

/** 默認的初始化容量,2^4 */
static final int DEFAULT_INITIAL_CAPACITY = 16;

/** 默認的負載因子 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/*數組,Entry是存放Key value的基礎bean */
transient Entry[] table;

/** 負載因子*/
final float loadFactor;

/** *下一次的負載table容量 (capacity * load factor) *
int threshold;

public V put(K key,V value)

public V get(Object key)

為了幫助理解和記憶,貼上兩張圖:

Paste_Image.png

Paste_Image.png

從上圖我們可以發現哈希表是由 數組+鏈表 組成的,一個長度為 我們看到的第一個變量 DEFAULT_INITIAL_CAPACITY=16 的數組中,每個元素存儲的是一個鏈表的頭結點。那么這些元素是按照什么樣的規則存儲到數組中呢。一般情況是通過hash(key.hashCode()%table.length)獲得,也就是元素的key的哈希值對數組長度取模得到。

HashMap其實也是一個線性的數組實現的,所以可以理解為其存儲數據的容器就是一個線性數組。HashMap里面實現一個靜態內部類Entry,其重要的屬性有key,value,next,從屬性key,value我們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,我們上面說到HashMap的基礎就是一個線性數組,這個數組就是Entry[],Map里面的內容都保存在Entry[]里面。

HashMap存取過程

public V put(K key, V value) {    
     if (key == null)       
     return putForNullKey(value);    
     int hash = hash(key.hashCode());   
     int i = indexFor(hash, table.length);   
     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;        
  }   
 }    
     modCount++;   
     addEntry(hash, key, value, i);    return null;
}

看源碼得出幾個結論:

1.HashMap可以存儲null的key。

2.通過key.hashCode找到hash值,然后跟table.length的取模,得到數組下標。然后遍歷鏈表,如果hash值且key值相等,則替換原來的值。

3.table添加新的元素時并沒有奇怪,奇怪的是我們知道數據的默認容量是16,超過了16怎么辦,HashMap是怎么處理的呢?

所以要看一下上述代碼中addEntry這個方法里面到底做什么操作。

void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

到這里已經很明顯,我們發現了HashMap的處理方式,是size > threshold的時候就會去擴容。那么這個threshold是什么東西呢?文章的開頭我們已經把他列出來了,threshold = capacity loadFactor的結果,loadFactor是負載因此,capacity是容量。那么我們可以得出很簡單的結論就是首次初始化的時候threshold = DEFAULT_INITIAL_CAPACITY(16) DEFAULT_LOAD_FACTOR (0.75) ,就是超過了容量的75%的時候就會去擴容。

當哈希表的容量超過默認容量時,必須調整table的大小。當容量已經達到最大可能值時,那么該方法就將容量調整到Integer.MAX_VALUE返回,這時,需要創建一張新表,將原表的映射到新表中。

我帶著好奇心繼續探索resize這個方法。

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;
                    //重新計算index
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

到這里基本上清楚了HashMap的進行put時候的整個過程了。相同get方法我就不貼代碼了。有興趣可以去研究源碼。

當我看源碼看到這一步我可能比較困惑,HashMap究竟優勢在哪兒?

我們要分析一下HashMap的數據結構,上述我們已經了解HashMap的數據結構是 數組加鏈表。

數組存儲區間是連續的,占用內存嚴重,故空間復雜的很大。但數組的二分查找時間復雜度小,為O(1);數組的特點是:尋址容易,插入和刪除困難;

鏈表存儲區間離散,占用內存比較寬松,故空間復雜度很小,但時間復雜度很大,達O(N)。鏈表 的特點是:尋址困難,插入和刪除容易。

 

 

來自:http://www.jianshu.com/p/006f9f446fcb

 

 本文由用戶 GeorgiaToma 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!