HashMap 源碼解析

HashMap簡介:java

  HashMap在平常的開發中應用的很是之普遍,它是基於Hash表,實現了Map接口,以鍵值對(key-value)形式進行數據存儲,HashMap在數據結構上使用的是數組+鏈表。容許null鍵和null值,不保證鍵值對的順序。算法

HashMap檢索數據的大體流程:數組

  當咱們使用HashMap搜索key所對應的value時,HashMap會根據Hash算法對key進行計算,獲得一個key的hash值,再根據hash值算出該key在數組中存儲的位置index,而後獲取數組在index位置的鍵值對e,再使用鏈表對e進行遍歷,查找遍歷的元素是否和給定的key相符合,如有符合的,則返回其value值。安全

 本身手動畫了一個HashMap的數據結構圖:數據結構

HashMap源碼分析:多線程

  HashMap是存儲鍵值對的集合,實現了Map接口,下面咱們看一下Map接口的定義:app

 

/**
*映射key到value的頂級接口,不能包含重複的key,一個key最多能夠映射到一個value,鍵和值都可爲null
*/
public interface Map<K,V> {

    //返回該map包含的鍵值對的個數,若是鍵值對的個數超過了Integer.MAX_VALUE,則返會Integer.MAX_VALUE
    int size();

    //若是該Map沒有包含鍵值對,則返回true,不然返回false
    boolean isEmpty();

    //判斷該map是否包含指定的key所對應的鍵值對,key能夠爲null
    boolean containsKey(Object key);

    //判斷該map是否包含指定的value所對應的鍵值對,若map中包含有一個及以上的key,對應指定的value,則返回true,value能夠爲null
    boolean containsValue(Object value);

    //返回指定的key所對應的value,若key不存在,則返回null;可是返回null的key,不表明key在map中不存在,有多是key所對應的value爲null
    V get(Object key);

    //將指定的key和value映射爲一個鍵值對,放入map中;若以前該map中包含了指定的Key,則該key所對應的老的值oldValue將會被替換爲value
    V put(K key, V value);

    //刪除指定的key所對應的鍵值對,並返回該key對應的value
    V remove(Object key);

    //將指定的map中的鍵值對複製到當前map中
    void putAll(Map<? extends K, ? extends V> m);

    //清除map中全部的鍵值對,該操做完成後,該map就是空的了
    void clear();

    //將map中全部的key返回,因爲map中的key是不能重複的,因此用Set集合的方式裝載全部的key
    Set<K> keySet();

    //將map中全部的value返回,因爲map中的value是可重複的,因此用Collection集合的方式裝載全部的value
    Collection<V> values();

    //將map中全部的鍵值對Entry返回,因爲map中的鍵值對是不可重複的(key不可重複),因此用Set集合的方式裝載全部的value
    Set<Map.Entry<K, V>> entrySet();

    //Map中承載鍵值對的數據結構Entry
    interface Entry<K,V> {

        //返回鍵值對的鍵值key
        K getKey();

        //返回鍵值對的value值
        V getValue();

        //將當前鍵值對的value值 替換爲指定的value值
        V setValue(V value);

        //判斷指定的對象和當前鍵值對是否equals相等,若相等,則表明是同一個鍵值對;
        boolean equals(Object o);

        //返回當前鍵值對的hashCode值
        int hashCode();
    }

    //判斷指定對象和當前Map的equals是否相等
    boolean equals(Object o);

    //返回當前Map的hashCode
    int hashCode();
}

 

 

下面咱們具體的看一下HashMap:函數

    //HashMap是基於hash表來實現Map接口的,HashMap維護了下面幾個變量:

    //HashMap默認的初始化大小爲16
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    //HashMap包含鍵值對的最大容量爲2^30,HashMap的容量必定要是2的整數次冪
    static final int MAXIMUM_CAPACITY = 1 << 30;

    //默認的加載因子爲0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //裝載鍵值對的內部容器Entry數組,長度必定得是2的冪
    transient Entry[] table;

    //HashMap中包含的鍵值對的個數
    transient int size;

    //HashMap的極限 當鍵值對的個數達到threshold時,數組table要擴容的
    int threshold;

    //加載因子
    final float loadFactor;

    //HashMap結構上被改變的次數,結構上的改變包括:鍵值對的大小的改變,修改HashMap的內部結構(比較進行了rehash操做),此屬性用來保持fail-fast
    transient volatile int modCount;

