1. 程式人生 > >Hashmap\LinkedHashMap的實現原理分析

Hashmap\LinkedHashMap的實現原理分析

雖然網上已有很多人寫關於HashMap原始碼分析的文章,但看完過一段時間後,又有點模糊了,覺得只有自己真正再將其履一遍,並真正把它能講清楚,自己才算真正掌握了。在讀本文之前如果你對以下幾個問題都瞭如指掌,此文可略過。
1. HashMap的資料結構是什麼?hash衝突是指什麼?
2. HashMap是怎麼解決hash衝突的,連結串列法是如何實現的?
3. 為什麼HashMap的容量必須是2的指數冪?
4. LinkedHashMap的實現與HashMap有哪些不同?LinkedHashMap是怎麼實現元素有序的?
5. 為什麼重寫equals方法必須重寫hashcode方法?
6. HashMap的put方法實現過程?

1. 資料結構

hashmap實際上是一個數組+連結串列的資料結構,hashmap底層是一個數組結構,陣列中每一項又是一個連結串列結構。連結串列是為了解決hash衝突。

     /**
     * 空表
     */
    static final HashMapEntry<?,?>[] EMPTY_TABLE = {};

    /**
     * 內部是一個數組,陣列的每一項是一個連結串列,陣列的長度必須是2的指數冪,當需要時進行擴容
     */
    transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;
    /**
     * 陣列的每一項是一個連結串列結構,通過next屬性指向下一項,連結串列的每一項的是一個map結構資料,包含key、value
     */
static class HashMapEntry<K,V> implements Map.Entry<K,V> { final K key; V value; HashMapEntry<K,V> next; int hash; }

2. hashmap的初始化

hashmap的建構函式中會對hashmap容量進行初始化,預設初始容量是16,裝載因為為0.75,即當容量達到12時,會對hashmap進行擴容,增大一部,變成32.我們也可以呼叫其建構函式,自己設定其容量大小。

//initialCapacity初始容量,預設16、loadFactor裝載因子,預設0.75
public HashMap(int initialCapacity, float loadFactor) {

 }

3. hasmap存取實現

3.1 put操作

put操作實現的方式是:根據key計算出hash值,查詢在陣列中對應的位置,判斷該陣列中對應位置是否有值,有值再判斷是否有相同的key值,有則將新值替換舊值,並將舊值返回,儲存結束。該陣列在對應位置中沒有值,或是有值但沒有相同的key值,則新建一個Entry元素,並將其放到對應的陣列位置處。
先看下原始碼:

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            //判斷鍵是否為空,為空放到陣列頭部table[0]位置
            return putForNullKey(value);    
        //步驟1:通過hash演算法計算出key的hash值
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        //步驟2:根據hash值找到其對應在hashmap陣列中位置
        int i = indexFor(hash, table.length);
        //步驟3:查詢hashmap的陣列判斷該位置是否有值,沒有直接略過for迴圈,有則進入for迴圈中查詢該位置對應的連結串列中元素的key值是否和當前要儲存的元素的key值相同
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果hash值相同且key值相同,會用新元素替換舊元素,並將舊元素返回
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //步驟4:如果當前元素的hash值對應陣列位置中連結串列不存在或是連結串列存在,但是連結串列中所有元素值的key與當前要儲存的元素key值不一樣的話,新建一個Entry儲存到hashmap中
        addEntry(hash, key, value, i);
        return null;
    }

上述四個步驟並沒有體現出hash衝突的處理方法,hash衝突是指key值不一樣,但是計算出的hash值是一樣的。我們接著看addEntry方法:

void addEntry(int hash, K key, V value, int bucketIndex) {
    //步驟1:檢查當前hashmap中有值的陣列是否達到極限值(預設初始容量16*0.75),若超過,會進行陣列擴容。重新計算出當前要存入元素hash值對應在新陣列中的位置
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    //步驟2:新建一個Entry物件存入hashmap中
    createEntry(hash, key, value, bucketIndex);
}

我們再進入createEntry函式檢視hashmap是怎麼將新元素存入陣列中的。

void createEntry(int hash, K key, V value, int bucketIndex) {
    //步驟1:取出當前陣列中的元素,是一個Entry連結串列結構資料
    HashMapEntry<K,V> e = table[bucketIndex];
    //步驟2:將當前元素加入到Entry中,將原值e和新值的key,value,hash傳進去
    table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
    size++;
}

在這裡還沒有體現出hashmap處理衝突的方式,我們接著看新建一個Entry元素方法。

HashMapEntry(int h, K k, V v, HashMapEntry<K,V> n) {
    value = v;//新值value
    next = n;//關鍵點,將舊值賦給新值的next,這樣就相當於把新值放在連結串列頭部,新元素的next指向原陣列連結串列中的值
    key = k;
    hash = h;
}

終於找到了hashmap處理衝突的程式碼,當發生hash衝突時,會將新元素放在連結串列頭部,並將next指向原連結串列中元素。打個比方, 第一個鍵值對A進來,通過計算其key的hash得到的index=2,記做:Entry[2] = A。一會後又進來一個鍵值對B,通過計算其index也等於2,HashMap會將B.next = A,Entry[2] = B,如果又進來C,index也等於2,那麼C.next = B,Entry[2] = C;這樣我們發現index=2的地方其實存取了A,B,C三個鍵值對,他們通過next這個屬性連結在一起。

3.2 根據hash值查詢在陣列中位置

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

