HashMap的實現原理

想了解HashMap的原因是,前幾天去某公司面試,被問到這個,我一臉懵逼,所以決定回來補補知識。

  1. 數據結構基礎操作執行性能
    • 數組:採用一段連續的存儲單元存儲數據。對於查找指定數字的下標,時間複雜度是O(1)。對於查找關鍵字是否在數組裏面,時間複雜度是O(n)。對於插入刪除,時間複雜度是O(n)。
    • 線性鏈表:對於鏈表的新增刪除等操作,時間複雜度爲O(1)。而查找操作時間複雜度是O(n)。
    • 二叉樹:對於相對平衡的有序二叉樹,對其進行插入查找刪除的操作,平均複雜度是O(logn)。
    • 哈希表:哈希表中進行添加,刪除,查找的操作性能十分之高,不考慮哈希衝突的情況下,只需一次定位即可完成。時間複雜度爲O(1)。
  2. 數據結構的物理存儲只有兩種:順序存儲鏈式存儲
  3. 哈希表的主幹就是數組
  4. 我們要新增或查找某個元素,通過把當前元素的關鍵字通過某函數映射到數組中的某個位置。這個函數被稱爲哈希函數,這個函數的好壞直接影響哈希表的優劣。
  5. 哈希衝突:當我們對某個元素進行哈希運算,得到一個存儲值,,在進行插入的時候發現地址被佔用了,那麼就發生了所謂的哈希衝突。好的哈希函數可以保證計算簡單散列地址分佈均勻
    • 哈希衝突的解決方法:開放定址法,再散列函數法,鏈地址法。而HashMap採用鏈地址法。也就是HashMap使用的是數組+鏈表
  6. HashMap的實現原理:HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成元素。每個Entry包含一個key-value鍵值對。初始值是空數組{}。主幹數組的長度一定是2的次冪。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
  1. Entry是HashMap的靜態內部類。
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構
        int hash;//對key的hashcode值進行hash運算後得到的值,存儲在Entry,避免重複計算

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        } 

則HashMap的整體結構
在這裏插入圖片描述
8. 簡單來說,HashMap是由數組+鏈表組成。數組是HashMap的主體,鏈表是爲了解決哈希衝突。如果定位到的數組不包含鏈表,那麼查找和添加操作就很快,僅需一次尋址。如果定位到的數組包含鏈表,對於添加操作就是時間複雜度O(n),首先遍歷數組,存在就覆蓋,不存在就新加。對於查找操作,仍需遍歷鏈表,通過key對象的equals方法逐一比對查找。所以性能考慮,HashMap中的鏈表出現越少,性能越好。
9. HashMap類中其他幾個重要的數據結構

  • transient int size:實際存儲key-value鍵值對的個數
  • int threshold:閥值,當table={}時,這個值是初始值16。當table被填充,也就是爲table分配內存空間之後,threshold一般爲capacity*loadFactor。HashMap在進行擴容的時候回考慮threshold。
  • final float loadFactor:負載因子,代表了HashMap的填充度有多少,一般是0.75。
  • transient int modCount:用於快速失敗,由於HashMap非線程安全,在對HashMap進行線程迭代的時候,如果其他線程的參與導致HashMap結構發生變化(比如:put,remove等操作),需要拋出異常ConcurrentModificationException。
  1. HashMap有四個構造器:
  • HashMap():構造一個空的HashMap,默認初始容量是16,負載因子是0.75。
  • HashMap(int initialCapacity):構造一個空的HashMap,初始容量是initialCapacity,負載因子是0.75。
  • HashMap(int initialCapacity,float loadFactor):構造一個空的HashMap,初始容量是initialCapacity,負載因子是loadFactor。
  • HashMap(Map<? extends K,? extends V> m):用相同的Map構造一個新的HashMap。
  1. 看一下上面的第三個構造函數的代碼。
public HashMap(int initialCapacity, float loadFactor) {
     //此處對傳入的初始容量進行校驗,最大不能超過MAXIMUM_CAPACITY = 1<<30(230)
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
     
        init();//init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現
    }

