java容器类2:Map及HashMap深入解读
Java的編程過程中經常會和Map打交道,現在我們來一起了解一下Map的底層實現,其中的思想結構對我們平時接口設計和編程也有一定借鑒作用。(以下接口分析都是以jdk1.8源碼為參考依據)
1. Map
An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.Map提供三種訪問數據的方式: 鍵值集、數據集、數據-映射,對應下表中的標記為黃色的三個接口。public interface Map<K, V>
| 方法名 | 描述 |
| void clear() | 從此映射中移除所有映射關系(可選操作)。 |
| boolean containsKey(Object key) | 如果此映射包含指定鍵的映射關系,則返回 true。 |
| boolean containsValue(Object value) | 如果此映射將一個或多個鍵映射到指定值,則返回 true。 |
| Set<Map.Entry<K,V>> entrySet() | 返回此映射中包含的映射關系的 Set 視圖。 |
| boolean equals(Object o) | 比較指定的對象與此映射是否相等。 |
| V get(Object key) | 返回指定鍵所映射的值;如果此映射不包含該鍵的映射關系,則返回 null。 |
| int hashCode() | 返回此映射的哈希碼值。 |
| boolean isEmpty() | 如果此映射未包含鍵-值映射關系,則返回 true。 |
| Set<K> keySet() | 返回此映射中包含的鍵的 Set 視圖。 |
| V put(K key, V value) | 將指定的值與此映射中的指定鍵關聯(可選操作)。 |
| void putAll(Map<? extends K,? extends V> m) | 從指定映射中將所有映射關系復制到此映射中(可選操作)。 |
| V remove(Object key) | 如果存在一個鍵的映射關系,則將其從此映射中移除(可選操作)。 |
| int size() | 返回此映射中的鍵-值映射關系數。 |
| Collection<V> values() | 返回此映射中包含的值的 Collection 視圖。 |
在Java8中的Map有增添了一些新的接口不在上述表格之中,這里不一一列舉。
這里涉及到一個靜態內部接口:Map.Entry<K,V> ,用于存儲一個鍵值對,該接口中設置set和get鍵值和value值的接口。
所以Map中存儲數據都是以這種Entry為數據單元存儲的。
2. AbatractMap
AbstractMap中增加了兩個非常重要的成員變量:
transient Set<K> keySet;
transient Collection<V> values;
通過這兩個成員變量,我們已經知道Map是如何存儲數據的了:鍵值存入keySet中,value存入values中。(由于Map需要保證鍵值的唯一性所以選擇Set作為鍵值的存儲結構,而Value則對此沒有任何要求所以選擇Collection作為存儲結構)
AbstractMap實現了Map中的部分接口,都是通過調用接口:Set<Entry<K,V>> entrySet() 實現的,而該接口的具體實現卻留給了具體的子類。以下代碼列出了equal()方法的具體實現:
public boolean equals(Object o) {if (o == this)return true;if (!(o instanceof Map))return false;Map<?,?> m = (Map<?,?>) o;if (m.size() != size())return false;try {Iterator<Entry<K,V>> i =entrySet().
iterator();while (i.hasNext()) {Entry<K,V> e = i.next();K key = e.getKey();V value = e.getValue();if (value == null) {if (!(m.get(key)==null && m.containsKey(key)))return false;} else {if (!value.equals(m.get(key)))return false;}}} catch (ClassCastException unused) {return false;} catch (NullPointerException unused) {return false;}return true;}3. HashMap
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable除了繼承了AbstractMap中HashMap中的兩個成員變量以外,又增加了如下幾個成員變量:transient Set<Map.Entry<K,V>> entrySet;transient Node<K,V>[] table;transient int size;transient int modCount;作為table存儲的基本類型,Node類的源碼如下:
View Code
Node是HashMap的一個內部類,實現了Map.Entry接口,本質是就是一個映射(鍵值對)。
建議看HashMap源碼前了解一些散列表(HashTable)的基礎知識:http://www.cnblogs.com/NeilZhang/p/5651492.html
包括:散列函數、碰撞處理、負載因子等。
3.1 hash值計算
static final int hash(Object key) { //jdk1.8 & jdk1.7int h;// h = key.hashCode() 為第一步 取hashCode值// h ^ (h >>> 16) 為第二步 高位參與運算return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }首先獲取key值的hash值(每個類都有計算hash值的方法),然后將該hash值的高16位異或低16位即得到散列值。
3.2 hash散列函數
?????? 通過hash函數可以得到key值對應的hash值,那么如何通過該hash將key散列到hashtale中呢?下面再介紹一個函數:
對應的運算如下所示:length為table的長度(通常選擇2^n)
static int indexFor(int h, int length) { //jdk1.7的源碼,jdk1.8沒有這個方法,但是實現原理一樣的return h & (length-1); //第三步 取模運算 }這里的取模運算等于 hash%length ,然而&運算比%運算的效率更高。
3.3 碰撞算法:HashTable+鏈表+紅黑樹
當hash散列函數對不同的值散列到table的同一個位置該如何處理?何時需要擴容table的大小,分配一個更大容量的table?
下面這張網絡上流行的圖基本解釋了當發生碰撞時的處理辦法,
1、HashMap的主要存儲為HashTable
2、當散列到的位置已經有元素存在時,通過鏈表將當前元素鏈接到table中的元素后面
3、當鏈表長度太長(默認超過8)時,鏈表就轉換為紅黑樹,利用紅黑樹快速增刪改查的特點提高HashMap的性能。
紅黑樹的相關知識可以參考:算法導論 第三部分——基本數據結構——紅黑樹
3.4 hashtable的擴容
這里先列出了HashMap源碼中的幾個常量:
/*** 默認hashtable的長度 16*/static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/*** hashtable的最大長度*/static final int MAXIMUM_CAPACITY = 1 << 30;/*** hashtable的默認負載因子*/static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** 當Hashtable中鏈表長度大于該值時,將鏈表轉換成紅黑樹*/static final int TREEIFY_THRESHOLD = 8;HashMap構造函數可以傳入table的初始大小和負載因子的大小:
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);
}這里有一個很巧妙牛逼的tableSizeFor算法:返回一個大于等于且最接近 cap 的2的冪次方整數,如給定9,返回2的4次方16。它的具體實現(全部通過位運算完成):
/*** Returns a power of two size for the given target capacity.*/static final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}那么關鍵的問題,什么時候會增大table的容量呢?原來table中的Node如何重新散列到新的table中?下面圍繞這兩個問題展開:
HashMap中有個成員變量 : threshhold,當table中存放的node個數大于該值時就會調用resize()函數,給table重新分配一個2倍的容量的數組(具體可能涉及很多邊界問題),并且將原來table中的元素重新散列到擴容的新表中(個人猜想這過程應該是非常耗時的,所以為了避免HashTable不斷擴容的操作,使用者可以在構造函數的時候預先設置一個較大容量的table)。
那么這個threshhold的值時如何計算的呢?
1、構造函數的時候賦值: this.threshold = tableSizeFor(initialCapacity);
2、resize()的時候 threshold也會隨著table容量的翻倍而翻倍。
3、threshold 的初始值: DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY
這里有個疑問: 通過HashMap()和HashMap(int,int)兩種構造函數得得到的threshold值計算方法不同,前一種永遠是table.length * 0.75 第二種是通過tableSizeFor(cap)計算所得,為table.length 這時負載因子似乎失去了意義?
HashTable重新散列:
當重新分配了一個table時,需要將原來table中的Node重新散列到新的table中。源碼中針對hashtable、鏈表、紅黑樹中節點分別作了處理。
1. 如果是table中的值(next為null):直接映射到大的table中,剛看的時候沒理解為什么不需要判斷如果新位置已經有元素怎么辦?
這里不需要考慮大的table中該節點已經有Node了,比如和value | 1111 的元素只有一個(table中不是鏈表),那么 value | 11111 的元素也一定只有一個。(1111為擴容前table長度減1,11111位擴容后table長度減1)
在擴充HashMap的時候,不需要像JDK1.7的實現那樣
2、 如果是鏈表中的值,則重新散列后他們可能有兩種不同的值(增加了一個異或位),需要重新散列到兩個位置。
java1.8 重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,HashMap的源碼真的有太多精妙的地方了。
3、如果是紅黑樹中的節點,重新散列后的值也可能出現兩種,需要對紅黑數進行操作,重新散列(這一塊沒有具體看源碼)。
resize()函數源碼:
View Code
3.5 put方法分析
????? 介紹了上面的這么多下面分析put函數就不是那么難了:
JDK1.8HashMap的put方法源碼如下:
1 public V put(K key, V value) {2 // 對key的hashCode()做hash3 return putVal(hash(key), key, value, false, true);4 }56 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,7 boolean evict) {8 Node<K,V>[] tab; Node<K,V> p; int n, i;9 // 步驟①:tab為空則創建 10 if ((tab = table) == null || (n = tab.length) == 0) 11 n = (tab = resize()).length; 12 // 步驟②:計算index,并對null做處理 13 if ((p = tab[i = (n - 1) & hash]) == null) 14 tab[i] = newNode(hash, key, value, null); 15 else { 16 Node<K,V> e; K k; 17 // 步驟③:節點key存在,直接覆蓋value 18 if (p.hash == hash && 19 ((k = p.key) == key || (key != null && key.equals(k)))) 20 e = p; 21 // 步驟④:判斷該鏈為紅黑樹 22 else if (p instanceof TreeNode) 23 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 24 // 步驟⑤:該鏈為鏈表 25 else { 26 for (int binCount = 0; ; ++binCount) { 27 if ((e = p.next) == null) { 28 p.next = newNode(hash, key,value,null);//鏈表長度大于8轉換為紅黑樹進行處理 29 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 30 treeifyBin(tab, hash); 31 break; 32 }// key已經存在直接覆蓋value 33 if (e.hash == hash && 34 ((k = e.key) == key || (key != null && key.equals(k)))) break; 36 p = e; 37 } 38 } 39 40 if (e != null) { // existing mapping for key 41 V oldValue = e.value; 42 if (!onlyIfAbsent || oldValue == null) 43 e.value = value; 44 afterNodeAccess(e); 45 return oldValue; 46 } 47 }48 ++modCount; 49 // 步驟⑥:超過最大容量 就擴容 50 if (++size > threshold) 51 resize(); 52 afterNodeInsertion(evict); 53 return null; 54 }?
HashMap實際使用中注意點:
當HashMap的key值為自定義類型時,需要重寫它的?equals() 和?hashCode() 兩個函數才能得到期望的結果。如下例所示:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public class PhoneNumber { ????private int prefix; //區號 ????private int phoneNumber; //電話號 ? ????public PhoneNumber(int prefix, int phoneNumber) ????{ ????????this.prefix = prefix; ????????this.phoneNumber = phoneNumber; ????} ? ????@Override ????public boolean equals(Object o) ????{ ????????if(this == o) ????????{ ????????????return true; ????????} ????????if(!(o instanceof PhoneNumber)) ????????{ ????????????return false; ????????} ????????PhoneNumber pn = (PhoneNumber)o; ????????return pn.prefix == prefix && pn.phoneNumber == phoneNumber; ????} ? ????@Override ????public int hashCode() ????{ ????????int result = 17; ????????result = 31 * result + prefix; ????????result = 31 * result + phoneNumber; ????????return result; ????} } |
這里有個疑問: 為什么在put() 一個元素時,不直接調用equals() 判斷集合中是否存在相同的元素,而是先調用 hashCode() 看是否有相同hashCode() 元素再通過equal進行確認?
答: 這里是從效率的方面考慮的,一個集合中往往有大量的元素如果一個個調用equals比較必然效率很低。如果兩個元素相同他們的hashCode必然相等(反之不成立),先調用hashCode可以過濾大部分元素。
?
HashMap與ArrayMap的區別
??????? 由于HashMap在擴容時需要重建hash table 是一件比較耗時的操作,為了優化性能Androd的系統中提供了ArrayMap,當容量較小時ArrayMap的性能更優。
?????? ArrayMap使用的是數組存放key值和value值,擴容時只需要重建一個size*2的數組讓后將之前的數據拷貝進去,再新添新數據。但是ArrayMap也有缺點: 它在每次put數據時,如果這個key值map中不存在,那么都可能會涉及到數組的拷貝操作。
????? HashMap每次put、delete操作(不涉及擴容或者容量重新分配)耗時較小,但是擴容操作時較耗時。
????? ArrayMap每次put、delete操作耗時,但是擴容操作不那么耗時。
參考: http://www.cnblogs.com/NeilZhang/p/5657265.html http://www.importnew.com/20386.html ArrayMap :https://blog.csdn.net/hp910315/article/details/48634167夢想不是浮躁,而是沉淀和積累
總結
以上是生活随笔為你收集整理的java容器类2:Map及HashMap深入解读的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 德普胜诉前妻艾梅柏关键原因被曝光:“海后
- 下一篇: 微软承认Win7到Win11存在“离奇”