1. 程式人生 > >Java HashMap的工作原理和實現

Java HashMap的工作原理和實現

目錄

概述

HashMap的基本操作如下:

map.put("Chinese", 1);
map.put("Math", 2);
map.put("Englist", 3);
map.put("Chemistry", 4);
map.put("Biology", 5);

for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ":" + entry.getValue());
}

定義

HashMap實現了Map介面,繼承子AbstractMap。其中,Map介面定義了鍵對映到值的規則。

    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
}

建構函式

HashMap提供了三個建構函式,具體實現如下。

  • 構造一個預設具有初始容量(16)和預設載入因子(0.75)的空HashMap
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    public
HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
  • 構造一個具有預設因子(0.75)的空HashMap
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
  • 構造一個帶指定初始容量和載入因子的空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函式為空,需要有特殊需求的子類單獨實現 init(); }

通過上面的三個建構函式,我們可以看出,HashMap的建構函式完成的工作就是對loadFactor和threshold這兩個成員屬性賦值。而這兩個成員屬性的含義如下:

  • threshold: 初始容量,表示雜湊表中桶的數量。
  • loadFactor:負載因子,表示當前雜湊表的最大填滿比例。當threshold * loadFactor < 當前雜湊表中桶數目時,雜湊表的threshold需要擴大為當前的2倍。

資料結構

JAVA中HashMap是由陣列和引用實現的”連結串列雜湊”。HashMap底層實現是陣列,但是陣列的每一項都是一個連結串列,其中initialCapacity就代表了陣列的長度。HashMap初始化資料結構的程式碼如下:

    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

其中,Entry為HashMap的內部類,它包含了鍵key、值value、下一個節點next,以及hash值。這個內部類非常重要,正是由於Entry才構成table陣列的項為連結串列。

儲存實現:put(key, value)

講完了HashMap的資料結構,我們就來看一下put儲存函式的原始碼實現:

    public V put(K key, V value) {
        // 當有資料需要儲存時,才對table陣列分配記憶體
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // 當key為null時,呼叫putForNullKey方法儲存key為null的鍵值對。將該key儲存在table陣列下標為0的位置上。
        if (key == null)
            return putForNullKey(value);

        // 計算key的hash值
        int hash = hash(key);
        // 計算插入資料所在連結串列的下標,使用的方法是hash值取餘數組長度
        int i = indexFor(hash, table.length);
        // 遍歷此下標對應的連結串列,看是否存在該key值
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 判斷該條連結串列上是否有相同hash值的entry,如果有,則替換entry的value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                // 返回舊值,結束插入操作
                return oldValue;
            }
        }

        // 在下標i對應的連結串列中沒有找到key相同的Entry,則建立一個新的Entry,進行插入操作
        modCount++;
        // 使用頭插法在下標為i的連結串列中進行插入操作
        addEntry(hash, key, value, i);
        return null;
    }

讀取實現:get(key)

通過對儲存函式put方法的講解,我們很容易就能理解get方法的實現。原始碼如下:

    public V get(Object key) {
        // 若key為null,呼叫getForNullKey方法,其實就是查詢下標為0的連結串列中key為null的Entry的value
        if (key == null)
            return getForNullKey();
        // getEntry方法實現見下面的函式
        Entry<K,V> entry = getEntry(key);

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

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        // 獲取key的hash值
        int hash = (key == null) ? 0 : hash(key);
        // 根據hash值獲取索引值
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            // 若搜尋的key與查詢的key相同,則返回對應的value
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

JAVA實現HashMap

在Github上實現了一個HashMap的程式碼,還沒來得及實現擴容,歡迎指導。
自定義HashMap

面試常考問題

什麼時候會使用HashMap?他有什麼特點?

當需要儲存鍵值對時需要使用HashMap,它可以接收key為null的鍵值對,但是是非執行緒同步的。

HashMap的工作原理?

這個問題很大,其實上面講的就是HashMap的工作原理。簡單的說如下:
HashMap底層是陣列實現的,陣列的每個元素是連結串列,由Entry內部類實現。HashMap通過put方法儲存物件,通過get方法獲取物件。
儲存物件時,我們將K/V鍵值對傳給put方法,它首先呼叫hash方法計算K的hash值,取餘HashMap陣列長度後獲取該鍵值對所在連結串列的陣列下標,進一步儲存時,會適當調整陣列大小,並且採用頭插法將Entry鍵值對插入到連結串列中。
獲取物件時,我們將K傳給get方法,也是先呼叫hash方法計算hash值獲取陣列中所在連結串列的下標。然後,順序遍歷連結串列,查詢相同Entry的key的value值。

equals()和hashCode()都有什麼作用?

通過取key的hashCode()獲取初步的hash值,使用equals()方法來判斷key值是否相等。

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

如果超過了負載因子(預設0.75),則會重新resize一個原來長度兩倍的HashMap,並且重新呼叫hash方法。

HashMap和HashTable的區別

  1. HashTable是執行緒安全的,HashMap不是。所以,在不需要執行緒安全的場景下,HashMap的效率更高。
  2. HashTable不允許儲存key為null的鍵值對,HashMap可以儲存,儲存在資料下標為0的連結串列中。

參考文獻