在這裏可以看到,在常規構造器中,沒有爲數組table分配內存空間(除了第四個構造器)。而是在執行put操作的時候才真正構建table數組。
12. 在瞭解put操作之前,我們先了解一下inflateTable方法。

private void inflateTable(int toSize) {
        int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次冪
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此處爲threshold賦值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不會超過MAXIMUM_CAPACITY,除非loadFactor大於1
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

inflateTable方法用於給主幹數組table分配內存空間,通過roundUpToPowerOf2(toSize)可以確保capacity爲大於或等於toSize的最接近toSize的二次冪。如果toSize=13,那麼capacity=16。如果toSize=20,那麼capacity=32。
13. 接着瞭解一下,hash()函數。

final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

用了很多的異或,移位等運算,對key的hashcode進一步進行計算,以及二進制位的調整等來保證最終獲取的存儲位置儘量分佈均勻。
14. 當用hash()函數計算出了值之後,通過indexFor()進一步處理來獲取實際存儲位置。

static int indexFor(int h, int length) {
        return h & (length-1);
    }

h & (length -1 )可以保證最終獲得的下標一定在數組範圍內。舉例:h = 23,length = 14,length-1 = 13。
   1 0 1 1 1
& 0 1 1 0 1
——————————————
    0 0 1 0 1 =5
則最後在數組中的下標是5。當然這也可以用模運算來實現,但是位運算對計算機來說性能更高。
15. addEntry()的實現:

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//當size超過臨界閾值threshold,並且即將發生哈希衝突時進行擴容
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

當size大於閥值或者發生哈希衝突的時候,需要進行數組擴容。擴容時需要新建一個長度爲之前數組2倍的新數組,然後將當前的Entry數組全部傳輸過去
16. 最後瞭解一下put()方法

 public V put(K key, V value) {
        //如果table數組爲空數組{},進行數組填充(爲table分配實際內存空間),入參爲threshold,此時threshold爲initialCapacity 默認是1<<4(24=16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
       //如果key爲null,存儲位置爲table[0]或table[0]的衝突鏈上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//對key的hashcode進一步計算,確保散列均勻
        int i = indexFor(hash, table.length);//獲取在table中的實際位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果該對應數據已存在,執行覆蓋操作。用新value替換舊value,並返回舊value
            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++;//保證併發訪問時,若HashMap內部結構發生變化,快速響應失敗
        addEntry(hash, key, value, i);//新增一個entry
        return null;
    }    
  1. 關於HashMap的數組長度爲何一定是2的次冪。
    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, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

如果數組發生可擴容,那麼數組長度發生變化,存儲位置index = h & (length-1)的肯定會發生變化。需要重新計算index。
再看看transfer方法。

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
     //for循環中的代碼,逐個遍歷鏈表,重新計算索引位置,將老數組數據複製到新數組中去(數組不存儲實際數據,所以僅僅是拷貝引用而已)
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
          //將當前entry的next鏈指向新的索引位置,newTable[i]有可能爲空,有可能也是個entry鏈,如果是entry鏈,直接在鏈表頭部插入。
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

數組長度一定是2的次冪是因爲,爲了保持新的數組索引和老的數組索引的大致上的一致
舉例:h = 21, length - 1 = 15 ,new_length - 1 = 31。
    1 0 1 0 1
&  0 1 1 1 1
————————
     0 0 1 0 1    =5

    0 1 0 1 0 1
&  0 1 1 1 1 1
————————
     0 1 0 1 0 1    =21
當length-1低位全爲1的情況下,h的低位就只有一種情況。這樣就避免了之前散列的很好的老數組的數據位置重新調換

  1. get()方法:
public V get(Object key) {
     //如果key爲null,則直接去table[0]處去檢索即可。
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
 }

getEntry()方法:

final Entry<K,V> getEntry(Object key) {
            
        if (size == 0) {
            return null;
        }
        //通過key的hashcode值計算hash值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor (hash&length-1) 獲取最終數組索引,然後遍歷鏈表,通過equals方法比對找出對應記錄
        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 != null && key.equals(k))))
                return e;
        }
        return null;
    }    
  1. 寫equals()方法時需要重寫hashCode()方法

轉載自
作者: dreamcatcher-cx
出處: http://www.cnblogs.com/chengxiao/

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