Java 基礎 ——HashMap構造、PUT、GET

Java 基礎 ——HashMap構造、PUT、GET

  • HashMap的數據結構包括了初始數組、鏈表、紅黑樹;
  • 插入數據的時候使用key%size來進行插入數據;
  • 當兩個或者兩個以上的key的key相同,且key值不同的時候(即%【取餘】發生衝突,就會掛在數組初始化位置的鏈表後)
  • 當某個節點後出現過多的鏈表節點的時候,就會換成紅黑樹以提高效率;

HashMap結構

  • Key是通過Set組織的,即不允許重複
/**
 * Returns a {@link Set} view of the keys contained in this map.
 * The set is backed by the map, so changes to the map are
 * reflected in the set, and vice-versa.  If the map is modified
 * while an iteration over the set is in progress (except through
 * the iterator's own <tt>remove</tt> operation), the results of
 * the iteration are undefined.  The set supports element removal,
 * which removes the corresponding mapping from the map, via the
 * <tt>Iterator.remove</tt>, <tt>Set.remove</tt>,
 * <tt>removeAll</tt>, <tt>retainAll</tt>, and <tt>clear</tt>
 * operations.  It does not support the <tt>add</tt> or <tt>addAll</tt>
 * operations.
 *
 * @return a set view of the keys contained in this map
 */
public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}
  • Value是通過Collection組織的,即體現允許重複數據(ArrayList、LinkedList)
/**
 * Returns a {@link Collection} view of the values contained in this map.
 * The collection is backed by the map, so changes to the map are
 * reflected in the collection, and vice-versa.  If the map is
 * modified while an iteration over the collection is in progress
 * (except through the iterator's own <tt>remove</tt> operation),
 * the results of the iteration are undefined.  The collection
 * supports element removal, which removes the corresponding
 * mapping from the map, via the <tt>Iterator.remove</tt>,
 * <tt>Collection.remove</tt>, <tt>removeAll</tt>,
 * <tt>retainAll</tt> and <tt>clear</tt> operations.  It does not
 * support the <tt>add</tt> or <tt>addAll</tt> operations.
 *
 * @return a view of the values contained in this map
 */
public Collection<V> values() {
    Collection<V> vs = values;
    if (vs == null) {
        vs = new Values();
        values = vs;
    }
    return vs;
}

HashMap(JDK8之前)

HashMap是數組+鏈表存儲結構

  • 數組特點是查詢速度快、增刪較慢
  • 鏈表特點是查詢速度慢、增刪較快

HashMap結合兩者特點組成

如圖所示(HashMap存儲結構 JDK8之前):

在這裏插入圖片描述

  • HashMap數組長度默認是16,16個數組(桶)中每個元素存儲的就是鏈表的頭結點
/**
 * 默認初始容量,必須是2的冪次方,即桶默認是16個
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

HashMap(JDK8之後)

HashMap是數組+鏈表+紅黑樹結構

  • 數組特點是查詢速度快、增刪較慢
  • 鏈表特點是查詢速度慢、增刪較快
  • 通過常量TREEIFY_THRESHOLD來控制是否將鏈表轉換成紅黑樹來存儲

數據結構 —— 紅黑樹算法


以下對HashMap基於JDK8講解


HashMap內部結構

通過數組Node<K,V>與鏈表Set<Map.Entry<K,V>>組成的複合結構

  • 基礎結構
// 數組結構
transient Node<K,V>[] table;
// 鏈表結構
transient Set<Map.Entry<K,V>> entrySet;
  • Node<K,V>
/**
 * Node是通過hash值、鍵值對、以及指向下一個節點來組成的
 */
static class Node<K,V> implements Map.Entry<K,V> {
	// hash值
    final int hash;
    // 鍵
    final K key;
    // 值
    V value;
    // 指向下一個節點
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

數組被分爲一個個的backet(桶),通過hash值決定了鍵值對在這個數組的尋址,相同的鍵值對以鏈表形式存儲!!!