接下來咱們看一下HashMap的構造函數:源碼分析

/**
*根據指定的容量initialCapacity和加載因子loadFactor構造HashMap
*/
    public HashMap(int initialCapacity, float loadFactor) {
        //對initialCapacity進行參數校驗,若小於0,則拋出IllegalArgumentException異常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //若initialCapacity大於MAXIMUM_CAPACITY(2^30),則將MAXIMUM_CAPACITY賦值給initialCapacity
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //對參數loadFactor進行有效性校驗,不能<=0,不能非數字,不然拋出IllegalArgumentException異常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        // Find a power of 2 >= initialCapacity 找到一個2的冪的數capacity,使其不小於參數initialCapacity
        int capacity = 1;
        //若capacity小於initialCapacity,則將capacity擴大一倍
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        //設置極限,大小爲 capacity * loadFactor,即(容量*負載因子)
        threshold = (int)(capacity * loadFactor);
        //建立一個大小爲capacity的Entry數組table,用來保存鍵值對
        table = new Entry[capacity];
        //調用方法init(),進行額外的初始化操做
        init();
    }
    //init方法是空的,若是你定製額外的初始化操做,能夠繼承HashMap,覆蓋init()方法
    void init() {

    }

    //經過指定的容量initialCapacity來構造HashMap,這裏使用了默認的加載因子DEFAULT_LOAD_FACTOR 0.75
    public HashMap(int initialCapacity) {
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    //無參的構造函數 加載因子爲DEFAULT_LOAD_FACTOR 0.75,容量爲默認的DEFAULT_INITIAL_CAPACITY 16,極限爲 16*0.75=12
    public HashMap() {
            this.loadFactor = DEFAULT_LOAD_FACTOR;
            threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
            table = new Entry[DEFAULT_INITIAL_CAPACITY];
            init();
    }

 

下面咱們看一下HashMap中承載鍵值對的數據結構Entry的實現,Entry<K,V>是HashMap的一個靜態內部類性能

/**
*Entry是HashMap裏面承載鍵值對數據的數據結構,實現了Map接口裏面的Entry接口,除了方法recordAccess(HashMap<K,V> m),recordRemoval(HashMap<K,V> m)外,其餘方法均爲final方法,表示即便是子類也不能覆寫這些方法。
*/
static class Entry<K,V> implements Map.Entry<K,V> {
        //鍵值,被final修飾,代表一旦賦值,不可修改
        final K key;
        //value值
        V value;
        //下一個鍵值對的引用
        Entry<K,V> next;
        //當前鍵值對中鍵值的hash值
        final int hash;

        /**
         * Creates new entry.
         */
        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;
        }

        //設置value值,返回原來的value值
        public final V setValue(V newValue) {
          V oldValue = value;
            value = newValue;
            return oldValue;
        }
        //比較鍵值對是否equals相等,只有鍵、值都相等的狀況下,才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();
            //若k1 == k2(k1,k2引用同一個對象),或者k1.equals(k2)爲true時,進而判斷value值是否相等
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                //若v1 == v2(v1,v2引用同一個對象),或者v1.equals(v2)爲true時,此時才能說當前的鍵值對和指定的的對象equals相等
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            //其餘均爲false
            return false;
        }

        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

        //此方法只有在key已存在的時候調用m.put(key,value)方法時,纔會被調用,即覆蓋原來key所對應的value值時被調用
        void recordAccess(HashMap<K,V> m) {
        }
        //此方法在當前鍵值對被remove時調用
        void recordRemoval(HashMap<K,V> m) {
        }
}

 

下面是Hash的put方法的實現:

