Java容器解析——HashMap
前言
HashMap是一個散列表,它存儲的內容是鍵值對(key-value)映射。
1 定義
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {}由HashMap定義可以看出
1) HashMap<K,V>表示支持泛型
2)繼承自AbstractMap抽象類,實現對于Map容器的操作方法。
3)實現Map接口,實現Map接口中定義的諸多方法。
4)實現Cloneable接口,
5)實現Serializable接口,保證容器的可序列化。
2 屬性值
HashMap的屬性值含義已在代碼注釋中給出。
//默認初始化容量大小,必須為2的冪的數,初始為16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量值static final int MAXIMUM_CAPACITY = 1 << 30;//默認負載因子為0.75,代表table的填充度static final float DEFAULT_LOAD_FACTOR = 0.75f;//鏈表長度閾值,HashMap采用數組+鏈表形式存儲//當鏈表長度過長影響查詢效率,因此當鏈表長度超過此值時,鏈表轉為紅黑樹形式存儲,以提升效率。static final int TREEIFY_THRESHOLD = 8;static final int UNTREEIFY_THRESHOLD = 6;//樹最小容量static final int MIN_TREEIFY_CAPACITY = 64;//存儲節點的數組transient Node<K,V>[] table;transient Set<Map.Entry<K,V>> entrySet;// 容器中鍵值對的數目transient int size;transient int modCount;//閾值,超過閾值則需要擴容int threshold;//負載因子final float loadFactor;//節點數據結構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;}}3 構造方法
1) 無參數
//采用默認值初始化public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}2)初始化容量為initialCapacity
public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}3)初始化容量為initialCapacity,負載因子為loadFactor
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);}4)使用集合初始化
public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}//遍歷集合,將集合中元素添加到this容器中final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {int s = m.size();if (s > 0) {if (table == null) { // pre-sizefloat ft = ((float)s / loadFactor) + 1.0F;int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);if (t > threshold)threshold = tableSizeFor(t);}else if (s > threshold)resize();for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {K key = e.getKey();V value = e.getValue();putVal(hash(key), key, value, false, evict);}}}4 核心方法
| get(Object key) | 根據key獲取value | O(1) |
| put(K key, V value) | 存儲鍵值對 | O(1) |
| containsKey(Object key) | 是否包含key | O(n) |
| containsValue(Object value) | 是否包含value | O(n) |
| remove(Object key) | 刪除key對應的value | O(n) |
| size() | 容器元素數目 | O(1) |
| isEmpty() | 集合是否為空 | O(1) |
| clear() | 清空集合 | O(n) |
5 put()方法
put()方法添加鍵值對。在分析put()過程中會發現HashMap中有紅黑樹的實現過程。HashMap是采用數組加鏈表的方式存儲數據的,當鏈表長度過長時影響查找效率,因此當鏈表長度超過一定閾值時,將鏈表結構轉為紅黑樹存儲,提升查找效率。
//添加鍵為key,值為valuepublic V put(K key, V value) {return putVal(hash(key), key, value, false, true);}//添加鍵值對的方法final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {//使用到的中間變量Node<K,V>[] tab; Node<K,V> p; int n, i;//如果當前數組為空,或者表長度為0,調用resize()方法重新分配容量if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 如果數組中對應的索引位置的節點p為空,即不存在沖突情況,直接將鍵值對存儲在索引為i位置。if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);//若存在沖突,則需要遍歷鏈表尋找添加位置else {Node<K,V> e; K k;//如果節點p的鍵值與待存儲節點的鍵值相同,將p節點賦給e節點//后面會對e幾點進行判斷,如果不為空,則將e節點的值賦值為value,采用替換的方式存儲新的鍵值對,保證key的不可重復。if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// 如果p節點的類型是TreeNode,,說明此時p節點所處的鏈表已經轉為紅黑樹存儲的方式。則調用紅黑樹的添加節點方法,添加新的節點 else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//p節點的鍵不重復,且當前仍采用鏈表形式存儲 ,則遍歷p節點為頭節點的鏈表else {//鏈表的遍歷,binCount記錄鏈表的長度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的作用在于將鏈表結構改為紅黑樹存儲treeifyBin(tab, hash);break;}// 在遍歷鏈表過程中發現了有相同key的節點,則采用替換方式。if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//如果e節點不為空,說明存在相同key,則替換此節點的value,并返回舊的valueif (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;//更新鍵值對數目,并判斷是否需要擴容。if (++size > threshold)resize();afterNodeInsertion(evict);return null;}//resize方法進行重新分配容量final Node<K,V>[] resize() {//獲取舊表Node<K,V>[] oldTab = table;//舊表為空oldCap=0,否則oldCap = 舊表長度int oldCap = (oldTab == null) ? 0 : oldTab.length;// 存儲舊閾值int oldThr = threshold;int newCap, newThr = 0;//舊表不為空if (oldCap > 0) {// 原數組長度大于最大容量(1073741824) 則將threshold設為Integer.MAX_VALUE=2147483647// 接近MAXIMUM_CAPACITY的兩倍if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}//沒有達到最大容量,則容量擴大二倍,同時閾值擴大二倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in threshold// 如果原來的thredshold大于0則將容量設為原來的thredshold// 在第一次帶參數初始化時候會有這種情況newCap = oldThr;else { // zero initial threshold signifies using defaults// 在默認無參數初始化會有這種情況newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {// loadFactor 哈希負載因子 默認0.75,可在初始化時傳入,16*0.75=12 可以放12個鍵值對float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}//設置新的臨界值threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})//擴容操作,創建新的容量大小的數組newTabNode<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;// 如果原來的table有數據,則將數據復制到新的table中if (oldTab != null) {// for循環遍歷舊數組for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)//如果當前節點不存在下一個節點,即此節點為存儲在數組中的節點//將節點e添加至數組索引為e.hash & (newCap - 1)的位置。newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)//如果節點e是TreeNode// 如果e節點時紅黑樹的節點,則調用TreeNode的split()方法// 由于紅黑樹的知識也是比較復雜,本篇中不做過多解釋。這里只說明樹的各類方法的作用。split()方法是拆分紅黑樹,以實現節點的重新映射。((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // 如果e節點是鏈表中的節點,則實現鏈表的復制//鏈表的復制操作,即將舊表中的含有e節點的鏈表復制到新表中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;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;}}}}}//返回擴容后的新表return newTab;}6 get()方法
get()方法根據鍵值獲取元素值.
public V get(Object key) {Node<K,V> e;//查找鍵值為key的節點,查找成功返回value值,否則返回null。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;//檢查集合不為空,將first指向第一個節點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) {// 如果節點e類型為紅黑樹的節點類型,則調用getTreeNode()方法返回節點。getTreeNode()方法是完成紅黑樹的查找操作。if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);// 如果節點e為普通類型的節點,則遍歷鏈表查找do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}//沒有查找到返回nullreturn null;}7 contains()方法
//判斷是否包含鍵keypublic boolean containsKey(Object key) {//同樣采用getNode方法進行查找,查找結果不為null則說明存在return getNode(hash(key), key) != null;}//是否包含valuepublic 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;}8 remove()方法
@Overridepublic boolean remove(Object key, Object value) {return removeNode(hash(key), key, value, true, true) != null;}//刪除節點final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;//查找節點操作if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;else if ((e = p.next) != null) {// 紅黑樹的節點查找if (p instanceof TreeNode)node = ((TreeNode<K,V>)p).getTreeNode(hash, key);else {//鏈表的節點查找do {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {//查找到的節點為紅黑樹節點,則調用紅黑樹的刪除節點方法if (node instanceof TreeNode)((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);else if (node == p)// 查找到的節點屬于數組table中的元素,則直接將tab中的元素重新賦值tab[index] = node.next;else//鏈表的節點賦值p.next = node.next;++modCount;//更新數目--size;afterNodeRemoval(node);return node;}}return null;}9 小結
HashMap采用數組+鏈表的方式存儲鍵值對,當鏈表長度超過限定閾值,則將鏈表結構調整為紅黑樹,提高查找效率。通過源碼可以看出在添加鍵值對時沒有null檢查,因此HashMap是允許null值的。
10 對比
1) HashMap允許將null作為一個entry的key或者value,而Hashtable不允許。
2) 由于HashMap非線程安全,Hashtable是線程安全的。
3)HashMap增加了紅黑樹。
總結
以上是生活随笔為你收集整理的Java容器解析——HashMap的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 汇编解析(1)-内存寻址之实模型平面模式
- 下一篇: Java基础面试题与答案