HashMap一文通

1.什么是HashMap

  • 基于哈希表的Map接口的非同步實(shí)現(xiàn)
  • 此實(shí)現(xiàn)提供所有可選的映射操作,并允許使用null值和null鍵
  • 此類不保證映射順序
  • 此實(shí)現(xiàn)假定哈希函數(shù)將元素適當(dāng)分布在各桶之間,為讀取操作提供穩(wěn)定性能
  • 迭代時間與實(shí)例容量(桶的數(shù)量)及其大小(鍵-值映射關(guān)系數(shù))成正比
  • 本版本為JDK1.7, 此鏈接為JDK1.8版本(加班趕點(diǎn)中)

2.HashMap的數(shù)據(jù)結(jié)構(gòu)

  • 類定義
public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
  • 重要全局變量 - 鏈表散列的數(shù)據(jù)結(jié)構(gòu)(數(shù)組+鏈表【沖突解決方案-封閉尋址方法】)
備注:建議直接看英文注釋,更加清晰明了
//The default initial capacity - MUST be a power of two.
static final int DEFAULT_INITIAL_CAPACITY = 16;
//The maximum capacity - MUST be a power of two <= 1<<30.
static final int MAXIMUM_CAPACITY = 1 << 30;
//The load factor used when none specified in constructor.
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//The table, resized as necessary. Length MUST Always be a power of two.
transient Entry<K,V>[] table;
//The number of key-value mappings contained in this map.
transient int size;
//The next size value at which to resize (capacity * load factor).
int threshold;
//The load factor for the hash table.
final float loadFactor;
/**
  * The number of times this HashMap has been structurally modified
  * Structural modifications are those that change the number of mappings in
  * the HashMap or otherwise modify its internal structure (e.g.,
  * rehash).  This field is used to make iterators on Collection-views of
  * the HashMap fail-fast.  (See ConcurrentModificationException).
  */
transient int modCount;
  • 構(gòu)造器
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);
    // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
        this.loadFactor = loadFactor;
        //閾值為容量*負(fù)載因子和最大容量+1之間的最小值 以此值作為容量翻倍的依據(jù)(不能超過最大容量)
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //初始化一個2次冪的Entry類型數(shù)組 一個桶對應(yīng)一個Entry對象
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() && 
        (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
}
  • 數(shù)組內(nèi)元素的鏈表結(jié)構(gòu)
//靜態(tài)類 默認(rèn)實(shí)現(xiàn)內(nèi)部Entry接口 (接口中可定義內(nèi)部接口-Map.Entry接口為Map的內(nèi)部接口)
//PS:JDK8中引入default,作用為在接口中定義默認(rèn)方法實(shí)現(xiàn)
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;//key具有引用不可變特性
    V value;
    Entry<K,V> next;//next指向下一個:單向鏈表,頭插入
    final int hash;
    ……
}

3.HashMap的存儲

  • put方法解析
  • @return key不存在返回null,否則返回舊值
public V put(K key, V value) {
    //其允許存放null的key和null的value
    //當(dāng)其key為null時,調(diào)用putForNullKey方法,放入到table[0]的這個位置(null鍵只有一個)
    if (key == null)
        return putForNullKey(value);
    //通過調(diào)用hash方法對key進(jìn)行哈希,得到哈希之后的數(shù)值
    //其目的是為了盡可能的讓鍵值對可以分不到不同的桶中
    int hash = hash(key);
    //根據(jù)上一步驟中求出的hash得到在數(shù)組中是索引i
    int i = indexFor(hash, table.length);
    //如果i處的Entry不為null,則通過其next指針不斷遍歷e元素的下一個元素。
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;//使用臨時變量k主要用于e.key的賦值,意義有限
        //hash一致 && (key引用相同 或 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++;
    addEntry(hash, key, value, i);//新增
    return null;//若key不存在則返回null
}
  • hash方法解析
