1. 程式人生 > >HashMap底層儲存原理

HashMap底層儲存原理

HashMap在日常工作中使用場景非常多,程式設計師都知道是HashMap是執行緒非安全的,但是底層是以什麼方式儲存的?本人仔細研讀了一下原始碼,也只是掌握了核心的儲存功能,並沒有把全部程式碼看明白,但是對於理解hashMap的儲存結構完全夠了。

儲存結構

  1. hashmap底層是以陣列方式進行儲存。將key-value對作為陣列中的一個元素進行儲存。
  2. key-value都是Map.Entry中的屬性。其中將key的值進行hash之後進行儲存,即每一個key都是計算hash值,然後再儲存。每一個Hash值對應一個數組下標,陣列下標是根據hash值和陣列長度計算得來。
  3. 由於不能的key有可能hash值相同,即該位置的陣列中的元素出現兩個,對於這種情況,hashmap採用連結串列形式進行儲存。
  4. 下圖描述了hashmap的儲存結構圖
    hashmap結構

Entry結構分析

  1. Entry是hashMap中封裝key-value鍵值對的,主要包括如下屬性
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;// map中key值,可以為null。
        V value; // map中的value值,可以為null。
        Entry<K,V> next;// 連結串列引用,防止key值不同,hash值相同。
        int hash; // 每個key的hash值
// 建構函式 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; } // 同一個key時,新值替換舊值,返回舊值
public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // key值重寫equals方法 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; } // 重寫hashCode值 public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } public final String toString() { return getKey() + "=" + getValue(); } // 其他方法省略 }

HashMap屬性分析

  1. HashMap的屬性分析
public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{

    /**
     *預設情況下,hashmap大小為16.即1<<4就是1乘以2的4次冪=16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
      * hashMap的最大值
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 預設載入載入因子,即使用空間達到總空間的0.75時,需要擴容。
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 宣告hashmap一個空陣列。
     */
    static final Entry<?,?>[] EMPTY_TABLE = {};

    /**
     * 最開始時,hashmap是一個空陣列。
     */
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

    /**
     * map的元素的個數
     */
    transient int size;

    /*
     * hashmap的實際儲存空間大小。這個空間是總空間*載入因子得出的大小。
     * 比如預設是16,載入因子是0.74。則threshold就是12。
     */
    int threshold;

    /**
     * 載入因子,即使用空間達到總空間的0.75時,需要擴容。
     */
    final float loadFactor;

    /**
     *
     */
    transient int modCount;

    /**
     *  threshold這個值的最大值就是Integer.MAX_VALUE
     */
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

put方法

  1. put(key,value)方法是hashmap中最重要的方法,使用hashmap最主要的就是使用put,get兩個方法。put方法底層儲存又是什麼呢,我們可以從put方法的原始碼進行分析
 public V put(K key, V value) {
         // 首次儲存元素,初始化儲存空間
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // 如果key為null,則將null放入元素的第一個位置
        if (key == null)
            return putForNullKey(value);
        // 計算key的hash值    
        int hash = hash(key);
        // 根據key的hash值,陣列長度計算該Entry<key,value>的陣列下標
        int i = indexFor(hash, table.length);
        /**
        **如果當前key的已經存在於map中,則將新值替換成舊值。
        **/
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 判斷同一個key,既要判斷hash值相同,還要判斷key是同一個key,因為
            // 相同的key有可能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;
            }
        }
       // 如果是新的key需要儲存,則增加操作次數modCount++
        modCount++;
        // 將新增key-value鍵值對新增中map中。
        addEntry(hash, key, value, i);
        return null;
    }

addEntry方法

  1. addEntry方法是將新增的key-value鍵值對存入到map中。該方法主要完成兩個功能:
    1.1. 新增新元素前, 判斷是否需要對map的陣列進行擴容,如果需要擴容,則擴容空間大小是多少?
    1.2. 對於新增key-value鍵值對,如果key的hash值相同,則構造單向列表。
  2. 從原始碼分析結果如下:
/**
**  hash:key的hash值
**  key:儲存的鍵
**  value:儲存的value物件值
*** bucketIndex:陣列下標位置,即key-value在陣列中的位置。
**/
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);
        }
      // 往陣列中新增新的key-value鍵值對
      createEntry(hash, key, value, bucketIndex);
    }

createEntry

  1. 該方法主要完成兩個功能,第一是新增新的key到Entry陣列中,第二就是對於不同key的hash值相同的情況下,在同一個陣列下標處,構建單向連結串列進行儲存。
 void createEntry(int hash, K key, V value, int bucketIndex) {
       // 取出當前位置的元素,如果是新新增的key,則e為null,已經有的元素為不為空。
        Entry<K,V> e = table[bucketIndex];
        // 新增新的key-value值或構建連結串列
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }