HashMap爲Map接口的一個實現類,實現了全部Map的操做。HashMap除了容許key和value保存null值和非線程安全外,其餘實現幾乎和HashTable一致。java
HashMap使用散列存儲的方式保存kay-value鍵值對,所以其不支持數據保存的順序。若是想要使用有序容器可使用LinkedHashMap。node
在性能上當HashMap中保存的key的哈希算法可以均勻的分佈在每一個bucket中的是時候,HashMap在基本的get和set操做的的時間複雜度都是O(n)。算法
在遍歷HashMap的時候,其遍歷節點的個數爲bucket的個數+HashMap中保存的節點個數。所以當遍歷操做比較頻繁的時候須要注意HashMap的初始化容量不該該太大。 這一點其實比較好理解:當保存的節點個數一致的時候,bucket越少,遍歷次數越少。數組
另外HashMap在resize的時候會有很大的性能消耗,所以當須要在保存HashMap中保存大量數據的時候,傳入適當的默認容量以免resize能夠很大的提升性能。 具體的resize操做請參考下面對此方法的分析安全
HashMap是非線程安全的類,看成爲共享可變資源使用的時候會出現線程安全問題。須要使用線程安全容器:數據結構
Map m = new ConcurrentHashMap();或者 Map m = Collections.synchronizedMap(new HashMap());
具體的HashMap會出現的線程安全問題分析請參考9中的分析。app
HashMap使用數組+鏈表+樹形結構的數據結構。其結構圖以下所示。函數
transient Node<K,V>[] table; //Node類型的數組,記咱們常說的bucket數組,其中每一個元素爲鏈表或者樹形結構 transient int size;//HashMap中保存的數據個數 int threshold;//HashMap須要resize操做的閾值 final float loadFactor;//負載因子,用於計算threshold。計算公式爲:threshold = loadFactor * capacity 其中還有一些默認值得屬性,有默認容量2^4,默認負載因子0.75等.用於構造函數沒有指定數值狀況下的默認值。
HashMap提供了三個不一樣的構造函數,主要區別爲是否傳入初始化容量和負載因子。分別文如下三個。源碼分析
//此構造函數建立一個空的HashMap,其中負載因子爲默認值0.75 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } //傳入默認的容量大小,創造一個指定容量大小和默認負載因子爲0.75的HashMap public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //建立一個指定容量和指定負載由於HashMap,如下代碼刪除了入參檢查 public HashMap(int initialCapacity, float loadFactor) { this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
注意:此處的initialCapacity爲數組table的大小,即bucket的個數。性能
其中在指定初始化容量的時候,會根據傳入的參數來肯定HashMap的容量大小。
初始化this.threshold的值爲入參initialCapacity距離最近的一個2的n次方的值。取值方法以下:
case initialCapacity = 0: this.threshold = 1; case initialCapacity爲非0且不爲2的n次方: this.threshold = 大於initialCapacity中第一個2的n次方的數。 case initialCapacity = 2^n: this.threshold = initialCapacity
具體的計算方法爲tableSizeFor(int cap)函數。計算方法是將入參的最高位下面的全部位都設置爲1,而後加1
下面以入參爲134217729爲例分析計算過程。
首先將int轉換爲二進制以下:
cap = 0000 1000 0000 0000 0000 0000 0000 0001
static final int tableSizeFor(int cap) { int n = cap - 1; //n =0000 1000 0000 0000 0000 0000 0000 0000 此操做爲了處理cap=2的n次方的狀況,若是不減1,計算的結果位2的n+1方,添加次操做當cap = 2^n的時候計算結果爲2^n //如下操做將n的最高位到最低位之間的各位所有設置爲1 // n = 0000 1000 0000 0000 0000 0000 0000 0000 |0000 01000 0000 0000 0000 0000 0000 000 = 0000 1100 0000 0000 0000 0000 0000 0000 //最高兩位設置爲1 n |= n >>> 1; // n = 0000 1100 0000 0000 0000 0000 0000 0000 | 0000 0011 0000 0000 0000 0000 0000 0000 = 0000 1111 0000 0000 0000 0000 0000 0000 //最高四位設置爲1 n |= n >>> 2; //n = 0000 1111 0000 0000 0000 0000 0000 0000 | 0000 0000 1111 0000 0000 0000 0000 0000 = 0000 1111 1111 0000 0000 0000 0000 0000 //最高8爲設置爲1 n |= n >>> 4; //n = 0000 1111 1111 0000 0000 0000 0000 0000 | 0000 0000 0000 1111 1111 0000 0000 0000 = 0000 1111 1111 1111 1111 0000 0000 0000 //最高16爲設置爲1 n |= n >>> 8; //n = 0000 1111 1111 1111 1111 0000 0000 0000 | 0000 0000 0000 0000 0000 1111 1111 1111 = 0000 1111 1111 1111 1111 1111 1111 1111 //不足32位,最高28位設置位1 n |= n >>> 16; //n = n + 1 = 0000 1111 1111 1111 1111 1111 1111 1111 + 1 = 0001 0000 0000 0000 0000 0000 0000 0000 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
另外此處賦值爲this.threshold,是由於構造函數的時候並不會建立table,只有實際插入數據的時候纔會建立。目的應該是爲了節省內存空間吧。
在第一次插入數據的時候,會將table的capacity設置爲threshold,同時將threshold更新爲loadFactor * capacity
HashMap在插入數據的時候傳入key-value鍵值對。使用hash尋址肯定保存數據的bucket。當第一次插入數據的時候會進行HashMap中容器的初始化。具體操做以下:
Node<K,V>[] tab; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
其中resize函數的源碼以下,主要操做爲根據cap和loadFactory建立初始化table
Node<K, V>[] oldTab = table; int oldThr = threshold; //oldThr 根據傳入的初始化cap決定 2的n次方 int newCap, newThr = 0; if (oldThr > 0) // 當構造函數中傳入了capacity的時候 newCap = oldThr; //newCap = threshold 2的n次方,即構造函數的時候的初始化容量 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } float ft = (float)newCap * loadFactor; // 2的n次方 * loadFactory newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); threshold = newThr; //新的threshold== newCap * loadFactory Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //長度爲2的n次方的數組 table = newTab;
在初始化table以後,將數據插入到指定位置,其中bucket的肯定方法爲:
i = (n - 1) & hash // 此處n-1一定爲 0000 1111 1111....的格式,取&操做以後的值必定在數組的容量範圍內。
其中hash的取值方式爲:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
具體操做以下,建立Node並將node放到table的第i個元素中
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = new Node(hash, key, value, null);
當HashMap中已有數據的時候,再次插入數據,會多出來在鏈表或者樹中尋址的操做,和當size到達閾值時候的resize操做。多出來的步驟以下:
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = new Node(hash, key, value, null); else { Node<K,V> e; K k; // hash相等,且key地址相等或者equals爲true的時候直接替換 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) // 當bucket的此節點爲樹結構的時候,在樹中插入一個節點 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //便利此bucket節點,插入到鏈表尾部 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);//當鏈表節點樹大於TREEIFY_THRESHOLD的時候,轉換爲樹形結構 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();
另外,在resize操做中也和第一次插入數據的操做不一樣,當HashMap不爲空的時候resize操做須要將以前的數據節點複製到新的table中。操做以下:
if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) //只有一個節點 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // 若是是樹形結構,拆開 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } }
在HashMap的定義中實現了Cloneable接口,Cloneable是一個標識接口,主要用來標識 Object.clone()的合法性,在沒有實現此接口的實例中調用 Object.clone()方法會拋出CloneNotSupportedException異常。能夠看到HashMap中重寫了clone方法。
HashMap實現Serializable接口主要用於支持序列化。一樣的Serializable也是一個標識接口,自己沒有定義任何方法和屬性。另外HashMap自定義了
private void writeObject(java.io.ObjectOutputStream s) throws IOException private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException
兩個方法實現了自定義序列化操做。
注意:支持序列化的類必須有無參構造函數。這點不難理解,反序列化的過程當中須要經過反射建立對象。
如下討論兩種遍歷方式,測試代碼以下:
方法一:
經過map.keySet()獲取key的集合,而後經過遍歷key的集合來遍歷map
方法二:
經過map.entrySet()方法獲取map中節點集合,而後遍歷此集合遍歷map
測試代碼以下:
public static void main(String[] args) throws Exception { Map<String, Object> map = new HashMap<>(); map.put("name", "test"); map.put("age", "25"); map.put("address", "HZ"); Set<String> keySet = map.keySet(); for (String key : keySet) { System.out.println(map.get(key)); } Set<Map.Entry<String, Object>> set = map.entrySet(); for (Map.Entry<String, Object> entry : set) { System.out.println("key is : " + entry.getKey() + ". value is " + entry.getValue()); } }
輸出以下:
HZ test 25 key is : address. value is HZ key is : name. value is test key is : age. value is 25
個人博客即將搬運同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=3c3z5rvu0g00g