1. 程式人生 > >HashMap的底層實現原理

HashMap的底層實現原理

最近做的幾個專案都是用Map來儲存的資料 ,雖然用得挺順手,但是對HashMap的底層原理卻只知甚少,今天便來簡單學習和整理一下。

  資料結構中有陣列和連結串列這兩個結構來儲存資料。

  陣列儲存區間是連續的,佔用記憶體嚴重,故空間複雜的很大。但陣列的二分查詢時間複雜度小,為O(1);陣列的特點是:定址容易,插入和刪除困難;

  連結串列儲存區間離散,佔用記憶體比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N)。連結串列的特點是:定址困難,插入和刪除容易。

  綜合這兩者的優點,摒棄缺點,雜湊表就誕生了,既滿足了資料查詢方面的特點,佔用的空間也不大。

  雜湊表可以說就是陣列連結串列,底層還是陣列但是這個陣列每一項就是一個連結串列。

  在這個陣列中,每個元素儲存的其實是一個連結串列的頭,元素的儲存位置一般情況是通過hash(key)%len獲得,也就是元素的key的雜湊值對陣列長度取模得到。比如上述雜湊表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都儲存在陣列下標為12的位置。

  HashMap的建構函式

HashMap實現了Map介面,繼承AbstractMap。其中Map介面定義了鍵對映到值的規則,而AbstractMap類提供 Map 介面的骨幹實現。

HashMap提供了三個建構函式:

  HashMap():構造一個具有預設初始容量 (16) 和預設載入因子 (0.75) 的空 HashMap。

  HashMap(int initialCapacity):構造一個帶指定初始容量和預設載入因子 (0.75) 的空 HashMap。

       HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和載入因子的空 HashMap。

複製程式碼

 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都會構造一個table陣列,而table陣列的元素為Entry節點。

複製程式碼

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
}

複製程式碼

  HashMap也可以說是一個數組連結串列,HashMap裡面有一個非常重要的內部靜態類——Entry,這個Entry非常重要,它裡面包含了鍵key,值value,下一個節點next,以及hash值,Entry是HashMap非常重要的一個基礎Bean,因為所有的內容都存在Entry裡面,HashMap的本質可以理解為 Entry[ ] 陣列。

  HashMap.put(key,value)

複製程式碼

public V put(K key, V value) {
        //當key為null,呼叫putForNullKey方法,儲存null與table第一個`        位置中,這是HashMap允許為null的原因
        if (key == null)
            return putForNullKey(value);
        //計算key的hash值
        int hash = hash(key.hashCode());                  ------(1)
        //計算key hash 值在 table 陣列中的位置
        int i = indexFor(hash, table.length);             ------(2)
        //從i出開始迭代 e,找到 key 儲存的位置
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {
            Object k;
            //判斷該條鏈上是否有hash值相同的(key相同)
            //若存在相同,則直接覆蓋value,返回舊value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;    //舊值 = 新值
                e.value = value;
                e.recordAccess(this);
                return oldValue;     //返回舊值
            }
        }
        //修改次數增加1
        modCount++;
        //將key、value新增至i位置處
        addEntry(hash, key, value, i);
        return null;
    }
 

複製程式碼

  我簡單的理解一下,當執行put操作的時候,HashMap會先判斷一下要儲存內容的key值是否為null,如果為null,如果為null,則執行putForNullKey方法,這個方法的作用就是將內容儲存到Entry[]陣列的第一個位置,如果key不為null,則去計算key的hash值,然後對陣列長度取模,得到要儲存位置的下標,再迭代該陣列元素上的連結串列,看該連結串列上是否有hash值相同的,如果有hash值相同的,就直接覆蓋value的值,如果沒有hash值相同的情況,就將該內容儲存到連結串列的表頭,最先儲存的內容會放在連結串列的表尾,其實這帶程式碼也順道解釋了HashMap沒有Key值相同的情況。這裡還有一個情況也要說明一下,會不會出現連結串列過長的情況?隨著要儲存的內容越來越多,HashMap裡面的東西也越來越多,相同下標的情況也增多,那麼迭代連結串列的也無疑增加了,這會影響資料的查詢效率,HashMap對此也做了優化,當HashMap中儲存的內容超過陣列長度 *loadFactor時,陣列就會進行擴容,預設的陣列長度是16,loadFactor為載入因子,預設的值為0.75。對於擴容需要說明的一點就是,擴容是一個非常“消耗”的過程,需要重新計算資料在新陣列中的位置,並且將內容複製到新陣列中,如果我們預先知道HashMap中的元素個數,預設元素的個數,能有效的提高HashMap的儲存效率。

  HashMap.get(key)

複製程式碼

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

final Entry<K,V> getEntry(Object key) {

        if (size == 0) {

            return null;

        }

 

        int hash = (key == null) ? 0 : hash(key);

        for (Entry<K,V> e = table[indexFor(hash, table.length)];

             e != null;

             e = e.next) {

            Object k;

            if (e.hash == hash &&

                ((k = e.key) == key || (key != null && key.equals(k))))

                return e;

        }

        return null;

    }

複製程式碼

  get(key)方法的程式碼比較好理解,根據key的hash值找到對應的Entry即連結串列,然後在返回該key值對應的value。

  HashMap的遍歷

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

public static void main(String[] args) { 

Map<String, String> map = new HashMap<String, String>(); 

map.put("1""value1"); 

map.put("2""value2"); 

map.put("3""value3"); 

//第一種:普遍使用,二次取值 

System.out.println("通過Map.keySet遍歷key和value:"); 

for (String key : map.keySet()) { 

System.out.println("key= "+ key + " and value= " + map.get(key)); 

//第二種 

System.out.println("通過Map.entrySet使用iterator遍歷key和value:"); 

Iterator<Map.Entry<String, String>> it = map.entrySet().iterator(); 

while (it.hasNext()) { 

Map.Entry<String, String> entry = it.next(); 

System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue()); 

//第三種:推薦,尤其是容量大時 

System.out.println("通過Map.entrySet遍歷key和value"); 

for (Map.Entry<String, String> entry : map.entrySet()) { 

System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue()); 

//第四種 

System.out.println("通過Map.values()遍歷所有的value,但不能遍歷key"); 

for (String v : map.values()) { 

System.out.println("value= " + v); 

}   

  使用HashMap的匿名內部類Entry遍歷比使用keySet()效率要高很多,使用forEach迴圈時要注意不要在迴圈的過程中改變鍵值對的任何一方的值,否則出現雜湊表的值沒有隨著鍵值的改變而改變,到時候在刪除的時候會出現問題。 此外,entrySet比keySet快些。對於keySet其實是遍歷了2次,一次是轉為iterator,一次就從hashmap中取出key所對於的value。而entrySet只是遍歷了第一次,他把key和value都放到了entry中,所以就快了。