深入Collection之HashMap

深入Collection之HashMap

​ 作爲Map中最常使用的實現類HashMap,它的重要性當然毋庸置疑,所以這篇文章就是有關HashMap的實現和功能介紹。

成員變量

    //默認數組初始化容量
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    /**
     * 定義最大的數組容量,當初始化時的入參的capacity容量比這個值大時
     * 使用這個MAXIMUM_CAPACITY
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默認的負載因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * hashmap存儲的實際實現,使用Entry類的數組
     * 數組長度必須是2的指數倍
     */
    transient Entry<K,V>[] table;

    /**
     * map中包含的key-value對的數目
     */
    transient int size;

    /**
     * 數組需要擴容的閥值,當map中包含k-v數達到threshold,需要對數組擴容
     * 計算標準是數組容量 * 負載因子(capacity * load factor)
     */
    int threshold;

    /**
     * 哈希表的負載因子
     */
    final float loadFactor;

    /**
     * 哈希表結構上被改變的次數,主要爲了防止併發出錯。
     */
    transient int modCount;

    /**
     * 這個常量是爲字符串作爲key時準備的表容量的默認閥值。可替代的哈希減少了因爲字符串哈希值計算的脆弱性
     * 造成的哈希碰撞
     */
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

    /**
     * holds values which can't be initialized until after VM is booted.
     */
    private static class Holder {

            // Unsafe mechanics
        /**
         * Unsafe utilities
         */
        static final sun.misc.Unsafe UNSAFE;

        /**
         * Offset of "final" hashSeed field we must set in readObject() method.
         */
        static final long HASHSEED_OFFSET;

        /**
         * Table capacity above which to switch to use alternative hashing.
         */
        static final int ALTERNATIVE_HASHING_THRESHOLD;

        static {
            String altThreshold = java.security.AccessController.doPrivileged(
                new sun.security.action.GetPropertyAction(
                    "jdk.map.althashing.threshold"));

            int threshold;
            try {
                threshold = (null != altThreshold)
                        ? Integer.parseInt(altThreshold)
                        : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;

                // disable alternative hashing if -1
                if (threshold == -1) {
                    threshold = Integer.MAX_VALUE;
                }

                if (threshold < 0) {
                    throw new IllegalArgumentException("value must be positive integer.");
                }
            } catch(IllegalArgumentException failed) {
                throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
            }
            ALTERNATIVE_HASHING_THRESHOLD = threshold;

            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                HASHSEED_OFFSET = UNSAFE.objectFieldOffset(
                    HashMap.class.getDeclaredField("hashSeed"));
            } catch (NoSuchFieldException | SecurityException e) {
                throw new Error("Failed to record hashSeed offset", e);
            }
        }
    }

    /**
     * If {@code true} then perform alternative hashing of String keys to reduce
     * the incidence of collisions due to weak hash code calculation.
     */
    transient boolean useAltHashing;

    /**
     * A randomizing value associated with this instance that is applied to
     * hash code of keys to make hash collisions harder to find.
     */
    transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);

構造函數

    /**
    * 檢查兩個參數,設置容量爲大於initialCapacity最小的2的指數。
    * 負載因子爲入參的負載因子,閥值是計算出來的值和默認最大值中較小的一個
    * 創建大小爲capacity的Entry數組。
    * 判斷是否啓用可替代的hash值
    */
    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);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
    }

    //只需要定義初始容量,負載因子採用默認的0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    //設定初始容量爲16,初始負載因子爲1.75
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    //創建新的hashmap,將m中的值放入新的map中
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
    }

成員方法

基本方法

先介紹兩個最基本的方法:hash() 散列值優化的擾動函數和indexFor()計算在數組中的位置。

/**
* for jdk8
*/
static final int hash(Object key){
  int h;
  return (key == null)?0:(h=key.hashCode() ^ (h>>>16));
}

/**
*   h是優化後的散列值,length是table.length。又因爲長度是2的指數倍,因此length-1在二進制中爲全1.
*/
static int indexFor(int h, int length) {
        return h & (length-1);
}

​ 提起hashmap,我們都知道是通過計算key的hash值,然後將對應的value直接放入table數組中hash值對應的位置。從而達到K,V一一對應,不需要像List遍歷查找,直接通過hash值取出存放的value。所以在一個hashMap中我們應該考量兩點:1、不應該發生太多的哈希碰撞:如果模擬到最惡劣的情況,hashMap將變成鏈表。2、原型數組table不應太大:適當的數組大小應滿足適當的內存空間和合適的哈希碰撞概率。