/**
*將指定的鍵key,值value放到HashMap中
*/
public V put(K key, V value) {
    //首先判斷鍵key是否爲null,若爲null,則調用putForNullKey(V v)方法完成put
    if (key == null)
        return putForNullKey(value);
    //程序走到這,說明key不爲null了,先調用hash(int)方法,計算key.hashCode的hash值
    int hash = hash(key.hashCode());
    //再調用indexFor(int,int)方法求出此hash值對應在table數組的哪一個下標i上 (bucket桶)
    int i = indexFor(hash, table.length);
    //遍歷bucket桶上面的鏈表元素,找出HashMap中是否有相同的key存在,若存在,則替換其value值,返回原來的value值
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //若元素e.hash值和上面計算的hash值相等,而且(e.key == 入參key,或者入參key equals 相等 e.key),則說明HashMap中存在了和入參相同的key了,執行替換操做
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            //在執行替換操做的時候,調用Entry對象的recordAccess(HashMap<K,V> m)方法,這個上面說過了
            e.recordAccess(this);
            return oldValue;
        }
    }
    //程序走到這,說明原來HashMap中不存在key,則直接將鍵值對插入便可,因爲插入元素,修改了HashMap的結構,所以將modeCount+1
    modCount++;
    //調用addEntry(int,K,V,int)方法進行鍵值對的插入
    addEntry(hash, key, value, i);
    //因爲原來HashMap中不存在key,則不存在替換value值問題,所以返回null
    return null;
}

當key爲null時,HashMap是這樣進行數據插入的:

//先看看HashMap中原先是否有key爲null的鍵值對存在,若存在,則替換原來的value值;若不存在,則將key爲null的鍵值對插入到Entry數組的第一個位置table[0]
private V putForNullKey(V value) {
    //獲取Entry數組的第一個元素:table[0],而後經過e.next進行鏈表的遍歷,若遍歷過程當中有元素e.key爲null,則替換該元素的值
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        //說明原來以前HashMap中就已經存在key問null的鍵值對了,如今又插入了一個key爲null的新元素,則替換掉原來的key爲null的value值
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            //在執行替換操做的時候,調用Entry對象的recordAccess(HashMap<K,V> m)方法,這個上面說過了
            e.recordAccess(this);
            return oldValue;
        }
    }
    //程序走到這,說明HashMap中原來沒有key爲null的鍵值對,須要直接插入元素,因爲插入元素,修改了HashMap的結構,所以將modeCount+1
    modCount++;
    //調用addEntry(int,K,V,int)方法進行鍵值對的插入,這裏將入參hash設置爲0,K爲null,插入的位置index也爲0,說明key爲null的元素插入在bucket[0]第一個桶上
    addEntry(0, null, value, 0);
    return null;
}

 

HashMap在插入數據以前,要根據key值和hash算法來計算key所對應的hash值

//根據key的hashCode值,來計算key的hash值
static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
*在HashMap中要找到某個元素,須要根據key的hash值來求得對應數組中的位置,如何計算這個位置就是hash算法.
*HashMap的數據結構是數組和鏈表的結合,因此咱們固然但願這個HashMap裏面的元素位置儘可能的分佈均勻些,儘可能使得每一個位置上的元素數量只有一個,
*那麼當咱們用hash算法求得這個位置的時候,立刻就能夠知道對應位置的元素就是咱們要的,而不用再去遍歷鏈表.
*因此咱們首先想到的就是把hashcode對數組長度取模運算,這樣一來,元素的分佈相對來講是比較均勻的,可是,「模」運算的消耗仍是比較大的,
*能不能找一種更快速,消耗更小的方式那?java中時這樣作的 :將hash值和數組長度按照位與&來運算
*/
static int indexFor(int h, int length) {
    return h & (length-1);
}

下面咱們看一看實際進行數據添加的操做addEntry方法

/**
*將指定的key,value,hash,bucketIndex 插入鍵值對,若此時size 大於極限threshold,則將Entry數組table擴容到原來容量的兩倍
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
    //取出原來bucketIndex處的鍵值對e
    Entry<K,V> e = table[bucketIndex];
    //建立一個新的鍵值對,使用給定的hash、key、value,並將新鍵值對的next屬性指向e,最後將新鍵值對放在bucketIndex處
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    //將size+1,若此時size 大於極限threshold,則將Entry數組table擴容到原來容量的兩倍
    if (size++ >= threshold)
        //調用resize(int)方法,進行數組的擴容
        resize(2 * table.length);
}

 

咱們知道HashMap採用的數組+鏈表來實現的,當HashMap中元素的個數size大於極限threshold時,會進行數組的擴容操做,這個操做使用resize(int newCapacity)方法實現的:

/**
*將HashMap中Entry數組table的容量擴容至新容量newCapacity,數組的擴容不但涉及到數組元素的複製,還要將原數組中的元素rehash到新的數組中,很耗時;若是能預估到放入HashMap中元素的大小,最好使用new HashMap(size)方式建立足夠容量的HashMap,避免沒必要要的數組擴容(rehash操做),提升效率
*/
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //若是原數組的大小已經爲容許的最大值MAXIMUM_CAPACITY了,則不能進行擴容了,這裏僅僅將極限threshold設置爲Integer.MAX_VALUE,而後返回
    if (oldCapacity == MAXIMUM_CAPACITY) {
        //將極限threshold設置爲Integer.MAX_VALUE
        threshold = Integer.MAX_VALUE;
        return;
    }
    //程序走到這,說明能夠進行擴容,先建立容量爲newCapacity的新Entry數組newTable
    Entry[] newTable = new Entry[newCapacity];
    //調用tranfer(Entry[] newTable)方法,進行數組元素的移動和rehashing
    transfer(newTable);
    //將新數組newTable賦值給table
    table = newTable;
    //計算極限threshold的最新值
    threshold = (int)(newCapacity * loadFactor);
}