  • 鏈表的大小超過TREEIFY_THRESHOLD閾值(默認是8),會改造成紅黑樹
/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
 // TREEIFY_THRESHOLD(樹化)
static final int TREEIFY_THRESHOLD = 8;
  • 而某個桶,上面的鏈表由於刪除了某些值,低於了閾值(UNTREEIFY_THRESHOLD),紅黑樹又被轉變爲鏈表,來確保性能
/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;

HashMap構造函數

構造函數不是直接指定大小,而是給一些成員變量賦值,所以HashMap是在首次使用的時候才被初始化。

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
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);
}

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and the default load factor (0.75).
 *
 * @param  initialCapacity the initial capacity.
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 * Constructs a new <tt>HashMap</tt> with the same mappings as the
 * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
 * default load factor (0.75) and an initial capacity sufficient to
 * hold the mappings in the specified <tt>Map</tt>.
 *
 * @param   m the map whose mappings are to be placed in this map
 * @throws  NullPointerException if the specified map is null
 */
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

總結HashMap結構

  • HashMap是數組+鏈表+紅黑樹的結構
  • 默認數組爲16個桶
  • 構造函數不做大小指定,在首次使用(構造函數指定大小或者put)時對大小進行指定

具體使用數組、鏈表、紅黑樹如何轉變的通過put函數說明。


HashMap添加元素(put)

  • 源碼分析
/**
  * 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) {
     return putVal(hash(key), key, value, false, true);
 }

/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table 爲空時,調用resize()方法來初始化table
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 進行hash運算,算出鍵值對,在table中的具體位置
    if ((p = tab[i = (n - 1) & hash]) == null)
    	// 當沒有元素時,直接new該鍵值對的node放到數組當中
        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);
        // 如果不是樹化的,按照鏈表的插入方式往鏈表中添加元素,同時判斷鏈表元素的總數,一旦超過TREEIFY_THRESHOLD,則將鏈表進行樹化
        else {
            for (int binCount = 0; ; ++binCount) {
            	// 如果不是樹化的,按照鏈表的插入方式往鏈表中添加元素
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 同時判斷鏈表元素的總數(binCount),一旦超過TREEIFY_THRESHOLD,則將鏈表進行樹化(treeifyBin)
                    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;
            // 如果插入的鍵位存在於HashMap中,則對對應的鍵位進行更新操作
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 當HashMap中的size,當元素大於閾值(threshold)時,對hashMap進行擴容(resize方法)
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

resize即具備初始化,也具備擴容的功能

總結put邏輯

  • 1.若HashMap未被初始化,則進行初始化操作;
  • 2.對Key求Hash值,依據Hash值計算下標;
  • 3.若未發生碰撞,則直接放入桶中;
    "碰撞":就是計算得到相同的hash值
  • 4.若發生碰撞,則以鏈表的方式鏈接到後面;
  • 5.若鏈表長度超過閾值,且HashMap元素超過最低樹化容量,則將鏈表轉成紅黑樹;
    默認閾值:TREEIFY_THRESHOLD=8
    默認最低樹化容量:MIN_TREEIFY_CAPACITY=64
    即:當前桶的容量超過8,並且整個HashMap的元素超過64就會將鏈表轉換爲紅黑樹。如果桶容量超過8,但是整個HashMap元素沒有超過64,只會發生resize(擴容),而不會發生樹化(紅黑樹樹化)
  • 6.若節點已存在,則用新值替換舊值;
  • 7.若桶滿了(默認容量16*擴容因子0.75),就需要resize(擴容2倍後重排);

HashMap獲取元素(get)

主要使用鍵值對的hashcode,通過hash算法找到backet(桶)的位置,找到backet位置調用key.equlas(k)方法,去找到鏈表中正確的節點,最終找到要找的值並返回

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 通過hash算法找到backet(桶)的位置 (first = tab[(n - 1) & hash]) != null)
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node 找到backet位置調用key.equlas(k)方法,去找到鏈表中正確的節點
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
            	// 最終找到要找的值並返回
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

總結

  • HashMap由數組+鏈表+紅黑樹組成
  • 數組也被稱爲桶(backed),默認爲16個桶
  • 在PUT操作時,會先進行hash運算,計算數組下標是否出現“hash碰撞”
  • 如果沒有出現hash碰撞,則直接插入桶中
  • 如果出現hash碰撞,則以鏈表方式鏈接到這個桶的後邊(默認每個桶大小爲8個)
  • 當桶中鏈表長度超過閾值(8)時,且hashMap桶超過(64)個時,則發生樹化(紅黑樹樹化)
  • 若節點已經存在,則用新值替換舊值
  • 若桶滿了(默認容量16*擴容因子0.75),就需要resize(擴容2倍後重排)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章