​ 一個對象的hashcode是int型,意味着有接近40億的整型可以存放,而不必擔心過多的哈希碰撞的問題。然而,對一個數組而言,不可能設置大小爲40億。一、內存過大不足以實現,二、即使以後能夠實現,那也是浪費太多的空間資源。因此如何在小範圍內依然能體現哈希值的散列性,這就是以上兩個函數的目標。

​ 先看計算在數組中的下標:因爲table的長度都是2的指數唄,因此length-1正好可以作爲hash值的低位掩碼。

        10101010 11001100 10110010
&       00000000 00000000 00001111
-----------------------------------
        00000000 00000000 00000010

​ 如此計算出來,在長度爲16的數組中,索引爲2.

​ 那麼問題來了,對於散列值而言,就算散列函數再優秀,但是如果只取其低四位的值,發生碰撞的概率將大大提升。那怎樣才能使哈希值的散列性體現到後幾位中呢?這就是擾動函數的作用了。

h.hashCode():       1010 1110 1001 1111 0010 1001 0111 0110
h >>> 16            0000 0000 0000 0000 1010 1110 1001 1111
^
------------------------------------------------------------------
                    1010 1110 1001 1111 1000 0111 1110 1001

​ 右移16位,正好將高16位和低16位異或,最大程度的混合了高低位的信息,增強了哈希值的隨機性,並將隨機性體現到值的低位上。其目的就是爲了最大程度的減小哈希碰撞的概率。

常用成員方法

  1. public V put(K key, V value)

    public V put(K key, V value) {
           if (key == null)
               return putForNullKey(value);
           int hash = hash(key);//計算散列優化後的哈希值
           int i = indexFor(hash, table.length);//計算在數組中的索引
           for (Entry<K,V> e = table[i]; e != null; e = e.next) {//如果在數組對應索引下的單向鏈表
                                                                //中有同樣的key,替換該key的value
               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;
               }
           }
    
           modCount++;
           addEntry(hash, key, value, i);
           return null;
       }

    如果key爲null時:

    //只要有key爲null,放置在table的第一位。且存放下一個null時,會將之前的value頂替掉。
    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;
       }

    添加新的entry:

    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);
           size++;
       }
    //如果容量已經是最大值,將閥值設爲Integer.MAX_VALUE;
    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];
           boolean oldAltHashing = useAltHashing;
           useAltHashing |= sun.misc.VM.isBooted() &&
                   (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
           boolean rehash = oldAltHashing ^ useAltHashing;
           transfer(newTable, rehash);
           table = newTable;
           threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
       }
    
       /**
        * 將當前所有的entry轉移到新的table中。需要注意一點:每個entry的單向鏈表轉移後
        * 會反向,不過影響不大。因爲對鏈表遍歷重新注入相當於後進先出,而先放進新鏈表的
        * 會出現在新鏈表的頭部。
        */
       void transfer(Entry[] newTable, boolean rehash) {
           int newCapacity = newTable.length;
           for (Entry<K,V> e : table) {
               while(null != e) {
                   Entry<K,V> next = e.next;
                   if (rehash) {
                       e.hash = null == e.key ? 0 : hash(e.key);
                   }
                   int i = indexFor(e.hash, newCapacity);
                   e.next = newTable[i];
                   newTable[i] = e;
                   e = next;
               }
           }
       }

    添加entry的過程:

    1. 數組目標索引添加第一個元素:

      狀態1

    2. 數組目標索引添加第二個元素:

      狀態2

    3. 數組目標索引添加第三個索引:

      狀態3

    4. 以此類推,可以看到,在數組同一個索引下一直添加元素,會形成一個單向鏈表的數據結構。而單向鏈表的訪問離不開遍歷。因此會對直接訪問的效率影響很大。這就是爲什麼要在hashMap中避免hash碰撞的原因。

  2. public V get(Object key)

    public V get(Object key) {
           if (key == null)
               return getForNullKey();
           Entry<K,V> entry = getEntry(key);
    
           return null == entry ? null : entry.getValue();
       }
    //遍歷數組對應下標下的單向鏈表,找到目標key對應的entry
    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;
       }
  3. public V remove(Object key)

    public V remove(Object key) {
           Entry<K,V> e = removeEntryForKey(key);
           return (e == null ? null : e.value);
       }
    //整個過程就是遍歷單向鏈表刪除某一項的過程,在此不多過介紹
    final Entry<K,V> removeEntryForKey(Object key) {
           int hash = (key == null) ? 0 : hash(key);
           int i = indexFor(hash, table.length);
           Entry<K,V> prev = table[i];
           Entry<K,V> e = prev;
    
           while (e != null) {
               Entry<K,V> next = e.next;
               Object k;
               if (e.hash == hash &&
                   ((k = e.key) == key || (key != null && key.equals(k)))) {
                   modCount++;
                   size--;
                   if (prev == e)
                       table[i] = next;
                   else
                       prev.next = next;
                   e.recordRemoval(this);
                   return e;
               }
               prev = e;
               e = next;
           }
    
           return e;
       }
  4. 其他成員方法例如contain(),clear()等均是對table數組和數組索引對應的單向鏈表的遍歷。因此不再一一贅述。