//將原Entry數組table中的全部鍵值對遷移到新Entry數組newTable上
void transfer(Entry[] newTable) {
    //原數組賦值給src
    Entry[] src = table;
    //新數組長度爲newCapacity
    int newCapacity = newTable.length;
    //遍歷原數組
    for (int j = 0; j < src.length; j++) {
        //獲取原數組中的元素(鍵值對),賦值給e
        Entry<K,V> e = src[j];
        //若元素e不爲null
        if (e != null) {
            //釋放舊Entry數組的對象引用(for循環後,舊的Entry數組再也不引用任何對象)
            src[j] = null;
            //遍歷元素e所對應的bucket桶內的全部元素
            do {
                //獲取下一個元素next
                Entry<K,V> next = e.next;
                //從新計算鍵值對e在新數組newTable中的bucketIndex位置(即rehash操做)
                int i = indexFor(e.hash, newCapacity);
                //標記[1]
                e.next = newTable[i];
                //將當前元素e放入新數組的i位置
                newTable[i] = e;
                //訪問下一個Entry鏈上的元素
                e = next;
            } while (e != null);
        }
    }
}

 註釋標記[1]處,將newTable[i]的引用賦給了e.next,也就是使用了單鏈表的頭插入方式,同一位置上新元素總會被放在鏈表的頭部位置;這樣先放在一個索引上的元素終會被放到Entry鏈的尾部(若是發生了hash衝突的話);

若某個桶上面的元素不止一個,則會行成一個鏈表,在resize過程當中,會致使鏈表逆序排列。好比在第i個桶內,有元素tab[i1]=a -> tab[i2]=b -> tab[i3]=c -> tab[i4]=d,

在resize後,好比會放到新的數組下標j處,則新的鏈表爲newTab[j1]=d -> newTab[j2]=c -> newTab[j3]=b -> newTab[j4]=a

下面咱們看一下HashMap檢索數據的操做:

//獲取指定key所對應的value值
public V get(Object key) {
    //若key==null,則直接調用getForNullKey()方法
    if (key == null)
        return getForNullKey();
    //到這說明key != null,先獲取key的hash值
    int hash = hash(key.hashCode());
    //在indexFor(int hash,int length)獲取key落在Entry數組的哪一個bucket桶上,獲取該bucket桶上的元素e,進而遍歷e的鏈表,找相對應的key,若找到則返回key所對應的value值,找不到則返回null
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        //若元素e.hash == 上面計算的hash值,而且(e.key 和入參key是同一個對象的引用,或者e.key和入參key equals相等),
        //則認爲入參key和當前遍歷的元素e的key是同一個,返回e的value值
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    //上面遍歷了一遍也沒有找到,則返回null
    return null;
}

//獲取key爲null的value值,由上面putForNullKey方法可知,key爲null的鍵值對,被放在了Entry數組table的第一個bucket桶(table[0])
private V getForNullKey() {
    //獲取Entry數組table的第一個元素e,從e開始往下遍歷,若找到元素e.key 爲null的,則直接返回該元素e.value值
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        //找到元素e.key 爲null的,則直接返回該元素e.value值
        if (e.key == null)
            return e.value;
    }
    //從table[0]開始,遍歷鏈表一遍,沒有找到key爲null的,則返回null
    return null;
}