//JDK1.7
final int hash(Object k) {
    int h = 0;
    if (useAltHashing) {
        if (k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h = hashSeed;
    }
    //異或就是兩個數(shù)的二進(jìn)制形式,按位對比,相同取0,不同取一
    //此算法加入了高位計(jì)算,防止低位不變,高位變化時,造成的 hash 沖突
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
//JDK1.8 擾動函數(shù) -> 散列值優(yōu)化函數(shù)
static final int hash(Object key) {
    int h;
    //把一個數(shù)右移16位即丟棄低16為,就是任何小于2^16的數(shù),右移16后結(jié)果都為0
    //2的16次方再右移剛好就是1 同時int最大值為32位
    //任何一個數(shù),與0按位異或的結(jié)果都是這個數(shù)本身
    //為indexFor做準(zhǔn)備
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • indexFor方法解析
//@Param h 根據(jù)hash方法得到h
//@Param length 一定是2次冪
//2進(jìn)制32位帶符號的int表值范圍從-2147483648到2147483648,加起來大概40億空間,內(nèi)存不能直接讀取
//用之前還要先做對數(shù)組的長度取模運(yùn)算,得到的余數(shù)才能用來訪問數(shù)組下標(biāo)
static int indexFor(int h, int length) {
    //2次冪-1 返回的結(jié)果的二進(jìn)制為永遠(yuǎn)是都是1 比如 15 -> 1111 (16 -> 10000)
    //與運(yùn)算 只有 1 & 1 = 1 正好相當(dāng)于一個“低位掩碼”
    //如果length-1中某一位為0,則不論h中對應(yīng)位的數(shù)字為幾,對應(yīng)位結(jié)果都是0,這樣就讓兩個h取到同一個結(jié)果,hash沖突
    //同時這個操作可以保證索引不會大于數(shù)組的大小(見開頭的描述)
    return h & (length-1);
}
  • addEntry方法解析
//該方法為包訪問 package java.util(本包私有性高于子類)
void addEntry(int hash, K key, V value, int bucketIndex) {
    //當(dāng)前容量超過閾值 && 當(dāng)前坐標(biāo)數(shù)組非空
    //有個優(yōu)雅的設(shè)計(jì)在于,若bucketIndex處沒有Entry對象,那么新添加的entry對象指向null,從而就不會有鏈了
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);//容量擴(kuò)容一倍
        hash = (null != key) ? hash(key) : 0;//hash重新計(jì)算
        bucketIndex = indexFor(hash, table.length);//index重新計(jì)算
    }
    createEntry(hash, key, value, bucketIndex);//新增Entry元素到數(shù)組的制定下標(biāo)位置
}
//該方法為包訪問 package java.util
void createEntry(int hash, K key, V value, int bucketIndex) {
    // 獲取指定 bucketIndex 索引處的 Entry
    Entry<K,V> e = table[bucketIndex];
    // 將新創(chuàng)建的 Entry 放入 bucketIndex 索引處,并讓新的 Entry 指向原來的 Entry
    // 形成鏈表,新加入的放入鏈表頭部,最先加入的放入尾部
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}
  • 關(guān)于indexFor和hash方法的進(jìn)一步解讀
  • hashCode返回的-2147483648到2147483648的int值,加起來大概40億的映射空間。只要哈是函數(shù)映射比較均勻松散,一般很難出現(xiàn)碰撞key.hashCode()
  • 但問題是40億長度數(shù)組,內(nèi)存放不小,該散列值不能直接拿來用。用之前必須先對數(shù)組長度取模運(yùn)算,得到的余數(shù)才能來訪問數(shù)組下標(biāo)indexFor()
  • 長度取2的整次冪,而length-1時正好相當(dāng)于一個低位掩碼。與操作的結(jié)果就是散列的高位全部歸零,只保留低位值,用作下標(biāo)訪問
     10100101 11000100 00100101
  & 00000000 00000000 00001111
     00000000 00000000 00000101    //高位全部歸零,只保留末四位
  • 但問題是,無論散列值在松散,但只取最后幾位,碰撞也很嚴(yán)重。更要命的是如果散列本身做的不好,分布上成等差數(shù)列,就會出現(xiàn)規(guī)律性重復(fù)
  • 擾動函數(shù)生效:
    擾動函數(shù)

    右位移16位(32位一半),讓高半?yún)^(qū)和低半?yún)^(qū)做異或,目的是混合原始哈希碼的高位和低位,以此來加大低位的隨機(jī)性。而且混合后的低位包含高位的部分特征,這樣高位的信息也變相保留下來。
  • 當(dāng)長度非2次冪(最后一位永遠(yuǎn)是0),進(jìn)行與運(yùn)算(只有都為1得1,否則為0),會造成最后一位永遠(yuǎn)是0,那最后一位就無法使用,導(dǎo)致(1)空間的巨大浪費(fèi)。同時可使用的位置比原數(shù)組長度小很多,(2)進(jìn)一步增加了碰撞的幾率。
  • 歸納
  • 存儲時,根據(jù)hash算法決定其在數(shù)組中的存儲位置,再根據(jù)equals方法決定其在該數(shù)組位置上的鏈表中的存儲位置;同時HashMap會根據(jù)當(dāng)前bucket的占用情況自動調(diào)整容量(超過Load Facotr則resize為原來的2倍)
  • 讀取時,根據(jù)hash算法找到其在數(shù)組中的存儲位置,再根據(jù)equals方法從該位置上的鏈表中取出該Entry。
    在產(chǎn)生碰撞的情況下,進(jìn)行g(shù)et時,兩步的時間復(fù)雜度是O(1)+O(n)。1.8使用紅黑樹(O(1)+O(logn))

