1. 程式人生 > >HashMap(常用方法、底層結構、擴容機制)

HashMap(常用方法、底層結構、擴容機制)

1.實現原理:

*HashMap的底層實現是一個雜湊表即陣列+連結串列;

*HashMap初始容量大小16,擴容因子為0.75,擴容倍數為2;

HashMap本質是一個一定長度的陣列,陣列中存放的是連結串列。

當向HashMap中put(key,value)時,會首先通過hash演算法計算出存放到陣列中的位置,比如位置索引為i,將其放入到Entry[i]中,如果這個位置上面已經有元素了,那麼就將新加入的元素放在連結串列的頭上,最先加入的元素在連結串列尾。比如,第一個鍵值對A進來,通過計算其key的hash得到的index=0,記做:Entry[0] = A。一會後又進來一個鍵值對B,通過計算其index也等於0,現在怎麼辦?HashMap會這樣做:B.next = A,Entry[0] = B,如果又進來C,index也等於0,那麼C.next = B,Entry[0] = C;這樣我們發現index=0的地方其實存取了A,B,C三個鍵值對,他們通過next這個屬性連結在一起,也就是說陣列中儲存的是最後插入的元素。

HashMap的get(key)方法是:首先計算key的hashcode,找到陣列中對應位置的某一元素,然後通過key的equals方法在對應位置的連結串列中找到需要的元素。從這裡我們可以想象得到,如果每個位置上的連結串列只有一個元素,那麼hashmap的get效率將是最高的。所以我們需要讓這個hash演算法儘可能的將元素平均的放在陣列中每個位置上。

2.擴容機制:

當HashMap中的元素越來越多的時候,hash衝突的機率也就越來越高,因為陣列的長度是固定的。所以為了提高查詢的效率,就要對HashMap的陣列進行擴容。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;      // HashMap初始容量大小(16) 
static final int MAXIMUM_CAPACITY = 1 << 30;               // HashMap最大容量
transient int size;                                       // The number of key-value mappings contained in this map
 
static final float DEFAULT_LOAD_FACTOR = 0.75f;          // 負載因子
 
HashMap的容量size乘以負載因子[預設0.75] = threshold;  // threshold即為開始擴容的臨界值
 
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;    // HashMap的基本構成Entry陣列

當HashMap中的元素個數超過陣列大小(陣列總大小length,不是陣列中個數size)*loadFactor時,就會進行陣列擴容,loadFactor的預設值為0.75,這是一個折中的取值。也就是說,預設情況下,陣列大小為16,那麼當HashMap中元素個數超過16*0.75=12(這個值就是程式碼中的threshold值,也叫做臨界值)的時候,就把陣列的大小擴充套件為 2*16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置。

0.75這個值成為負載因子,那麼為什麼負載因子為0.75呢?這是通過大量實驗統計得出來的,如果過小,比如0.5,那麼當存放的元素超過一半時就進行擴容,會造成資源的浪費;如果過大,比如1,那麼當元素滿的時候才進行擴容,會使get,put操作的碰撞機率增加。 HashMap中擴容是呼叫resize()方法,方法原始碼:

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);
    table = newTable;
    //更新臨界值
    threshold = (int)(newCapacity * loadFactor);
}
//舊陣列中元素往新陣列中遷移
void transfer(Entry[] newTable) {
    //舊陣列
    Entry[] src = table;
    //新陣列長度
    int newCapacity = newTable.length;
    //遍歷舊陣列
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);//放在新陣列中的index位置
                e.next = newTable[i];//實現連結串列結構,新加入的放在鏈頭,之前的的資料放在鏈尾
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

可以看到HashMap不是無限擴容的,當達到了實現預定的MAXIMUM_CAPACITY,就不再進行擴容。

3.Hashmap為什麼大小是2的冪次? 

因為在計算元素該存放的位置的時候,用到的演算法是將元素的hashcode與當前map長度-1進行與運算。原始碼:


static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

如果map長度為2的冪次,那長度-1的二進位制一定為11111...這種形式,進行與運算就看元素的hashcode,但是如果map的長度不是2的冪次,比如為15,那長度-1就是14,二進位制為1110,無論與誰相與最後一位一定是0,0001,0011,0101,1001,1011,0111,1101這幾個位置就永遠都不能存放元素了,空間浪費相當大。也增加了新增元素是發生碰撞的機會。減慢了查詢效率。所以Hashmap的大小是2的冪次。

4.get方法實現

Hashmap get一個元素是,是計算出key的hashcode找到對應的entry,這個時間複雜度為O(1),然後通過對entry中存放的元素key進行equal比較,找出元素,這個的時間複雜度為O(m),m為entry的長度。