//獲取指定key所對應的鍵值對Entry,先獲取key的hash值,再獲取該hash值應放入哪一個hash桶,而後遍歷該桶中的鍵值對,如有則返回該鍵值對;若沒有則返回null
final Entry<K,V> getEntry(Object key) {
    //若key爲null,則hash值爲0;若key != null,則利用hash(int)計算key的hash值
    int hash = (key == null) ? 0 : hash(key.hashCode());
    //獲取該hash值應放入哪一個hash桶,並遍歷該hash桶
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        //若元素e.hash == hash,而且(e.key == key,或者 key.equals(e.key)爲true),則認爲入參key和當前遍歷的元素e.key是同一個,返回該元素e
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    //若沒有則返回null
    return null;
}

 

//判斷HashMap中是否含有指定key的鍵值對,內部用getEntry(Object key)來實現
public boolean containsKey(Object key) {
    return getEntry(key) != null;
}

 

 

//將指定Map中的全部元素(鍵值對)拷貝到當前HashMap中,對於當前HashMap中存在的key,則進行value值的替換
public void putAll(Map<? extends K, ? extends V> m) {
    //若指定的Map中沒有元素,則直接返回
    int numKeysToBeAdded = m.size();
    if (numKeysToBeAdded == 0)
        return;

    //若必要,則進行數組的擴容
    if (numKeysToBeAdded > threshold) {
        int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
        if (targetCapacity > MAXIMUM_CAPACITY)
            targetCapacity = MAXIMUM_CAPACITY;
        //計算新的容量
        int newCapacity = table.length;
        while (newCapacity < targetCapacity)
            newCapacity <<= 1;
        //若新容量大於目前數組的長度,則調用resize(int)進行擴容
        if (newCapacity > table.length)
            resize(newCapacity);
    }
    //獲取指定Map的迭代器,循環調用put(K k,V v)方法,進行鍵值對的插入
    for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
        Map.Entry<? extends K, ? extends V> e = i.next();
        put(e.getKey(), e.getValue());
    }
}

 

 

下面看下HashMap的remove操做:

/**
* 刪除指定key所對應的元素   
*/
public V remove(Object key) {
    //調用方法reomveEntryForKey(Object key)來刪除並獲取指定的entry
    Entry<K,V> e = removeEntryForKey(key);
    //若entry爲null,則返回null;不然返回entry的value值
    return (e == null ? null : e.value);
}

/**
*移除並返回指定key所關聯的鍵值對entry,若HashMap中沒有鍵值對和指定的key相關聯,則返回null
*/
final Entry<K,V> removeEntryForKey(Object key) {
    //若key爲null,則hash值爲0;若key != null,則利用hash(int)計算key的hash值
    int hash = (key == null) ? 0 : hash(key.hashCode());
    //獲取key應放入的在數組中的位置索引i
    int i = indexFor(hash, table.length);
    //標識前一個元素
    Entry<K,V> prev = table[i];
    //標識遍歷過程當中的當前元素
    Entry<K,V> e = prev;
    //遍歷bucket桶table[i]中的元素,尋找對應的key
    while (e != null) {
        //當前元素的下一個元素
        Entry<K,V> next = e.next;
        Object k;
        //元素e.hash和上面計算的hash值相等,而且(key == e.key 或者key.equals(e.key) 爲true),說明找到了指定key所對應的鍵值對
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            //因爲刪除了一個元素,修改了HashMap的結構,所以將modCount+1
            modCount++;
            //將size-1
            size--;
            //若待查找的元素爲桶內的第一個元素,則當前元素的下一個元素next放入數組中位置i中
            if (prev == e)
                table[i] = next;
            //不然將上一個元素的next屬性指向 當前元素的next
            else
                prev.next = next;
            //當元素被remove時,調用Entry對象的recordRemove(Map<K,V> m)方法
            e.recordRemoval(this);
            //返回找到的當前元素
            return e;
        }
        //程序走到這,說明當前元素不是要查找的元素;則將當前元素賦值給上一個元素,將下一個元素賦值給當前元素,以完成遍歷
        prev = e;
        e = next;
    }

    return e;
}

 

 

