1. 程式人生 > >面試必考之HashMap原始碼分析與實現

面試必考之HashMap原始碼分析與實現

以下是JDK1.8之前版本的原始碼簡介
一、什麼是HashMap?

Hash:雜湊將一個任意的長度通過某種(hash函式演算法)演算法轉換成一個固定值。

Map:儲存的集合、類似於地圖X,Y座標的儲存
總結:通過hash出來的值,然後通過這個值定位到這個map,然後把這個value儲存到這個map中 ~~ hashMap基本原理

二、HashMap形式?

key,value。例如 wukong 30 、電話薄 a-z字母等等

二、面試中問到的問題?

1.0 key值可以為空嗎?

【回答】hashMap把Null當做一個key值來儲存,原因看原始碼

 public V put
(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } //原始碼中判斷了,如果key值未空的時候,沒有返回錯誤資訊,也是允許儲存的 if (key == null) return putForNullKey(value); int hash = hash(key); ............. }

2.0 HashMap和Hashtable的區別

【回答】HashMap是Hashtable的輕量級實現(非執行緒安全的實現),他們都完成了Map介面,主要區別在於HashMap允許空(null)鍵值(key),由於非執行緒安全,效率上可能高於Hashtable。


HashMap允許將null作為一個entry的key或者value,而Hashtable不允許。
HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因為contains方法容易讓人引起誤解。
Hashtable繼承自Dictionary類,而HashMap是Java1.2引進的Map interface的一個實現。
最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多個執行緒訪問Hashtable時,不需要自己為它的方法實現同步,而HashMap 就必須為之提供外同步。

Hashtable和HashMap採用的hash/rehash演算法都大概一樣,所以效能不會有很大的差異。

3.0 如果Hash key重複了,那麼value值會覆蓋嗎?

【回答】不會覆蓋、簡單解釋:在Entry類中,有個Entry< K,V > next 例項變數;它是來儲存hashKey衝突時,存放就的value值。不會覆蓋。詳細也是見原始碼

  static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
       ......
   }
 public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        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))) {
                //這裡實際上是先獲取原來的value,儲存老的備份,可以通過  xxx.get("xiaozheng").next.getValue()獲取。
                V oldValue = e.value; // 假設原來的是30,傳進來的是31 。 目前還是30
                //然後在把當前的value賦給它
                e.value = value;  // 31
                e.recordAccess(this);
                return oldValue; 
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

這裡寫圖片描述
4.0 HashMap什麼時候做擴容?

【回答】在put的時候,HashMap集合的容量高於0.75的時候,進行擴容。而且擴容是偶數的,以雙倍的形式向上擴容。具體也是看原始碼
這裡寫圖片描述

5.0 HashMap效能受什麼影響?

【回答】HashMap主要是受 初始化容量跟載入因子。初始化容量:建立一個HashMap預設給與多大的容量的值。載入因子:HashMap在擴容之前最大可以達到的容量。具體原因的話,看下面的“|不足之處“就明白了

6.0 HashMap table的資料結構?

【回答】陣列+連結串列。取兩者的優點

四、原始碼分析
4.1 初始化引數介紹
        4.1.1 初始化容量

    /**
     * 初始化容量  1左移4位  6位
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

        4.1.2 最大容量

 /**
     * 最大容量 1左移30位  也就是 2的30次方。儲存的HashMap的個數不能超過該最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

        4.1.3 載入因子:這也就可以解釋為什麼上述我說:HashMap集合的容量高於0.75的時候,進行擴容。0.75不是我說的,而是在程式碼中定義好,0.75

    /*
     * 載入因子係數
     * 可以理解成:在當前容量的四分之三的時候進行擴容。
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

        4.1.2 其他初始化引數

 /*
     * 建立一個Entry物件
     */
    static final Entry<?,?>[] EMPTY_TABLE = {};
    /**
     * 對table進行賦值
     */
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

    transient int size;
    //擴容變數
    int threshold;
    //臨時載入因子
    final float loadFactor;

    //修改標記
    transient int modCount;

    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

4.2 HashMap構造方法
構造方法的話,我們只需要瞭解下述三個構造方法即可 - - 我們可以手動指定HashMap的初始化容量以及它的載入因子。這也是提高HashMap效能的一種方式。例如:如果你知道你的hashMap需要儲存10萬個map。那麼一開始可以調大你的初始化容量。避免一開始16個集合,多次擴容,多次拷貝帶來的時間、效能消耗

  public HashMap(int initialCapacity, float loadFactor) {
        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();
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

4.3 put方法分析
上述第一個問題跟第三個問題的答案就這下述程式碼裡面

 public V put(K key, V value) {
        //判斷table是否已經初始化
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //如果key未空的話,呼叫存空key的方法,沒有報錯,也就是支援空key的情況
        if (key == null)
            return putForNullKey(value);
        //這也是hash的定義,雜湊,將key進行某種演算法(hash演算法),轉化成一個固定的值。也就是map資料的下標
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        //for迴圈判斷key的hash值是否是相等的,如果相等的話,就進行換位~~ 這裡結合下面下面的圖可以理解
        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))) {
                //這裡獲取原來舊的value
                V oldValue = e.value;
                //再把當前的value跟key對應,沒有覆蓋
                e.value = value;
                //儲存舊value
                e.recordAccess(this);
                return oldValue;
            }
        }
        //修改標記+1
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
   //核心程式碼,if(),也就是當前容量達到0.75,就會執行resize()方法,進行擴容。這也是上述4.0問題的答案。
   //下面我們可以看一下resize的方法--擴容方法
   void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //執行擴容
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
  void resize(int newCapacity) {
        //老的table資料
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //建立Entry物件陣列
        Entry[] newTable = new Entry[newCapacity];
        //這個方法是幹什麼用的呢?    -- 回答:賦值  ,
        //可見每次擴容的時候都必須把原來就的資料賦值到新的陣列過去,這就產生很大的開銷,這也就能夠解釋為什麼載入因子和初始量影響著HashMap效能這個問題
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

4.4 get方法分析
比較簡單,緊跟4.5一起看

 public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

4.5 entry物件介紹
Entry有next屬性變數。專門用來處理key值出現重複的情況用的,詳細解釋看下圖片

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        //預設情況之下,next=null,當出現一個key相同的情況之下,當前的value就會被替換,同時當前next-》會指向原來的value。看圖片
        Entry<K,V> next;
        int hash;

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

這裡寫圖片描述
4.6 賦值原始碼分析

   void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        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);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

五、不足之處
5.1 HashMap獲取Map集合的時間複雜度?
【回答】:與key值是否重複有關係,一般情況下g(O)1。如果出現key值重複的話,那就另外計算。key值是否重複取決於我們的Hash演算法
總結:時間複雜度:你的hash演算法絕對了你的效率
5.2 從伸縮性的角度分析不足之處
【回答】每當hashMap擴容的時候需要重新去add entry物件,需要重新Hash。然後放入我們新的entry table數組裡面。
如果你們的工作中你知道你的hashMap需要存多少值,幾千或者幾萬的時候,最好就是指定它們的擴容大小,防止在put的時候進行再次擴容 多次擴容