HashMap的遍歷

HashMap的遍歷有針對三個方向的遍歷:key,value,entry<>。其本質都是對entry<>數組的遍歷,因此在這裏只介紹entry<>的遍歷。其他的其實也是同樣的生成模式。

private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
        public Map.Entry<K,V> next() {
            return nextEntry();
        }
    }

Iterator<Map.Entry<K,V>> newEntryIterator()   {
        return new EntryIterator();
    }

public Set<Map.Entry<K,V>> entrySet() {
        return entrySet0();
    }

    private Set<Map.Entry<K,V>> entrySet0() {
        Set<Map.Entry<K,V>> es = entrySet;
        return es != null ? es : (entrySet = new EntrySet());
    }

    private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public Iterator<Map.Entry<K,V>> iterator() {
            return newEntryIterator();
        }
        public boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<K,V> e = (Map.Entry<K,V>) o;
            Entry<K,V> candidate = getEntry(e.getKey());
            return candidate != null && candidate.equals(e);
        }
        public boolean remove(Object o) {
            return removeMapping(o) != null;
        }
        public int size() {
            return size;
        }
        public void clear() {
            HashMap.this.clear();
        }
    }

我們通常使用map.entrySet();來獲取整個map的遍歷。而看以上代碼可以看出最終會指向內部私有類EntrySet。

類中的其他方法不足爲奇,只不過是對map方法的再一次調用。而遍歷方法iterator()是我們所需要關注的遍歷方法。而EntryIterator是繼承HashIterator,所以對map的遍歷追溯到最後就是HashIterator的實現。

private abstract class HashIterator<E> implements Iterator<E> {
        Entry<K,V> next;        // next entry to return
        int expectedModCount;   // For fast-fail
        int index;              // current slot
        Entry<K,V> current;     // current entry

        //構造函數
        //初始化時就對數組依次遍歷,找到第一個不爲空的數組元素
        HashIterator() {
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        //粗顆粒的邏輯來看就是如果數組元素爲空,則一直遍歷下去知道找到不爲空的數組元素或者遍歷完整個數組
        //如果數組元素不爲空,則遍歷這個單向鏈表,遍歷完後,再找下一個不爲空的數組元素。
        final Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();
            //將next向後移,如果單向鏈表中後一位不爲空,則取鏈表後一位,如果爲空,則去數組元素下一個
            //不爲空的元素
            if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
            current = e;
            return e;
        }

        public void remove() {
            if (current == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Object k = current.key;
            current = null;
            HashMap.this.removeEntryForKey(k);
            expectedModCount = modCount;
        }
    }

hashmap遍歷

如上圖,HashMap的遍歷方向是橫向遍歷尋找非空元素,如果縱向鏈表有非空,則先遍歷完鏈表再橫向遍歷數組。

小結

通過以上對HashMap各方面的分析,我們可以看出這種數據結構的特點:

  1. 查找key的時間複雜度十分低。在哈希碰撞不太嚴重的情況下,近似於O(1)。與此同時意味着哈希碰撞十分嚴重時,時間複雜度近似於O(N)。
  2. 由於在數組中的下標是根據哈希值確定的,因此遍歷時並不會按照添加時的順序遍歷出來。
  3. 爲了避免哈希碰撞帶來的時間上的損耗,比較浪費內存空間。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章