//判斷HashMap中是否包含指定的對象value
public boolean containsValue(Object value) {
    //若value爲null,則調用containsNullValue()方法處理
    if (value == null)
        return containsNullValue();
    //將數組table賦值給tab
    Entry[] tab = table;
    //遍歷數組tab的每一個索引位置(此層循環對應數組結構)
    for (int i = 0; i < tab.length ; i++)
        //對應指定的索引位置i,遍歷在索引位置i上的元素(此層循環對應鏈表結構)
        for (Entry e = tab[i] ; e != null ; e = e.next)
            //若某個元素e.value和指定的value equals相等,則返回true
            if (value.equals(e.value))
                return true;
    //遍歷完成沒有找到對應的value值,返回false
    return false;
}

//判斷HashMap是否包含value爲null的元素
private boolean containsNullValue() {
    //將數組table賦值給tab
    Entry[] tab = table;
    //遍歷數組tab的每一個索引位置(此層循環對應數組結構)
    for (int i = 0; i < tab.length ; i++)
        //對應指定的索引位置i,遍歷在索引位置i上的元素(此層循環對應鏈表結構)
        for (Entry e = tab[i] ; e != null ; e = e.next)
            //若某個元素e.value == null,則返回true
            if (e.value == null)
                return true;
    //沒有找到value值爲null的,返回false
    return false;
}

 

//清除HashMap中全部的鍵值對,此操做事後,HashMap就是空的了
public void clear() {
    //要刪除全部的元素,確定會對HashMap的結構形成改變,所以modCount+1
    modCount++;
    Entry[] tab = table;
    //遍歷數組,將數組中每一個索引位置的設置爲null
    for (int i = 0; i < tab.length; i++)
        tab[i] = null;
    //將size 修改成0
    size = 0;
}

 

 

如今看一下上面沒有講的一個構造函數:

//構造一個新的HashMap,以承載指定Map中全部的鍵值對,使用默認的加載因子DEFAULT_LOAD_FACTOR
public HashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    //調用putAllForCreate(Map<? extends K, ? extends V>)方法完成元素的遷移
    putAllForCreate(m);
}

private void putAllForCreate(Map<? extends K, ? extends V> m) {
    for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
        Map.Entry<? extends K, ? extends V> e = i.next();
        //在迭代器循環中調用putForCreate(K k,V v)來實現元素的建立
        putForCreate(e.getKey(), e.getValue());
    }
}

//該方法和put方法不一樣,它不會進行數組的擴容resize,和對modCount的檢查
private void putForCreate(K key, V value) {
    //若key爲null,則hash值爲0;若key != null,則利用hash(int)計算key的hash值
    int hash = (key == null) ? 0 : hash(key.hashCode());
    //求key應該放入哪一個hash桶(bucket)內
    int i = indexFor(hash, table.length);
    //遍歷鏈表,若key以前在Map中已經有了,則直接返回
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            e.value = value;
            return;
        }
    }
    //調用createEntry(int hash,K k,V v,int bucketIndex)方法完成鍵值對的建立並加入到鏈表中
    createEntry(hash, key, value, i);
}

void createEntry(int hash, K key, V value, int bucketIndex) {
    //將bucketIndex位置的元素取出來
    Entry<K,V> e = table[bucketIndex];
    //建立一個新的鍵值對,使用給定的hash、key、value,並將新鍵值對next指向e,最後將新鍵值對放在bucketIndex處
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    //將數組大小size + 1
    size++;
}

 

  下面咱們講下HashMap的負載因子loadfactor, 所謂負載因子就是散列表的實際元素數目(n)/散列表的容量(m), 它衡量的是一個散列表的空間的使用程度,默認狀況下loadfactor是0.75,它的做用是當HashMap中元素的個數size 大於(HashMap的容量capacity * 負載因子loadfactor)時,該HashMap便會進行擴容resize。

  咱們先說下hash衝突:

  當利用HashMap存數據的時候,先根據key的hashcode值來計算key的hash值(利用hash函數),再根據hash值來計算該key在數組table中的位置index(hash & (length-1)),好比咱們要存兩個鍵值對key1-value1和key2-value2,

若是key一、key2分別經過hash函數計算的hash值hash一、hash值hash2 相等,那key1和key2必定會落在數組table的同一個位置上,這就產生了衝突,這個衝突是由不一樣的key值可是卻相同的hash值引發的,稱之爲hash衝突。HashMap解決hash衝突的方式就是鏈表,雖然key1-value1和key2-value2這兩個鍵值對落在了數組table的同一個位置上,可是它們是鏈表的方式鏈接在一塊兒,當HashMap查找key1時,就須要遍歷這個鏈表了。

