深刻理解HashMap(JDK1.8)

HashMap 是 java 中用來存儲 key-value 鍵值對的一種容器,其中的 key 和 value 都容許爲 null。其底層的數據結構爲數組 + 鏈表 + 紅黑樹,當鏈表長度達到 TREEIFY_THRESHOLD = 8 時,該鏈表會自動轉化爲紅黑樹,以提高 HashMap 的查詢、插入效率,它實現了 Map<K, V>,Cloneable,Serializable接口。java

1、初始化

三個最主要的構造函數數組

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    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;
        this.threshold = tableSizeFor(initialCapacity);
    }

若是使用默認構造函數,則 HashMap 的初始化容量爲 1 << 4 也就是 16,默認加載因子是 DEFAULT_LOAD_FACTOR = 0.75f,當 HashMap 底層的數組元素個數 > 數組容量 * 加載因子時,HashMap 將進行擴容操做,固然也能夠在初始化時給定一個 loadFactor。若是在初始化時給定 initialCapacity,則初始化容量 C 需知足:C 是 2 的冪次方且 C >= initialCapacity 且 C <= (1 << 30)。其中的 tableSizeFor 方法保證函數返回值是大於等於給定參數 initialCapacity 最小的 2 的冪次方的數值,具體爲:數據結構

static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

參考app

2、put() 操做

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

向一個 HashMap 中添加一個元素,大體流程以下:函數

  • 對 key 求 hash 值,而後計算下標
    • hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    • index = (capacity - 1) & hash;
      在多數狀況下,數組容量不會超過 2 ^ 16,因此若是直接用哈希值與數組容量進行與運算的話,哈希值的高十六位就用不到,因此進行 (h = key.hashCode()) ^ (h >>> 16) 操做,這樣不只利用上了 hash 值得高十六位,還減少了 hash 衝突的概率!
      要計算一個 hash 的下標,一般是進行取餘操做:hash % capacity。但由於 capacity 是 2 的冪次方,因此 hash % capacity = (capacity - 1) & hash,且位運算更加高效,因此採用位運算的方式計算下標,這也是爲何 HashMap 的容量要等於2的冪次方的緣由。
  • 若是沒有碰撞,直接放入數組
  • 若是發生碰撞,以鏈表的形式連接到後面(連接到鏈表最後,遍歷的同時比較當前加入的節點是否已存在)
  • 若是鏈表的長度大於或等於閾值 TREEIFY_THRESHOLD,就把鏈表轉換成紅黑樹
  • 若是節點已存在,就替換舊值
  • 若是桶(數組)滿了(++size > threshold),就須要 resize

3、HashMap 的擴容操做

  1. 容量擴充爲原來的兩倍,而後對每一個節點從新計算哈希值
  2. 這個值可能在兩個地方,一個是原下標的位置,另外一種是在下標爲<原下標+原容量>的位置

假設 capacity = 10000,則 index = 1111 & hash。擴容爲二倍後,capacity = 100000,則 index = 11111 & hash。擴容後 capacity - 1 的低四位沒有變,而僅僅是多了一個最高位,而這個最高位(從右往左第五位)相對應的 hash 值的第五位只有 0 或 1 兩種可能:若是爲 0,則 index 不變;若是爲 1,則 index = 原下標 + 原容量。post

擴容時,會新建一個哈希數組,而後將原來數組中的每一個元素移過來,這是一個很是耗時的操做。因此若是咱們預先知道了有多少個鍵值對,那麼在初始化時咱們就能夠給定一個容量,這樣就能夠減小 HashMap 擴容所帶來的消耗。例如,若是咱們知道鍵值對大概有 1000 個,那麼就能夠獲得 1000 / 0.75 ≈ 1333,比 1333 大且爲 2 的冪次方的最小數是 2048。可是若是咱們將 HashMap 的初始化容量設置爲 2048 就會可能會出現空間浪費的狀況。由於當把一個鍵值對添加到 HashMap 時,可能有不少鍵值對都會發生哈希衝突,而後他們將會以鏈表或紅黑樹的方式鏈接到哈希數組中,因此哈希數組中不爲空的元素不必定爲 1000,可能爲 200,也有多是 300。因此在給定 HashMap 初始換容量時,不只要考慮鍵值對的數量,還要考慮這些鍵值對發生哈希衝突的機率等等。this

4、HashSet

HashSet 是 java 中用來存儲不能重複無序的數據的一種容器。但在本質上,HashSet 實際上是用 HashMap 來存儲數據的。在 HashSet 的源碼中,有以下兩個成員:code

private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

map 就是用來存儲數據的容器,HashSet 將全部的數據存儲在 map 的 key 中,由於 HashMap 中的 key 是惟一的,因此也就達到了 HashSet 存儲不重複元素的目的。在向 HashSet 中添加一個元素時,其實是調用了 HashMap 的 put() 方法:接口

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

能夠看到,在添加元素時,將元素做爲 key 添加到 map 中,而 value 則放入一個 Object 常量(本質上 value 沒什麼卵用),也就是說 map 中存儲的全部鍵值對的 key 都不相同,而 value 都相同。
HashSet 中的其它方法,其實也都是直接調用了 HashMap 中的方法,例如:ci

public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }
    
    public boolean contains(Object o) {
        return map.containsKey(o);
    }
    
    public int size() {
        return map.size();
    }
相關文章
相關標籤/搜索