HashMap實現原理

本篇博客僅為本人了解HashMap原理過程中,查閱多篇博客后,為了加強記憶,寫下此篇,在此對多篇博客多有借鑒

HashMap概述

HashMap是基于哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,并允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恒久不變。

HashMap數據結構

HashMap中數據的存儲是由數組與鏈表一起實現的。HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組。

數組

數組是在內存中開辟一段連續的空間,因此占用內存嚴重,故空間復雜的很大。我們只要知道數組首個元素的地址,在數組中尋址就會非常容易,其時間復雜度為O(1)。但是當要插入或刪除數據時,時間復雜度就會變為O(n)。數組的特點是:尋址容易,插入和刪除困難;

鏈表

鏈表在內存的存儲區間是離散的,其插入和刪除操作的內存復雜度為O(1),但是尋址操作的復雜度卻是O(n)。鏈表的特點是:尋址困難,插入和刪除容易。

image.png

從上圖中可以看出,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組。

HashMap原理

HashMap類有一個叫做Entry的內部類。這個Entry類包含了key-value作為實例變量。 每當往hashmap里面存放key-value對的時候,都會為它們實例化一個Entry對象,這個Entry對象就會存儲在前面提到的Entry數組table中。Entry具體存在table的那個位置是 根據key的hash值來決定。

/**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
transient Entry<K,V>[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

Entry就是數組中的元素,每個 Map.Entry 其實就是一個key-value對,它持有一個指向下一個元素的引用,這就構成了鏈表。

HashMap存取實現

存儲

 /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
//HashMap允許存放null鍵值
//當key為null是,調用putForNullKey()方法,將value插入到數組的第一個位置,即角標為0的位置
        if (key == null)
            return putForNullKey(value);
//計算key的hash值
        int hash = hash(key);
//搜索hash值對應的指定數組的索引
        int i = indexFor(hash, table.length);
//如果i處的索引處Entry不為null,遍歷e元素的下一個元素
        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;
                e.recordAccess(this);
                return oldValue;
            }
        }
//如果i出索引Entry為null,表明此處沒有Entry
        modCount++;
//將key和value添加到i處索引
        addEntry(hash, key, value, i);
        return null;
    }

從上面的源代碼中可以看出:當我們往HashMap中put元素的時候,先根據key的hash值,得到這個元素在數組中的位置(即下標),然后再在該索引上的單向鏈表進行循環遍歷用equals比較key是否存在,如果存在則用新的value覆蓋原值,如果不存在,則插入鏈表的頭部,這與后面的多線程安全相關。

putForNullKey(V value)方法

/**
     * Offloaded version of put for null keys
     */
    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;
    }

addEntry(int hash, K key, V value, int bucketIndex) 方法

根據計算出的hash值,將key-value對放在數組table的i索引處。addEntry 是 HashMap 提供的一個包訪問權限的方法,代碼如下:

/**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

    /**
     * The next size value at which to resize (capacity * load factor).一般是大于0.75,開始擴容,double
     * @serial
     */
    int threshold;
/**
     * Adds a new entry with the specified key, value and hash code to
     * the specified bucket.  It is the responsibility of this
     * method to resize the table if appropriate.
     *
     * Subclass overrides this to alter the behavior of put method.
     */
    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);
    }

createEntry(int hash, K key, V value, int bucketIndex)方法

/**
     * Like addEntry except that this version is used when creating entries
     * as part of Map construction or "pseudo-construction" (cloning,
     * deserialization).  This version needn't worry about resizing the table.
     *
     * Subclass overrides this to alter the behavior of HashMap(Map),
     * clone, and readObject.
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

讀取

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
//如果key為null,調用getForNullkey()方法,如果數組0角標對應的Entry不為null,遍歷e元素的下一個元素
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

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

從上面的源代碼中可以看出:從HashMap中get元素時,首先計算key的hash值,找到數組中對應位置的某一元素,然后通過key的equals方法在對應位置的鏈表中找到需要的元素。

getForNullKey()方法

/**
     * Offloaded version of get() to look up null keys.  Null keys map
     * to index 0.  This null case is split out into separate methods
     * for the sake of performance in the two most commonly used
     * operations (get and put), but incorporated with conditionals in
     * others.
     */
    private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

getEntry(Object key)方法

/**
     * Returns the entry associated with the specified key in the
     * HashMap.  Returns null if the HashMap contains no mapping
     * for the key.
     */
    final Entry<K,V> getEntry(Object key) {
        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;
    }

總結

HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層采用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據hash值來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry時,也會根據hash值找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Entry。

山外山

HashMap的兩個重要屬性是容量capacity和裝載因子loadfactor,默認值分別為16和0.75,當容器中的元素個數大于 capacity*loadfactor = 12時,容器會進行擴容resize 為2n,在初始化Hashmap時可以對著兩個值進行修改,負載因子0.75被證明為是性能比較好的取值,通常不會修改,那么只有初始容量capacity會導致頻繁的擴容行為,這是非常耗費資源的操作,所以,如果事先能估算出容器所要存儲的元素數量,最好在初始化時修改默認容量capacity,以防止頻繁的resize操作影響性能。

java8對hashmap做了優化 ,底層有兩種實現方法,一種是數組和鏈表,一種是數組和紅黑樹,hsahmap會根據數據量選擇存儲結構
if (binCount >= TREEIFY_THRESHOLD - 1)
當符合這個條件的時候,把鏈表變成treemap,這樣查找效率從o(n)變成了o(log n)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容