Java 集合學習--HashMap
一、HashMap 定義
HashMap 是一個基於散列表(哈希表)實現的鍵值對集合,每個元素都是key-value對,jdk1.8後,底層數據結構涉及到了數組、鏈表以及紅黑樹。目的進一步的優化HashMap的性能和效率。允許key和value為NULL,同樣非線程安全。
①、繼承AbstractMap抽象類,AbstractMap實現Map接口,實現部分方法的。同樣在上面HashMap的結構中,HashMap同樣實現了Map接口,這樣做是否有什麽深層次的用意呢?網上查閱資料發現,這種寫法只是一種失誤,在java集合框架中發現很多這種寫法,不用過於在乎這種失誤。
②、實現Map接口,Map類的頂層接口,Map接口定義了一組鍵值對映射通用的操作。儲存一組成對的鍵-值對象,提供key(鍵)到value(值)的映射,Map中的key不要求有序,不允許重復。value同樣不要求有序,但可以重復。
③、實現 Cloneable 接口和Serializable接口,分別支持對象的克隆與對象的序列化
二、字段屬性
//1.序列化版本標記,序列化與反序列化時發揮作用 private static final long serialVersionUID = 362498820763181265L; //2.HashMap集合默認初始化容量,1*2的4次方,即16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 //3.集合最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //4.默認的加載因子static final float DEFAULT_LOAD_FACTOR = 0.75f; //5.當桶(bucket)上的結點數大於這個值時會轉成紅黑樹(JDK1.8新增) static final int TREEIFY_THRESHOLD = 8; //6.當bucket上的節點數少於這個值得時候,數轉化成鏈表 static final int UNTREEIFY_THRESHOLD = 6; //7.當集合的容量大於這個值的時候,HashMap中桶的節點進行樹形化 static final int MIN_TREEIFY_CAPACITY = 64; //8.Node數組,Node是HashMap中自定義hash節點類。transient Node<K,V>[] table; //9.保存緩存的entrySet transient Set<Map.Entry<K,V>> entrySet; //10.集合元素個數,即HashMap中鍵值對的個數 transient int size; //11.集合修改次數 transient int modCount //12.下次擴容的臨界值,通常稱為閥值,等於capacity * load factor int threshold; //13.散列表的加載因子,可自己指定 final float loadFactor;
三、構造方法
①、默認無參構造器,設置加載因子為默認加載因子0.75
②、帶參構造器
1)指定初始化容量,使用默認加載因子
2)指定初始化容量,使用自定加載因子
四、常用方法
1.添加元素(鍵值對)
//添加鍵值對 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } ------------------------------------------------------------------------- //具體的步驟 //1.通過key計算hash值 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } //2.如果table數組為null,初始化數組,通過hash值確定鍵值對在數組中的索引位置 //3.確定元素在數組中的位置後,判斷那個位置的元素的key是否與添加的key一樣,如果一致則直接覆蓋老的鍵值對,如果不一樣則判斷結點的類型,是否是樹,如果是樹則在樹中插入一個新構造的樹結點 //4.如果數組對應位置上的節點不是樹,則是鏈表,這個時候要麽在鏈表的最後插入新的Node節點,要麽是當發現鏈表的節點個數大於8,並且集合元素個數大於64的時候,則將鏈表轉換成紅黑樹,要麽則是進行擴容。 ------------------------------------------------------------------------- //具體細節如下: /** * Implements Map.put and related methods * * @param hash hash for key 哈希值 * @param key the key 鍵 * @param value the value to pu t值 * @param onlyIfAbsent if true, don‘t change existing value * @param evict if false, the table is in creation mode. false表示table處於創建模式 * @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; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;//首次進行數組的初始化,resize主要用來進行擴容的 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; }View Code
2.查找元素(鍵值對)
①、通過 key 查找 value,首先通過key計算hash值,再根絕hash值確定key在所在數組的索引位置後,最後遍歷其後面的鏈表或者紅黑樹找到對應的value.
public V get(Object key) { Node<K,V> e; //確定key的hash值,計算方法與插入的時候一致 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; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {//數組不為空,並且在相關的位置存在Node結點。 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k))))//如果在第一個位置存在則直接返回Nnode結點 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或者 value,
//判斷集合中是否存在某個鍵,原理與查找鍵值類似 public boolean containsKey(Object key) { return getNode(hash(key), key) != null; } //遍歷整個數組,明顯效率很低 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; }
3.刪除元素(鍵值對),刪除的原理本質上是類似通過key查找value,首先通過key計算hash值,通過hash值確定key在散列表中的位置,由於key不重復,所以確定位置後,要麽是遍歷鏈表,要麽遍歷紅黑樹找到對應key所在的節點,找到node節點後刪除。
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } 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; //(n - 1) & hash找到桶的位置 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; //如果鍵的值與鏈表第一個節點相等,則將 node 指向該節點 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) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }View Code
4.遍歷集合,map的遍歷方法有多種,通常推薦的叠代器遍歷,不過遍歷的本質都是循環整個table數組,對數組的每個桶進行遍歷。
1 public class HashMapDemo { 2 3 public static void main(String[] args) { 4 5 HashMap<String, String> map = new HashMap<>(); 6 map.put("1", "a"); 7 map.put("2", "b"); 8 map.put("3", "c"); 9 //通過獲取所有的key後,再通過key獲取value的方式遍歷 10 for(String str : map.keySet()){ 11 System.out.print(map.get(str)+" "); 12 } 13 System.out.println("\n----------------"); 14 for(Entry entry : map.entrySet()){ 15 System.out.println(entry.getKey()+" "+entry.getValue()); 16 } 17 } 18 19 }
輸出結果:
a b c ---------------- 1 a 2 b 3 c
四、HashMap的hash值得秘密
1.HashMap 是數組+鏈表+紅黑樹的組合,hash值主要牽扯到它在hash表,也叫散列表中的位置。Hash表是一種根據關鍵字值(key - value)而直接進行訪問的數據結構。也就是說它通過把關鍵碼值映射到表中的一個位置來訪問記錄,它的查找速度非常快,查找一個元素可用通過key的hash值直接確定所在的位置。HashMap中確定key的值有其特定的算法,並且最終確定在散列表中的位置還需要經過&運算。具體如下:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } i = (table.length - 1) & hash;//這一步是在後面添加元素putVal()方法中進行位置的確定
主要分為三步:
①、取 hashCode 值: key.hashCode(),註意hashcode()的值是key對象在堆中的地址經過特殊的hahs算法映射後的一個int值
②、高位參與運算:h>>>16
③、取模運算:(n-1) & hash,註意:在n是偶數的時候,(n-1)&hash==hash%n;這也是為什麽HashMap的容量為2的n次方的原因,便於使用按位與運算代替取模,因為位運算的速度遠快於取模。這是一種優化。
2.為什麽要進行(h = k.hashCode()) ^ (h >>> 16)?
通過hashCode()的高16位 異或 低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麽做可以在數組table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。具體如下:
五、HashMap的擴容機制
當HashMap 集合的元素已經大於了最大承載容量threshold(capacity * loadFactor)的時候,會進行擴容,即擴大數組的長度。通常不指定集合容量的時候,初始容量為16.閥值為12.當集合的元素個數大於12的時候,集合進行擴容,擴大打原來集合的2倍大小,如下初始集合的首次擴容為32,依次是64,128。對應的閥值進行變化。依次為24,48,96。在擴容的同時,需要重新進行鍵值對的hash映射。在jdk1.8之前,擴容首先是創建一個新的大容量數組,然後依次重新計算原集合所有元素的索引,然後重新賦值。如果數組某個位置發生了hash沖突,使用的是單鏈表的頭插入方法,同一位置的新元素總是放在鏈表的頭部,這樣與原集合鏈表對比,擴容之後的可能就是倒序的鏈表了。在jdk1.8後,進行進一步的優化,使擴容的時候具體細節如下:
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { 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 newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; 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; } } } } } return newTab; }View Code
從上面的源碼可以看出,容量的增長是成倍增長,容量的大小為2的n次冪,那麽擴容的時候,元素的key的hash值要麽在原來位置不變,要麽在原來的位置再移動2次冪的位置。不需要再次進行按位與運算,進一步優化擴容的機制。僅僅需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”。
六、總結
- HashMap中兩個重要的屬性,初始容量和加載因子,這兩個屬性影響HashMap的性能,它的設值需要非常謹慎。初始容量是創建哈希表時的容量,加載因子是哈希表在其容量自動增加之前可以達到多滿的一種尺度,兩者決定何時進行擴容。
- HashMap中key和value都允許為null。
- 哈希表的容量一定要是2的整數次冪,length為2的整數次冪的話,h&(length-1)就相當於對h%length,這樣便保證了散列的均勻,使不同hash值發生碰撞的概率,同時也提升了效率。
Java 集合學習--HashMap