Java源碼閱讀——HashMap
定義
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
繼承了AbstractMap抽象類,實現Map,Cloneable,Serializable接口。
HashMap 是一個散列表,它存儲的內容是鍵值對(key-value)映射。
HashMap 繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable接口。
HashMap 的實現不是同步的,這意味着它不是線程安全的。它的key、value都可以爲null。此外,HashMap中的映射不是有序的。
HashMap 的實例有兩個參數影響其性能:“初始容量”和 “加載因子”。
容量是哈希表中桶的數量,初始容量只是哈希表在創建時的容量。
加載因子是哈希表在其容量自動增加之前可以達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操作(即重建內部數據結構),從而哈希表將具有大約兩倍的桶數。
通常,默認加載因子是0.75, 這是在時間和空間成本上尋求一種折衷。加載因子過高雖然減少了空間開銷,但同時也增加了查詢成本(在大多數 HashMap 類的操作中,包括 get 和 put 操作,都反映了這一點)。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減少 rehash 操作次數。如果初始容量大於最大條目數除以加載因子,則不會發生 rehash 操作。
HashMap與Hashtable區別
1. HashTable產生於JDK 1.1,而HashMap產生於JDK 1.2。
2. 兩個類的繼承體系有些不同。雖然都實現了Map、Cloneable、Serializable三個接口,但是HashMap繼承自抽象類AbstractMap,而HashTable繼承自抽象類Dictionary。其中Dictionary類是一個已經被廢棄的類
3. HashMap是支持null鍵和null值的,而HashTable在遇到null時,會拋出NullPointerException異常。因爲HashMap在實現時對null做了特殊處理,,將null的hashCode值定爲了0,從而將其存放在哈希表的第0個bucket中。
4. HashMap/Hashtable內部用Entry數組實現哈希表,而對於映射到同一個哈希桶(數組的同一個位置)的鍵值對,使用Entry鏈表來存儲(解決hash衝突)。在這一點上實現是一樣的。但是在遍歷方式上,兩者都可以用Iterator迭代,但Hashtable仍然有Enumeration(廢棄)的方式。
5. 在Hashtable中同步是通過synchronized關鍵字實現的,而HashMap需要使用ConcurrentHashMap包裝一下。
靜態常量
主要是一些默認參數
1. static final intDEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
默認初始容量爲16
2. static final intMAXIMUM_CAPACITY = 1 << 30;
最大容量爲1左移30位,如果隱式指定較高的值,則使用該容量,必須是2的指數倍。
3. static final floatDEFAULT_LOAD_FACTOR = 0.75f;
默認加載因子0.75
4. static final int TREEIFY_THRESHOLD= 8;
一個桶中bin(箱子)的存儲方式由鏈表轉換成樹的閾值。即當桶中bin的數量超過TREEIFY_THRESHOLD時使用樹來代替鏈表。默認值是8
5. static final intUNTREEIFY_THRESHOLD = 6;
當執行resize操作時,當桶中bin的數量少於UNTREEIFY_THRESHOLD時使用鏈表來代替樹。默認值是6
6. static final intMIN_TREEIFY_CAPACITY = 64;
當桶中的bin被樹化時最小的hash表容量。(如果沒有達到這個閾值,即hash表容量小於MIN_TREEIFY_CAPACITY,當桶中bin的數量太多時會執行resize擴容操作)這個MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。
成員變量
transient Node<K,V>[] table;//
transient Set<Map.Entry<K,V>>entrySet;// 保存緩存的entrySet()。請注意,AbstractMap字段用於keySet()和values()。
transient int size;//HashMap元素數量
transient int modCount;//HashMap結構改變的次數
int threshold;// 下一個調整大小的值(容量*加載因子)
final float loadFactor;//加載因子
數據結構Node
HashMap節點的數據結構,實現了Map.Entry接口,包括一個哈希值,key,value,和下一個節點next。
static class Node<K,V> implements Map.Entry<K,V> { 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; } }
TreeNode
JDK 1.8 以前 HashMap 的實現是 數組+鏈表,即使哈希函數取得再好,也很難達到元素百分百均勻分佈。
當 HashMap 中有大量的元素都存放到同一個桶中時,這個桶下有一條長長的鏈表,這個時候HashMap 就相當於一個單鏈表,假如單鏈表有 n 個元素,遍歷的時間複雜度就是O(n),完全失去了它的優勢。
針對這種情況,JDK 1.8中引入了 紅黑樹(查找時間複雜度爲 O(logn))來優化這個問題。
當鏈表長度大於8時,將鏈表轉換爲紅黑樹。
/**
* Entry for Tree bins. ExtendsLinkedHashMap.Entry (which in turn
* extends Node) so can be used asextension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-blacktree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed tounlink next upon deletion
boolean red;
HashMap中關於紅黑樹的三個關鍵參數:
/** * 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. * 一個桶的樹化閾值 * 當桶中元素個數超過這個值時,需要使用紅黑樹節點替換鏈表節點 * 這個值必須爲 8,要不然頻繁轉換降低效率 */ static final int TREEIFY_THRESHOLD = 8; /** * 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. * 一個樹的鏈表還原閾值 * 當擴容時,桶中元素個數小於這個值,就會把樹形的桶元素 還原(切分)爲鏈表結構 * 這個值應該比上面那個小,至少爲 6,避免頻繁轉換 */ static final int UNTREEIFY_THRESHOLD = 6; /** * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts * between resizing and treeification thresholds. * 哈希表的最小樹形化容量 * 當哈希表中的容量大於這個值時,表中的桶才能進行樹形化 * 否則桶內元素太多時會擴容,而不是樹形化 * 爲了避免進行擴容、樹形化選擇的衝突,這個值不能小於 4 * TREEIFY_THRESHOLD */ static final int MIN_TREEIFY_CAPACITY = 64;
JDK 1.8 以後哈希表的添加、刪除、查找、擴容方法都增加了一種 節點爲 TreeNode 的情況:
添加時,當桶中鏈表個數超過8 時會轉換成紅黑樹;
刪除、擴容時,如果桶中結構爲紅黑樹,並且樹中元素個數太少的話,會進行修剪或者直接還原成鏈表結構;
查找時即使哈希函數不優,大量元素集中在一個桶中,由於有紅黑樹結構,性能也不會差。
參考https://tech.meituan.com/java-hashmap.html
構造方法
構造方法有四個。
HashMap()
構造一個具有默認初始容量(16) 和默認加載因子 (0.75)的空 HashMap。
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
HashMap(int initialCapacity)
構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
調用另一個構造函數HashMap(intinitialCapacity, float loadFactor),使用默認加載因子。
HashMap(int initialCapacity, float loadFactor)
構造一個帶指定初始容量和加載因子的空 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; this.threshold = tableSizeFor(initialCapacity); }
判斷初始容量合法性,小於0拋出IllegalArgumentException異常,太大直接使用MAXIMUM_CAPACITY。判斷加載因子合法性,小於0或者爲NaN(Not a Number)拋出IllegalArgumentException異常。threshold(下一次調整大小的值,爲2的冪次方倍數),tableSizeFor(initialCapacity)該方法則是計算當前initialCapacity下次的大小,比如initialCapacity=17,則返回32。
HashMap(Map<? extends K,? extends V> m)
構造一個映射關係與指定Map 相同的新 HashMap。
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
使用putMapEntries方法填充。
public方法
void clear()
此映射中移除所有映射關係。
public void clear() { Node<K,V>[] tab; modCount++; if ((tab = table) != null && size > 0) { size = 0; for (int i = 0; i < tab.length; ++i) tab[i] = null; } }
增加修改結構次數,modCount++。將表中數據table循環置爲null,size爲0;
Object clone()
返回此 HashMap 實例的淺表副本:並不複製鍵和值本身。
public Object clone() { HashMap<K,V> result; try { result = (HashMap<K,V>)super.clone(); } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } result.reinitialize(); result.putMapEntries(this, false); return result; }
淺拷貝,先置空,然後進行賦值操作。putMapEntries方法將會把當前(this)map對象的k-v元素賦值到被clone的對象中,遇到hash值相同時,鏈接到相同hash值隊列的後面。
boolean containsKey(Object key)
如果此映射包含對於指定鍵的映射關係,則返回 true。
public boolean containsKey(Object key) { return getNode(hash(key), key) != null; }
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((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; }
查找是否存在該key,返回getNode(key)是否爲null。在getNode(int hash, Object key)中,找key的hash值相同的鏈表下,是否存在key。
boolean containsValue(Object value)
如果此映射將一個或多個鍵映射到指定值,則返回 true。
public boolean containsValue(Object value) { Node<K,V>[] tab; V v; if ((tab = table) != null && size > 0) { for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) { if ((v = e.value) == value || (value != null && value.equals(v))) return true; } } } return false; }
這個方法很好的說明了HashMap中存儲的結構。遍歷整個表,表中每個元素(具有相同hash值的一條鏈)再遍歷鏈中元素(e=e.next)。
Set<Map.Entry<K,V>> entrySet()
返回此映射所包含的映射關係的Set 視圖。
public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; }
V get(Object key)
返回指定鍵所映射的值;如果對於該鍵來說,此映射不包含任何映射關係,則返回 null。
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
和上面提到的類似,通過key找value,(遍歷相同hash值的情況)。
boolean isEmpty()
如果此映射不包含鍵-值映射關係,則返回 true。
public boolean isEmpty() { return size == 0; }
Set<K> keySet()
返回此映射中所包含的鍵的Set 視圖。
public Set<K> keySet() { Set<K> ks = keySet; if (ks == null) { ks = new KeySet(); keySet = ks; } return ks; }
KeySet爲AbstractMap中定義,通過iterator遍歷獲得KeySet。
V put(K key, V value)
在此映射中關聯指定值與指定鍵。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
調用putVal方法,參數分別爲key的hash值,key,value,(是否)改變存在的值,(是否)不是新建的map。該方法實現比較複雜,但是隻要記住HashMap實現的方式,就很好理解。
void putAll(Map<? extends K,? extends V> m)
將指定映射的所有映射關係複製到此映射中,這些映射關係將替換此映射目前針對指定映射中所有鍵的所有映射關係。
public void putAll(Map<? extends K, ? extends V> m) { putMapEntries(m, true); }
在putMapEntries()中,遍歷m,調用putVal()方法。
V remove(Object key)
此映射中移除指定鍵的映射關係(如果存在)。
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
調用removeNode()方法,參數和putVal()方法類似,後面兩個參數分別爲(如果爲true,只移除和value相等的元素)和(是否移動其他元素),當然這些參數我們外部調用時並不需要關心。
int size()
返回此映射中的鍵-值映射關係數。
public int size() { return size; }
Collection<V> values()
返回此映射所包含的值的Collection 視圖。
public Collection<V> values() { Collection<V> vs = values; if (vs == null) { vs = new Values(); values = vs; } return vs; }
values和keyset一樣是在AbstractMap中定義,通過iterator遍歷獲得所有value的值。
總結
HashMap實現比list稍微複雜一些,但是理解上並不難,只要理解HashMap是如何存儲的就能知道操作是怎麼實現的。HashMap中方法沒有synchronized關鍵字,所以不是線程安全的(在Hashtable中,方法都有synchronized修飾),如果需要進行線程安全控制,可以使用java.util.concurrent包裏的ConcurrentHashMap,後續會繼續閱讀該包下的一些源碼。