按位取並,作用上相當於取模mod或者取餘%操作,但是利用二進位制操作更快。這裡就體現了為什麼陣列長度必須是2的指數冪,只有2的指數冪,比如16的二進位制表示為 10000,那麼length-1就是15,二進位制為01111,同理擴容後的陣列長度為32,二進位制表示為100000,length-1為31,二進位制表示為011111。而擴容後只有一位差異,也就是多出了最左位的1,這樣在通過 h&(length-1)的時候,只要h對應的最左邊的那一個差異位為0,就能保證得到的新的陣列索引和老陣列索引一致,大大減少了之前已經雜湊良好的老陣列的資料位置重新調換.

3.3 存放key為空值實現

key值為空的所有元素都放到陣列的頭部table[0]位置處,這裡會判斷陣列頭部table[0]位置處是否有值,有值,再判斷是否有相同的key值,有則替換,沒有則新建一個Entry,步驟跟上面類似

private V putForNullKey(V value) {
        for (HashMapEntry<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;
    }

4 get方法

get操作的原始碼如下:

public V get(Object key) {
        //如果key值為null,則從陣列頭部中查詢
        if (key == null)
            return getForNullKey();
        //根據元素的key值查找出對應的Entry
        Entry<K,V> entry = getEntry(key);
        //返回Entry對應的value值
        return null == entry ? null : entry.getValue();
    }

我們再看看getEntry()方法的實現

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        //步驟一:計算出key對應的hash值
        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        //步驟二:查詢hash值對應在陣列中是否存在值,存在則判斷對應Entry的key是否和要查詢元素的key值相同,相同則返回對應的Entry
        for (HashMapEntry<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;
    }

5. hashmap遍歷方法

1)採用Iterator遍歷

    Iterator iter = map.entrySet().iterator();
  while (iter.hasNext()) {
      Map.Entry entry = (Map.Entry) iter.next();
      Object key = entry.getKey();
      Object val = entry.getValue();
  }

2)for each遍歷

for (Entry<String, String> entry : map.entrySet()) {
    Object key = entry.getKey();
    Object val = entry.getValue();
}

6. LinkedHashMap原理

LinkedHashMap是HashMap的子類,實現結構和HashMap類似,只是HashMap中的連結串列是單向連結串列,而LinkedHashMap是雙向連結串列,只是在在HashMap的基礎上加上了排序實現。

6.1 建構函式

LinkedHashMap的建構函式較HashMap多了一個排序引數accessOrder。

/**
     * @param  initialCapacity 初始容量,16
     * @param  loadFactor      裝載因子0.75
     * @param  accessOrder     排序方式標識,為true代表陣列元素按照訪問順序排序,為false代表按照插入順序排序,預設按插入順序排序
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

6.2 排序方式實現

在LinkedHashMapEntry中重寫了recordAccess方法,在HashMap的HashMapEntry是個空方法。該方法判斷是否按訪問順序進行排序,如果是呼叫addBefore()將當前被訪問的元素移至連結串列頭部。如果不是按訪問順序排序,則連結串列中元素沒有變化。因為插入元素時,新插入的元素在HashMap中實現就是放到連結串列頭部的。

void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
        }

/**
 * 將當前元素移至連結串列頭部
 */
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

6.3 put實現

LinkedHashMap沒有對put方式進行重寫,但對addEntry()、createEntry()和LinkedHashMapEntry中的recordAccess()方法進行了重寫。因為在HashMap中put一個元素時,如果要儲存的元素的hash值和key值在當前連結串列中存在的話,在替換舊值後,就呼叫了recordAccess()方法。而在createEntry()方法中,LinkedHashMap的實現如下,添加了addBefore()方法呼叫。將當前新插入的元素放至連結串列頭部。

void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry<K,V> old = table[bucketIndex];
        LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
        table[bucketIndex] = e;
        e.addBefore(header);//關鍵點,將當前元素放到連結串列頭部,從而實現最近訪問的元素都在連結串列頭部
        size++;
    }

6.3 get實現

LinkedHashMap中,在進行獲取元素時,也呼叫了recordAccess方法,將訪問元素移至連結串列頭部

public V get(Object key) {
        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);//關鍵點,如果是按訪問順序排序,會將當前訪問的元素移至連結串列頭部
        return e.value;
    }

7. LruCache中呼叫LinkedHashMap的實現

final LinkedHashMap<T, Y> cache = new LinkedHashMap<T, Y>(100, 0.75f, true);

通過設定accessOrder引數為true,就實現了按訪問順序排序。

8、為什麼object類的equals()重寫同時也要重寫 hashcode ()?

Object物件的equals(Object)方法,對於任何非空引用值x和y,當且僅當x和y引用同一個物件時,此方法才返回true。
當equals()方法被重寫時,通常必須重寫hashcode()方法,hash協定宣告相等物件必須具有相等的hashcode,如下:

1)當obj1.equals(obj2)為true時,obj1.hashCode()==obj2.hashCode()必須為true
2)當obj1.hashCode()==obj2.hashCode()為false時,obj1.equals(obj2)必須為false,若obj1.hashCode()==obj2.hashCode()為true時,obj1.equals(obj2)不一定為true.

若不重寫equals,比較的將是物件的引用是否指向同一塊記憶體地址,重寫後目的是為了比較兩個物件的value值是否相等。
Hashcode是用於雜湊資料快速存取,如利用HashSet\HashMap\Hashtable類來儲存資料時,都是先進行hashcode值判斷是否相同,不相同則沒必要再進行equals比較。
如果重寫了equeals()但沒重寫hashcode(),再new一個物件時,當原物件.equals(新物件)等於true時,兩者的hashcode卻不是一樣的,由此會得出兩個物件不相等的性況。在儲存時,也會發生兩個值一樣的物件,hashcode不一樣而儲存兩個,導致混淆。所以Object的equals()方法重寫同時也要重寫hashcode()方法。

經過自己擼一遍原始碼,自己再把它寫下來,對HashMap實現的理解又加深了一點映象。