4.HashMap的Resize

  • 性能參數(shù)
  • initialCapacity 初始容量 默認(rèn)16
  • loadFactor(負(fù)載因子) : 衡量的是一個散列表的空間的使用程度,負(fù)載因子越大表示散列表的裝填程度越高,反之愈小。對于使用鏈表法的散列表來說,查找一個元素的平均時間是 O(1+a),因此如果負(fù)載因子越大,對空間的利用更充分,然而后果是查找效率的降低;如果負(fù)載因子太小,那么散列表的數(shù)據(jù)將過于稀疏,對空間造成嚴(yán)重浪費(fèi)。默認(rèn)的的負(fù)載因子 0.75是對空間和時間效率的一個平衡選擇。當(dāng)容量超出此最大容量時, resize后的HashMap 容量是容量的兩倍
  • resize方法解析
/**
  * Rehashes the contents of this map into a new array with a
  * larger capacity.  This method is called automatically when the
  * number of keys in this map reaches its threshold.
  */
//目的:通過增加內(nèi)部數(shù)組的長度的方式,從而保證鏈表中只保留很少的Entry對象,從而降低put(),remove()和get()方法的執(zhí)行時間
//注意:如果兩個Entry對象的鍵的哈希值不一樣,但它們之前在同一個桶上,那么在調(diào)整以后,并不能保證它們依然在同一個桶上
void resize(int newCapacity) {
    Entry[] oldTable = table;//使用臨時拷貝,保證當(dāng)前數(shù)據(jù)時效性(參見JAVA的`觀察者`模式實(shí)現(xiàn))
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    //實(shí)例化一個newCapacity容量的新數(shù)組
    Entry[] newTable = new Entry[newCapacity];
    boolean oldAltHashing = useAltHashing;
    useAltHashing |= sun.misc.VM.isBooted() &&
            (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean rehash = oldAltHashing ^ useAltHashing;
    transfer(newTable, rehash);//遍歷舊數(shù)組對新數(shù)組賦值
    table = newTable;//引用替換
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//重新計(jì)算閾值
}
  • transfer方法解析
/**
  * Transfers all entries from current table to newTable.
  */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    //注意:如果在新表的數(shù)組索引位置相同,則鏈表元素會倒置,也就是先插入最近的
    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);//重新計(jì)算hash
            }
            int i = indexFor(e.hash, newCapacity);
            //注意:多線程環(huán)境可能由于執(zhí)行次序非有序造成next引用變更賦值出錯導(dǎo)致環(huán)形鏈接出現(xiàn),從而造成死循環(huán)
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

5.Fail-Fast機(jī)制

  • 錯誤機(jī)制
  • 當(dāng)使用迭代器的過程中有其他線程修改了map,將拋出ConcurrentModificationException
  • 源碼實(shí)現(xiàn)
transient int modCount;//修改計(jì)數(shù) put、remove或clear時mount++ clear時清空
HashIterator() {
    expectedModCount = modCount;
    if (size > 0) {
    Entry[] t = table;
    while (index < t.length && (next = t[index++]) == null)  
        ;
    }
}
final Entry<K,V> nextEntry() {
    //期望變更數(shù)量不匹配
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
  • HashMap的remove方法實(shí)現(xiàn)
public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
}
  • HashMap.KeySet的remove方法實(shí)現(xiàn)
