Jdk1.7中HashMap結構及代碼追蹤

HashMap在JDK1.7版本中的數據存儲結構實際上是一個 Entry<?, ?>[] EMPTY_TABLE數組

 static final Entry<?, ?>[] EMPTY_TABLE = {};

 // table就是HashMap實際存儲數組的地方
 transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;

 顯而易見,其中的每個元素又是一個鏈表

    static class Entry<K, V> implements Map.Entry<K, V> {

        final K key;

        V value;

        Entry<K, V> next;

        int hash;
...//省略後續代碼

因此,Java7 HashMap的結構大致如下圖

 總結:簡單來說,HashMap中的數據存儲結構是個數組,而每個元素都是一個單向鏈表,鏈表中每個元素是一個Entry的內部類對象,每個對象包含四個屬性:key,value,hash值,和用於單向鏈表的next

重要的成員變量:看一下其中的其他成員變量 

    // 默認的HashMap的空間大小16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認值是16

    // hashMap最大的空間大小
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // HashMap默認負載因子,負載因子越小,hash衝突機率越低
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //初始化的空數組
    static final Entry<?, ?>[] EMPTY_TABLE = {};

    // table就是HashMap實際存儲數組的地方
    transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;

    // HashMap 實際存儲的元素個數
    transient int size;

    // 臨界值(超過這個值則開始擴容),公式爲(threshold = capacity * loadFactor)
    int threshold;

    // HashMap 負載因子
    final float loadFactor;

 構造方法:從源碼中可以看出HashMap一共有個四個構造,他們分別爲

 

    //1.默認構造,會調用默認默認空間大小16和默認負載因子0.75
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

 

  //2.指定大小但不指定負載因子
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    //3.指定大小和負載因子
    public HashMap(int initialCapacity, float loadFactor) {
        //此處對傳入的初始容量進行校驗,最大不能超過MAXIMUM_CAPACITY = 1<<30(2的30次方)
        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方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現
        init();
    }
    //4.使用默認構造創建對象並將指定的map集合放入新創建的對象中
    public HashMap(Map<? extends K, ? extends V> m) {

        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,

                DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        //初始化數組
        inflateTable(threshold);
        //添加指定集合中的元素
        putAllForCreate(m);

    }

上面四個構造實際上都是在使用第三個構造方法:類中有幾個比較重要的字段:

//實際存儲的key-value鍵值對的個數
transient int size;

//當前數組容量,始終保持 2^n,可以擴容,擴容後數組大小爲當前的 2 倍。
int capacity

//閾值,當table == {}時,該值爲初始容量(初始容量默認爲16);當table被填充了,也就是爲table分配內存空間後,
//threshold一般爲 capacity*loadFactory。HashMap在進行擴容時需要參考threshold,後面會詳細談到
int threshold;

//負載因子,代表了table的填充度有多少,默認是0.75
final float loadFactor;

//用於快速失敗,由於HashMap非線程安全,在對HashMap進行迭代時,
//如果期間其他線程的參與導致HashMap的結構發生變化了(比如put,remove等操作),需要拋出異常ConcurrentModificationException
transient int modCount;

 從源碼中不難看出,實際上,在構造器中(第四個除外),並沒有爲數組分配內存空間,而是在put操作的時候才進行數據的構建

put操作 

下面看下put方法的執行過程 

  • 首先要判斷數字是否爲空數組,如果是空數組的話,需要對數組進行初始化
  • 如果key是null的話,回家元素的值彷彿唉table的0索引上,此時終止操作
  • 如果key值不是null的話
  1. 對key進行hash操作,獲取到hash值
  2. 找到key對應的數組下標
  3. 獲取到鏈表對象後遍歷鏈表,看是否有重複的key存在,如果有直接覆蓋並返回原來位置上的值,就此結束
  4. 如果不存在重複的key,將此該key和value組裝程Entry對象添加到鏈表中(存在數組擴容問題-後面有介紹)
//put操作源碼
 public V put(K key, V value) {
        // 當插入第一個元素的時候,需要先初始化數組大小
        if (table == EMPTY_TABLE) {
            // 數組初始化
            inflateTable(threshold);

        }
        // 如果 key 爲 null,最終會將這個 entry 放到 table[0] 中
        if (key == null)

            return putForNullKey(value);
        // 1. 求 key 的 hash 值
        int hash = hash(key);
        // 2. 找到對應的數組下標
        int i = indexFor(hash, table.length);
        // 3. 遍歷一下對應下標處的鏈表,看是否有重複的 key 已經存在,如果有,直接覆蓋,put 方法返回舊值就結束了
        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))) { // key -> value

                V oldValue = e.value;

                e.value = value;

                e.recordAccess(this);

                return oldValue;

            }
        }
        modCount++;
        // 4. 不存在重複的 key,將此 entry 添加到鏈表中
        addEntry(hash, key, value, i);
        return null;
    }

 數組初始化

   ps:在添加元素的開始,需要對數組是否初始化做判斷,如果沒有初始化需要做初始化處理

   保證數組大小是是2的N次方的好處:

