JAVA-HashMap1.7篇

數組:採用一定的存儲單元存儲一羣數據,對於指定下標查找,時間複雜度爲O(1),但對定值查找,則需要遍歷整個數據,一個一個的比較,時間複雜度是O(n),如果說是有序數組,可以採用二分查找法,增大查找效率,不過對於新增刪除等涉及到數組元素的移動,時間複雜度都是O(n)

鏈表:對於鏈表的新增,刪除,只需要將對應的節點引用上去就ok,時間複雜度是O(1),而查找則需要遍歷鏈表,時間複雜度爲O(N)

二叉樹:對相對平衡的有序二叉樹,查找,刪除,插入等操作,複雜度爲0(logn)

哈希表:哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希衝突的情況下,僅需一次定位即可完成,時間複雜度爲O(1)

哈希衝突:哈希表的主幹就是數組,插入一個新值的時候,會通過某個算法求出這個值的具體位置,而這個算法的優劣就直接決定哈希表的整個效率,而所謂的哈希衝突就是因爲算出來的位置可能是相等的,所以衝突,由此可見這個hash算法多麼的重要。哈希算法的設計要儘量的保證散列分佈均勻,雨漏均沾。

 HashMap1.7實現原理

主幹是一個Entry的數組 ,Entry是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;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
}

簡單來說,HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,如果定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鏈表,對於添加操作,其時間複雜度依然爲O(1),因爲最新的Entry會插入鏈表頭部,急需要簡單改變引用鏈即可,而對於查找操作來講,此時就需要遍歷鏈表,然後通過key對象的equals方法逐一比對查找。所以,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。 

基本屬性說明: 

//默認初始化化容量,即16  
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

//最大容量,即2的30次方  
static final int MAXIMUM_CAPACITY = 1 << 30;  

//默認裝載因子  
static final float DEFAULT_LOAD_FACTOR = 0.75f;  

//HashMap內部的存儲結構是一個數組,此處數組爲空,即沒有初始化之前的狀態  
static final Entry<?,?>[] EMPTY_TABLE = {};  

//空的存儲實體  
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  

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

//閾值,當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;

//默認的threshold值  
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

構造函數

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);

    this.loadFactor = loadFactor;
    threshold = initialCapacity;
    init();                              //在HashMap中沒有實現,子類LinkedHashMap實現了
}

 

從上面這段代碼我們可以看出,我們在構造HashMap的時候,並沒有馬上爲數組table分配內存空間(有一個入參爲指定Map的構造器例外),事實上是在執行第一次put操作的時候才真正構建table數組。

put操作流程:

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {             //table爲空,第一次put,創建數組
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);       //如果key爲null,存儲位置爲table[0]或table[0]的衝突鏈上
    int hash = hash(key);                  //所謂的hash算法,儘可能保證均勻分佈
    int i = indexFor(hash, table.length);
    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))) {
            V oldValue = e.value;
            e.value = value;               //如果value值相等,覆蓋
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);       //value不相等,新添加一個Entry
    return null;
}
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);
    }

    createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e); //JDK1.7版本put一個數據,是從頭部插入
    size++;
}

擴容操作:

 //按新的容量擴容Hash表  
    void resize(int newCapacity) {  
        Entry[] oldTable = table;//老的數據  
        int oldCapacity = oldTable.length;//獲取老的容量值  
        if (oldCapacity == MAXIMUM_CAPACITY) {//老的容量值已經到了最大容量值  
            threshold = Integer.MAX_VALUE;//修改擴容閥值  
            return;  
        }  
        //新的結構  
        Entry[] newTable = new Entry[newCapacity];  
        transfer(newTable, initHashSeedAsNeeded(newCapacity));//將老的表中的數據拷貝到新的結構中  
        table = newTable;//修改HashMap的底層數組  
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//修改閥值  
    } 

transfer(newTable, initHashSeedAsNeeded(newCapacity))這個方法將老數組中的數據逐個鏈表地遍歷,就不展開了,重新計算後放入新的擴容後的數組中,我們的數組索引位置的計算是通過 對key值的hashcode進行hash擾亂運算後,再通過和 length-1進行位運算得到最終數組索引位置。 

get操作,其實就是遍歷數組+鏈表,這邊就不詳述啦

重寫equals方法需同時重寫hashCode方法

如果我們已經對HashMap的原理有了一定了解,這個結果就不難理解了。儘管我們在進行get和put操作的時候,使用的key從邏輯上講是等值的(通過equals比較是相等的),但由於沒有重寫hashCode方法,所以put操作時,key(hashcode1)-->hash-->indexFor-->最終索引位置 ,而通過key取出value的時候 key(hashcode2)-->hash-->indexFor-->最終索引位置,由於hashcode1不等於hashcode2,導致沒有定位到一個數組位置而返回邏輯上錯誤的值null(也有可能碰巧定位到一個數組位置,但是也會判斷其entry的hash值是否相等,上面get方法中有提到。)

所以,在重寫equals的方法的時候,必須注意重寫hashCode方法,同時還要保證通過equals判斷相等的兩個對象,調用hashCode方法要返回同樣的整數值。而如果equals判斷不相等的兩個對象,其hashCode可以相同(只不過會發生哈希衝突,應儘量避免)

 

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