給HashMap作個深度剖析手術

   人們對於任何事物的認知,每每都存在這麼一個現象:只有你瞭解的東西,你纔會感興趣
   HashMap之於Java開發者來講,也是如此。相信多數人在起初至關長的時間內,對HashMap的印象都是「Map接口的實現類,是基於哈希的,用於存放鍵-值對,容許null做爲鍵和值,非線程安全的」,僅此而已。因而在程序編寫過程當中便「肆無忌憚」往裏放鍵-值對。而只有你對HashMap的實現有了必定的瞭解以後,你纔會有興趣研究HashMap深層次的問題,好比「HashMap最多能放多少個鍵-值對?如何提升HashMap的使用效率?」。其實,我一直都對HashMap的「線性數組+鏈表」的實現機制充滿好奇,剛剛有時間研究了一下源碼,現把心得與你們分享。java

 

   首先,咱們來看看HashMap的數據結構:數組

    /**
     *  初始容量,大小必須是2的指數次方,默認是16.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    /**
     * 默認最大容量,值爲1<<30
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
    /**
     * 哈希表的默認加載因子,值爲0.75.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    /**
     * 存儲元素的數組,大小必須是2的指數次方,默認是16.
     */
    transient Entry[] table;
    /**
     * THashMap中的存儲的<K,V>映射的數目.
     */
    transient int size;
    /**
     * threshold=容量*加載因子。當實際數目大於threshold時,HashMap就須要擴容.
     * @serial
     */
    int threshold;
    /**
     * 哈希表的加載因子,若是建立時不指定loadFactor,則使用DEFAULT_LOAD_FACTOR.
     *
     * @serial
     */
    final float loadFactor;
    //... ...
    /**
     * 
     * <p>用一個靜態內部類來定義數組項鍊表的元素</p>
     *
     * @author bruce.yang
     * @version 1.0 Created on 2014-10-22 上午11:39:50
     */
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;
        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        public final K getKey() {
            return key;
        }
        public final V getValue() {
            return value;
        }
        public final V setValue(V newValue) {
     V oldValue = value;
            value = newValue;
            return oldValue;
        }
        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }
        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }
        public final String toString() {
            return getKey() + "=" + getValue();
        }

能夠看出,HashMap正是採用數組(table)類存儲數據的,而數組每個元素則是一個被靜態內部類Entry封裝起來的對象,元素在數組中的下標則是根據key的hashcode計算出來的;當元素下標重複的時候,會在此下標處造成一個鏈表,而這個鏈表就是經過Entry結構來實現的。咱們看到,這個Entry結構是一個單向鏈表,它只有一個next項指向下一個元素,此外,它還包含<K,V>對,同時還有一個哈希值hash。安全

 

   其次,當咱們往hashmap中put元素的時候,先是根據key的hash值計算得出這個元素在數組中的位置(即下標),而後就能夠把這個元素放到對應的位置中了。若是這個位子上已經存放有其餘元素了,那麼在同一個位子上的元素將以鏈表的形式存放,新加入的放在鏈頭,最早加入的放在鏈尾。源碼:數據結構

   /**
     * 創建指定key和value之間的映射:
     * 若是已經存在key,則替換對應的value,並返回原來的value;若是原來沒有創建映射,則創建映射,並返回null。
     * 固然了,因爲HashMap容許key和value爲null,返回null還有可能就是原來的value爲null。
     */
    public V put(K key, V value) {
     //若是key==null,則調用專門的方法處理
        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;
    }
    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }
    /**
     * 添加key-value項
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
     Entry<K,V> e = table[bucketIndex];
     //加入該項到鏈表頭部  
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }
    /**
     *擴充HashMap的容量
     */
    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);
    }
    /**
     * 將原來數組中元素傳輸到新數組中
     */
    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;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

能夠看到,首先判斷key是否爲null:
      1).若爲null,則調用專門的方法putForNullKey(value)處理並返回。
        1.1)若是事先已經存在key爲null的映射,則替換後返回old value。
        1.2)若是不存在,則添加新的項到鏈表中
      2).若key不爲null
        2.1)首先計算key的哈希值,而後根據哈希值和table數組的長度定位數組項。
        2.2)對數組項的鏈表進行遍歷,若是key的哈希值與鏈表中的某一項的哈希值相等且key自己引用值相等或者引用值所指向的對象相等,則替換相應項的value值爲新的value,並返回老的value。若是沒有找到相同的key,則加入該項到鏈表中。
        2.3)addEntry方法直接將新的項加入到鏈表的頭部,新項的next引用指向原來的鏈表項。此外判斷是否須要擴容,若是此時存儲的項數目size大於等於threshold,則擴大HashMap容量爲原來的2倍。
        2.4)resize方法用來擴容HashMap。默認是擴容至原來的2倍大小。ide

 

   第三,當從hashmap中get元素時,首先計算key的hashcode,找到數組中對應位置的某一元素,而後經過key的equals方法在對應位置的鏈表中找到須要的元素。源碼:性能

   /**
     * 根據key取得value
     */
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }
    private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

能夠看到,若是key爲nullget方法與put方法對應,對null值也有特殊處理,即直接到table[0]中去找key爲null對應的value。
    若是key不爲null,則定位key所在數組項,而後遍歷鏈表,若是存在key,則返回對應的value值,不然返回nullthis

 

   第四,HashMap的初始化:spa

   /**
     *指定初始化容量和加載因子
     */
    public HashMap(int initialCapacity, float loadFactor) {
     //initialCapacity不能小於0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //若是指定容量大於默認最大容量,則按照默認最大容量建立
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //loadFactor不能小於0
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        //找最大於指定初始化容量的2的指數冪做爲表的真實容量
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
    }
    /**
     *只指定初始化容量,加載因子採用默認
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    /**
     * 初始化容量和加載因子都採用默認
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }

只看源碼和註釋就一目瞭然了,這裏所要特別注意一下帶兩個參數的構造方法,其初始化時的初始大小不必定是你所指定的大小。線程

 

   最後,文章開頭提出的那兩個問題已經再也不成爲問題了:
 1).HashMap最大容量是1 << 30,
 2).而影響HashMap性能的兩個因素是:初始容量和加載因子。這裏借用書中的一段話來表述:容量是哈希表中桶的數量,初始容量只是哈希表在建立時的容量。加載因子是哈希表在其容量自動增長以前能夠達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操做(即重建內部數據結構),從而哈希表將具備大約兩倍的桶數。
一般,默認加載因子 (0.75)在時間和空間成本上尋求一種折衷。加載因子太高雖然減小了空間開銷,但同時也增長了查詢成本(在大多數 HashMap類的操做中,包括 get put操做,都反映了這一點)。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減小 rehash操做次數。若是初始容量大於最大條目數除以加載因子,則不會發生 rehash操做。
讀到這裏,相信你對HashMap已經有了一個更深入的認識。code

   各位,晚安,美夢

相關文章
相關標籤/搜索