基礎知識-HashMap知識點

key值爲null時的存取

    咱們知道HashMap容許插入元素的key值爲null,咱們看下這部分的源代碼:java

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

    能夠看出,key=null時,對應的數據是保存在內部數組第一個位置的鏈表中。知道了它是如何保存的,那麼獲取也就簡單了:編譯內部數組第一個位置的列表,找到key=null的數據項,返回該數據項中的value便可。算法

private V getForNullKey() {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}


hashCode和equals方法

    在上一篇博文:《基礎知識-HashMap》原理剖析中能夠知道,對於key必須實現其hashCode和equals方法,缺一不可。可是你們知道,這兩個方法默認都是從Object對象中繼承來的,下面看下Object的原生的實現方式:數組

public native int hashCode();

public boolean equals(Object obj) {
    return (this == obj);
}

    能夠看到,hashCode方法使用了native關鍵字,表示其實現調用C/C++底層的函數來實現的,而equals方法則認爲只有兩個對象的引用指向同一個對象時,才認爲它們是相等的。併發

    若是你自定義了一個類,且沒有從新覆寫equals方法和hashCode方法,而你又使用該類的對象做爲key值保存到HashMap,那麼在讀取HashMap的時候,除非你使用一個與你保存時引用徹底相同的對象做爲key值,不然你再也得不到該key所對應的value。ide

    這裏給出良好hashCode和equals的實現例子:函數

public final class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix,
                       int lineNumber) {
        this.areaCode  = (short) areaCode;
        this.prefix  = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }

    @Override 
    public boolean equals(Object o) {
        if (o == null)
            return false;
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNumber == lineNumber
            && pn.prefix  == prefix
            && pn.areaCode  == areaCode;
    }

    @Override 
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }
}

    下面給出hashCode的實現建議:
性能

    一、把某個非零的常數值,好比17,保存在一個名爲result的int類型的變量中。
    二、對於對象中每一個關鍵域f(指equals方法中涉及的每一個域),完成如下步驟:
        a、爲該域計算int類型的散列碼c:
            i、若是該域是boolean類型,則計算(f?1:0)。
            ii、若是該域是byte,char,short或者int類型,則計算(int)f。
            iii、若是該域是long類型,則計算(int)(f^(f>>>32))。
            iv、若是該域是float類型,則計算Float.floatToIntBits(f)。
            v、若是該域是double類型,則計算Double.doubleToLongBits(f),而後按照步驟2.a.iii,爲獲得的long類型值計算散列值。
            vi、若是該域是一個對象引用,而且該類的equals方法經過遞歸地調用equals的方式來比較這個域,則一樣爲這個域遞歸地調用hashCode。若是須要更復雜的比較,則爲這個域計算一個範式(canonical representation),而後針對這個範式調用hashCode。若是這個域的值爲null,則返回0(其餘常數也行)。
            vii、若是該域是一個數組,則要把每個元素當作單獨的域來處理。也就是說,遞歸地應用上述規則,對每一個重要的元素計算一個散列碼,而後根據步驟2.b中的作法把這些散列值組合起來。若是數組域中的每一個元素都很重要,能夠利用發行版本1.5中增長的其中一個Arrays.hashCode方法。
        b、按照下面的公式,把步驟2.a中計算獲得的散列碼c合併到result中:result = 31 * result + c; //此處31是個奇素數,而且有個很好的特性,即用移位和減法來代替乘法,能夠獲得更好的性能:31*i == (i<<5) - i,現代JVM能自動完成此優化。
    三、返回result
    四、檢驗並測試該hashCode實現是否符合通用約定。
    注意:在計算過程當中,冗餘項要排除在外。必須排除能夠經過其餘域值計算出來或equals比較計算中沒用的的任何域,不然有可能違反hashCode第二條約定。測試

Fast-Fail機制

    HashMap內部維護了一個實例變量modCount,該變量被聲明爲volatile,被volatile聲明的變量表示任何線程均可以看到該變量被其餘線程修改的結果。當使用迭代器(Iterator)進行迭代時,會將modCount的值賦給expectedModCount,在迭代過程當中,經過每次比較二者是否相等來判斷HashMap是否在內部或者被其餘線程修改。而HashMap中不少方法都會改變ModCount,如:put,remove,clear。
優化

    先看下HashMap內部迭代器的實現:this

private abstract class HashIterator<E> implements Iterator<E> {
    Entry<K,V> next;    // next entry to return
    int expectedModCount;    // For fast-fail
    int index;        // current slot
    Entry<K,V> current;    // current entry

    HashIterator() {
        expectedModCount = modCount;
        if (size > 0) { // advance to first entry
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Entry<K,V> e = next;
        if (e == null)
            throw new NoSuchElementException();

        if ((next = e.next) == null) {
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
        current = e;
        return e;
    }

    //...
}

    從上面的實現能夠看出,HashMap所採用的Fast-Fail機制本質上是一種樂觀鎖機制,經過檢查modCount狀態,沒有問題則忽略,有問題則拋出異常的方式,來避免線程同步的開銷。當咱們在迭代的過程當中,修改了HashMap內部的元素,致使modCount的值改變,代碼就會拋出java.util.ConcurrentModificationException。有意思的是若是HashMap只有一個元素的時候, ConcurrentModificationException 異常並不會被拋出。須要注意的就是:注意,迭代器的快速失敗行爲不能獲得保證,通常來講,存在非同步的併發修改時,不可能做出任何堅定的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。所以,編寫依賴於此異常的程序的作法是錯誤的,正確作法是:迭代器的快速失敗行爲應該僅用於檢測程序錯誤


HashMap內部數組的容量

    當調用默認的構造函數時:

 public HashMap() {
     this.loadFactor = DEFAULT_LOAD_FACTOR;
     threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
     table = new Entry[DEFAULT_INITIAL_CAPACITY]; //數組的影子
     init();
 }

    table.length=16。

    當指定初始容量和加載因子時,源碼以下:

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;
    //經過上面這個算法,找到最接近initialCapacity,且又知足2的整數次方
    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor);
    table = new Entry[capacity]; //發現實際容量爲capacity, 並不是參數initialCapacity
    init();
}

    由此看出,構造函數中指定的initialCapacity並不必定是HashMap內部維護數組的初始大小,而永遠都是2的N次方。

相關文章
相關標籤/搜索