當負載因子越大的時候,出現hash衝突的可能性越大,這意味着數組table中某個具體的桶bucket上不止有一個元素(此時就構成了鏈表了)可能性增大,在檢索數據的時候須要遍歷鏈表的可能性增大,所以檢索的效率就比較低(耗時長),可是空間的利用率越高。

當負載因子越小的時候,出現hash衝突的可能性越小,這意味着數組table中某個具體的桶bucket上不止有一個元素(此時就構成了鏈表了)可能性減少了,在檢索數據的數據須要遍歷鏈表的可能性減少,所以檢索的效率就比較高,可是空間利用率越低。

  上面的源碼解析了提到了HashMap的容量必定得是2的整數此冪(2^n),這又是問什麼呢?

  經過上面的源碼咱們知道:根據hash值求該hash值在數組中的位置的實現是: hash & (length-1),當數組table的長度length爲2的整數次冪的時候,那(length-1)二進制表示形式從低位開始必定都是1,直到爲0,若是length依次爲2,4,8,16,32,64,則(length-1)一次爲1,3,7,15,31,63,那(length-1)的二進制形式依次爲1,11,111,1111,11111,咱們知道二進制的與運算&,當一方位數全是1的時候,進行&運算的結果徹底取決於另一方。

好比從0開始到15,依次和15進行&運算,看看結果的平均分佈狀況:

操做數0-15 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
15的二進制 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
進行位與&操做結果 0000 0001 0010  0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111

經過位與&操做後,發現0-15徹底平均的落在了數組的各個索引位置

 下面經過一個小例子予以驗證:

public class HashDemo {


    private final int[] table;

    public HashDemo(int size) {
        this.table = new int[size];
        for (int i = 0; i < size; i++) {
            table[i] = i;
        }
    }

    //求key所對應的在數組中的位置
    public int index(int key){
        //求hash值
        int hash = hash(key);
        //返回key所對應的在數組中的位置
        return hash & (table.length-1);
    }


    //HashMap中hash函數的實現,求hash值
    public int hash(int h){
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }


    public static void main(String[] args) {
        Map<String,Integer> keyToNumber = new HashMap<String, Integer>();
        int size = 16;
        HashDemo hashDemo = new HashDemo(size);
        int testSize = 1000;
        for (int i = 0; i < testSize; i++) {
            int index = hashDemo.index(i);
            Integer number = keyToNumber.get("key" + index);
            if (number == null) {
                keyToNumber.put("key"+index,1);
            }else {
                keyToNumber.put("key"+index,keyToNumber.get("key"+index)+1);
            }
        }
        Iterator<Map.Entry<String, Integer>> it = keyToNumber.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Integer> entry = it.next();
            System.out.println(entry.getKey() + "   == "+entry.getValue());
        }
    }

}

當咱們將size設置爲16 (2的4次冪)時,控制檯輸出:

key4   == 62
key3   == 62
key6   == 62
key5   == 62
key0   == 62
key2   == 62
key1   == 62
key10   == 63
key11   == 63
key8   == 63
key7   == 62
key9   == 63
key15   == 63
key14   == 63
key13   == 63
key12   == 63

 由上面的輸出可見,數據很是平均的分配在了數組的16個索引位置,

當size設置爲非2的整數次冪的時候,好比50,這時控制檯輸出:

key0   == 120
key17   == 124
key16   == 124
key1   == 120
key49   == 128
key48   == 128
key32   == 128
key33   == 128

因而可知1000個數據只分配在了8個索引位置上。

 

使用HashMap的注意事項:

  1.HashMap採用數組+鏈表的形式存儲數據,當預先知道要存儲在HashMap中數據量的大小時,可使用new HashMap(int size)來指定其容量大小,避免HashMap數組擴容致使的元素複製和rehash操做帶來的性能損耗。

  2.HashMap是基於Hash表、實現了Map接口的,查找元素的時候,先根據key計算器hash值,進而求得key在數組中的位置,可是要儘可能避免hash衝突形成的要遍歷鏈表操做,所以在咱們手動指定HashMap容量的時候,容量capacity必定得是2的整數次冪,這樣可讓數據平均的分配在數組中,減少hash衝突,提升性能。

   3.HashMap是非線程安全的,在多線程條件下不要使用HashMap,可使用ConcurrentHashMap代替。

相關文章
相關標籤/搜索