當數組長度爲2的n次冪的時候,
1、位移運算效率較高
2、不同的key的hash計算結果相同的機率較低,減少hash碰撞,使得數據在數組上分佈的比較均勻,
   查詢的時候就不用遍歷某個位置上的鏈表,可以提升定位元素的的效率

 


    private void inflateTable(int toSize) {

        // Find a power of 2 >= toSize 保證數組大小一定是 2 的 n 次方。
        // new HashMap(519),大小是1024
        //將數組大小保持爲2的n次方,在Java7和Java8的HashMap和 ConcurrentHashMap 都有相應的要求,實現代碼略有不同
        int capacity = roundUpToPowerOf2(toSize);

        // 計算擴容閾值:capacity * loadFactor
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        // 初始化數組
        table = new Entry[capacity];

        initHashSeedAsNeeded(capacity);

    }
//    確保capacity爲大於或等於toSize的最接近toSize的二次冪,比如toSize=13,則capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.
    static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        // 返回最接近臨界值的2的N次方
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;

    }

 

 計算元素在數組的具體位置

//簡單說就是取 hash 值的低 n 位。如在數組長度爲 32 的時候,其實取的就是 key 的 hash 值的低 5 位,作爲它在數組中的下標位置
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        // 簡單理解就是hash值和長度取模
        return h & (length - 1);
    }

 添加到鏈表

  ps:找到數組位置之後,就需要對key進行判重處理,如果有的話,就覆蓋重複key的值,返回舊值(判斷重複邏輯爲:可以的hash值相同,且原來key和當前key相等),如果沒有重複值,將新值放在鏈表的表頭

 

//主要邏輯爲,先判斷是否需要擴容,如果需要擴容就先擴容,最後再把數據封裝程Entry對象加入到鏈表表頭
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 如果當前 HashMap 大小已經達到了閾值,並且新值要插入的數組位置已經有元素了,那麼要擴容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            // 擴容,容量 * 2
            resize(2 * table.length);
            // 擴容以後,重新計算 hash 值
            hash = (null != key) ? hash(key) : 0;
            // 重新計算擴容後的新的下標
            bucketIndex = indexFor(hash, table.length);

        }

        // 創建元素
        createEntry(hash, key, value, bucketIndex);

    }

    // 將新值放到鏈表的表頭,然後 size++
    void createEntry(int hash, K key, V value, int bucketIndex) {

        Entry<K, V> e = table[bucketIndex];

        table[bucketIndex] = new Entry<>(hash, key, value, e);

        size++;

    }

數組擴容 

 長度爲當前長度的2倍

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        // 如果之前的HashMap已經擴充到最大了,那麼就將臨界值threshold設置爲最大的int值
        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);

    }
    //數組的拷貝
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        // 遍歷舊的數組
        for (Entry<K, V> e : table) {
            while (null != e) {
                //獲取下一個entry對象
                Entry<K, V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //當前對象的hash值
                int i = indexFor(e.hash, newCapacity);
                //頭插法,Entry對象放在新數組上第一位置,其他對象放在該對象的後一位置
                e.next = newTable[i];
                //將整體的對象放在指定的索引位置
                newTable[i] = e;
                //繼續循環下一個Entry
                e = next;
            }
        }
    }

get方法跟蹤

  • 根據key計算出key的hash值
  • 找到對應的數組下標
  • 遍歷該數組下的鏈表,直到找到與之相等的key的值,或找不到返回null
    //獲取數據
    public V get(Object key) {

        if (key == null)
            //如果key爲null,就從table[0]獲取(put中,key爲null也是存儲在該位置)
            return getForNullKey();
        Entry<K, V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }
//    從鏈表中查詢數據
    final Entry<K, V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);
        // 確定key對應的數組位置,遍歷鏈表直至找到,或者最終找不到返回null
        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;

    }

 

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