public boolean remove(Object o) {
    return HashMap.this.removeEntryForKey(o) != null;
}
  • HashMap.HashIterator的remove方法實(shí)現(xiàn)
public void remove() {
    if (current == null)
        throw new IllegalStateException();
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    Object k = current.key;
    current = null;
    HashMap.this.removeEntryForKey(k);
    //迭代器中刪除時同步了expectedModCount值與modCount相同
    expectedModCount = modCount;
}
  • remove方法解析
/**
  * Removes and returns the entry associated with the specified key
  * in the HashMap.  Returns null if the HashMap contains no mapping
  * for this key.
  */
final Entry<K,V> removeEntryForKey(Object key) {
    int hash = (key == null) ? 0 : hash(key);
    int i = indexFor(hash, table.length);
    Entry<K,V> prev = table[i];//用于記錄該key的前一個元素(默認(rèn)先從隊(duì)首開始)
    Entry<K,V> e = prev;//從隊(duì)首開始往隊(duì)尾遍歷
    //遍歷key所在鏈表
    while (e != null) {
        Entry<K,V> next = e.next;
        Object k;
        if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
            modCount++;//remove屬于結(jié)構(gòu)性改造,modCount計(jì)數(shù)+1
            size--;//當(dāng)前Map的有效元素?cái)?shù)量-1
            if (prev == e)
                table[i] = next;//若當(dāng)前key正好位于隊(duì)首,則隊(duì)首指向next
            else 
                prev.next = next;//若當(dāng)前key不位于隊(duì)首,則該key之前的元素的next指向該key的下一個元素
            e.recordRemoval(this);//鉤子方法
            return e;
        }
        //繼續(xù)往隊(duì)尾找
        prev = e;//指向當(dāng)前循環(huán)元素的上一個元素
        e = next;//指向下一次循環(huán)元素
    }
    return e;
}
  • 迭代推薦方式
Map map = new HashMap();
Iterator iter = map.entrySet().iterator();
while (iter.hasNext()) {
 Map.Entry entry = (Map.Entry) iter.next();
 Object key = entry.getKey();
 Object val = entry.getValue();
}

6.常見面試題

1.什么時候會使用HashMap?他有什么特點(diǎn)?

  • 基于Map接口實(shí)現(xiàn)的Key-Value容器,允許null值,同時非有序,非同步。

2.你知道HashMap的工作原理嗎?

  • <i class="icon-exclamation-sign">參見歸納 </i>
  • 在Java 8中,如果一個bucket中碰撞沖突的元素超過某個限制(默認(rèn)是8),則使用紅黑樹來替換鏈表,從而提高速度

3.你知道get和put的原理嗎?equals()和hashCode()的都有什么作用?

  • 通過對key的hashCode()進(jìn)行hashing,并計(jì)算下標(biāo)( n-1 & hash),從而獲得buckets的位置。如果產(chǎn)生碰撞,則利用key.equals()方法去鏈表或樹中去查找對應(yīng)的節(jié)點(diǎn)

4.你知道hash的實(shí)現(xiàn)嗎?為什么要這樣實(shí)現(xiàn)?

  • 在Java 1.8的實(shí)現(xiàn)中,是通過hashCode()的高16位異或低16位實(shí)現(xiàn)的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質(zhì)量來考慮的,這么做可以在bucket的n比較小的時候,也能保證考慮到高低bit都參與到hash的計(jì)算中,同時不會有太大的開銷。
  • 使用hash還有一個好處就是 盡可能確保每個鏈表中的長度一致

5. 如果HashMap的大小超過了負(fù)載因子(load factor)定義的容量,怎么辦?

  • 如果超過了負(fù)載因子(默認(rèn)0.75),則會重新resize一個原來長度兩倍的HashMap,并且重新調(diào)用hash方法;同時此時很可能出現(xiàn)一系列問題:<i class="icon-exclamation-sign">參見問題6 </i>

6. 你了解重新調(diào)整HashMap大小存在什么問題嗎?

  • 1.當(dāng)數(shù)據(jù)過多時,很可能出現(xiàn)性能瓶頸(包括rehash時間)
    <i class="icon-female"> 使用HashMap時一定保證數(shù)量有限</i>
    2.多線程情況下可能產(chǎn)生條件競競爭從而造成死循環(huán)(具體表現(xiàn)在CPU接近100%)。多線程同時試著調(diào)整大小,可能導(dǎo)致存儲在鏈表中的元素的次序顛倒,因?yàn)橐苿拥叫碌腷ucket位置的時候,HashMap并不會將元素放在鏈表的尾部,而是放在頭部,這是為了避免尾部遍歷。具體死循環(huán)代碼參見transfer(newTable)
    <i class="icon-female">多線程環(huán)境下推薦使用ConcurrentHashMap </i>

7. 為什么String, Interger這樣的wrapper類適合作為鍵?

  • 1.class具有final屬性,同時重寫equals()和hashCode()
    2.hashCode變動會導(dǎo)致讀取失效
    3.final同時保證線程安全
    <i class="icon-female">對象推薦重寫equals和hashCode方法,主要用于Map存取時的對比,同時有利于減少碰撞 </i>

8.我們可以使用自定義的對象作為鍵嗎?

  • 這是前一個問題的延伸。當(dāng)然你可能使用任何對象作為鍵,只要它遵守了equals()和hashCode()方法的定義規(guī)則,并且當(dāng)對象插入到Map中之后將不會再改變了。如果這個自定義對象時不可變的,那么它已經(jīng)滿足了作為鍵的條件,因?yàn)楫?dāng)它創(chuàng)建之后就已經(jīng)不能改變了。

9.如何對HashMap進(jìn)行排序?

  • 轉(zhuǎn)換:Map -> Set -> LinkedList(存key)
  • 排序:LinkedList自行sort
  • 存儲:存入有序LinkedHashMap

10.HashMap的remove陷阱?

  • 通過Iterator方式可正確遍歷完成remove操作
  • 直接調(diào)用list的remove方法就會拋異常

10.為什么只允許通過iterator進(jìn)行remove操作?

  • HashMap和keySet的remove方法都可以通過傳遞key參數(shù)刪除任意的元素
  • 而iterator只能刪除當(dāng)前元素(current),一旦刪除的元素是iterator對象中next所正在引用的,如果沒有通過modCount、 expectedModCount的比較實(shí)現(xiàn)快速失敗拋出異常,下次循環(huán)該元素將成為current指向,此時iterator就遍歷了一個已移除的過期數(shù)據(jù)
  • 之所以推薦迭代器remove的根本原因在于只有迭代器的remove方法中實(shí)現(xiàn)了變更時于modCount的同步工作
    expectedModCount = modCount;

10.如果是遍歷過程中增加或修改數(shù)據(jù)呢?

  • 增加或修改數(shù)據(jù)只能通過Map的put方法實(shí)現(xiàn),在遍歷過程中修改數(shù)據(jù)可以,但如果增加新key就會在下次循環(huán)時拋異常,因?yàn)樵谔砑有耴ey時modCount也會自增(迭代器只實(shí)現(xiàn)了remove方法也是原因之一)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 一、基本數(shù)據(jù)類型 注釋 單行注釋:// 區(qū)域注釋:/* */ 文檔注釋:/** */ 數(shù)值 對于byte類型而言...
    龍貓小爺閱讀 4,291評論 0 16
  • 實(shí)際上,HashSet 和 HashMap 之間有很多相似之處,對于 HashSet 而言,系統(tǒng)采用 Hash 算...
    曹振華閱讀 2,527評論 1 37
  • 5.1、對于HashMap需要掌握以下幾點(diǎn) Map的創(chuàng)建:HashMap() 往Map中添加鍵值對:即put(Ob...
    rochuan閱讀 698評論 0 0
  • 巷子里的人還在憨憨大睡 我們只路過青石階上歪脖子阿婆的眼 老樹迎著春天樂此不疲 誰偷穿了孫兒的綠袍衫 陽光撒下一地...
    斑鳩y閱讀 350評論 0 1
  • 朋友說,乖乖女都喜歡壞壞的男生??赡苊恳粋€乖乖女心里,都有一顆向往自由的心吧。 高中是我人生的一大轉(zhuǎn)折,偏離了我原...
    咸魚有夢y閱讀 221評論 0 0