java容器类2:Map及HashMap深入解读
Java的編程過程中經(jīng)常會和Map打交道,現(xiàn)在我們來一起了解一下Map的底層實(shí)現(xiàn),其中的思想結(jié)構(gòu)對我們平時(shí)接口設(shè)計(jì)和編程也有一定借鑒作用。(以下接口分析都是以jdk1.8源碼為參考依據(jù))
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提供三種訪問數(shù)據(jù)的方式: 鍵值集、數(shù)據(jù)集、數(shù)據(jù)-映射,對應(yīng)下表中的標(biāo)記為黃色的三個(gè)接口。public interface Map<K, V>
| 方法名 | 描述 |
| void clear() | 從此映射中移除所有映射關(guān)系(可選操作)。 |
| boolean containsKey(Object key) | 如果此映射包含指定鍵的映射關(guān)系,則返回 true。 |
| boolean containsValue(Object value) | 如果此映射將一個(gè)或多個(gè)鍵映射到指定值,則返回 true。 |
| Set<Map.Entry<K,V>> entrySet() | 返回此映射中包含的映射關(guān)系的 Set 視圖。 |
| boolean equals(Object o) | 比較指定的對象與此映射是否相等。 |
| V get(Object key) | 返回指定鍵所映射的值;如果此映射不包含該鍵的映射關(guān)系,則返回 null。 |
| int hashCode() | 返回此映射的哈希碼值。 |
| boolean isEmpty() | 如果此映射未包含鍵-值映射關(guān)系,則返回 true。 |
| Set<K> keySet() | 返回此映射中包含的鍵的 Set 視圖。 |
| V put(K key, V value) | 將指定的值與此映射中的指定鍵關(guān)聯(lián)(可選操作)。 |
| void putAll(Map<? extends K,? extends V> m) | 從指定映射中將所有映射關(guān)系復(fù)制到此映射中(可選操作)。 |
| V remove(Object key) | 如果存在一個(gè)鍵的映射關(guān)系,則將其從此映射中移除(可選操作)。 |
| int size() | 返回此映射中的鍵-值映射關(guān)系數(shù)。 |
| Collection<V> values() | 返回此映射中包含的值的 Collection 視圖。 |
在Java8中的Map有增添了一些新的接口不在上述表格之中,這里不一一列舉。
這里涉及到一個(gè)靜態(tài)內(nèi)部接口:Map.Entry<K,V> ,用于存儲一個(gè)鍵值對,該接口中設(shè)置set和get鍵值和value值的接口。
所以Map中存儲數(shù)據(jù)都是以這種Entry為數(shù)據(jù)單元存儲的。
2. AbatractMap
AbstractMap中增加了兩個(gè)非常重要的成員變量:
transient Set<K> keySet;
transient Collection<V> values;
通過這兩個(gè)成員變量,我們已經(jīng)知道Map是如何存儲數(shù)據(jù)的了:鍵值存入keySet中,value存入values中。(由于Map需要保證鍵值的唯一性所以選擇Set作為鍵值的存儲結(jié)構(gòu),而Value則對此沒有任何要求所以選擇Collection作為存儲結(jié)構(gòu))
AbstractMap實(shí)現(xiàn)了Map中的部分接口,都是通過調(diào)用接口:Set<Entry<K,V>> entrySet() 實(shí)現(xiàn)的,而該接口的具體實(shí)現(xiàn)卻留給了具體的子類。以下代碼列出了equal()方法的具體實(shí)現(xiàn):
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中的兩個(gè)成員變量以外,又增加了如下幾個(gè)成員變量:transient Set<Map.Entry<K,V>> entrySet;transient Node<K,V>[] table;transient int size;transient int modCount;作為table存儲的基本類型,Node類的源碼如下:
View Code
Node是HashMap的一個(gè)內(nèi)部類,實(shí)現(xiàn)了Map.Entry接口,本質(zhì)是就是一個(gè)映射(鍵值對)。
建議看HashMap源碼前了解一些散列表(HashTable)的基礎(chǔ)知識:http://www.cnblogs.com/NeilZhang/p/5651492.html
包括:散列函數(shù)、碰撞處理、負(fù)載因子等。
3.1 hash值計(jì)算
static final int hash(Object key) { //jdk1.8 & jdk1.7int h;// h = key.hashCode() 為第一步 取hashCode值// h ^ (h >>> 16) 為第二步 高位參與運(yùn)算return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }首先獲取key值的hash值(每個(gè)類都有計(jì)算hash值的方法),然后將該hash值的高16位異或低16位即得到散列值。
3.2 hash散列函數(shù)
?????? 通過hash函數(shù)可以得到key值對應(yīng)的hash值,那么如何通過該hash將key散列到hashtale中呢?下面再介紹一個(gè)函數(shù):
對應(yīng)的運(yùn)算如下所示:length為table的長度(通常選擇2^n)
static int indexFor(int h, int length) { //jdk1.7的源碼,jdk1.8沒有這個(gè)方法,但是實(shí)現(xiàn)原理一樣的return h & (length-1); //第三步 取模運(yùn)算 }這里的取模運(yùn)算等于 hash%length ,然而&運(yùn)算比%運(yùn)算的效率更高。
3.3 碰撞算法:HashTable+鏈表+紅黑樹
當(dāng)hash散列函數(shù)對不同的值散列到table的同一個(gè)位置該如何處理?何時(shí)需要擴(kuò)容table的大小,分配一個(gè)更大容量的table?
下面這張網(wǎng)絡(luò)上流行的圖基本解釋了當(dāng)發(fā)生碰撞時(shí)的處理辦法,
1、HashMap的主要存儲為HashTable
2、當(dāng)散列到的位置已經(jīng)有元素存在時(shí),通過鏈表將當(dāng)前元素鏈接到table中的元素后面
3、當(dāng)鏈表長度太長(默認(rèn)超過8)時(shí),鏈表就轉(zhuǎn)換為紅黑樹,利用紅黑樹快速增刪改查的特點(diǎn)提高HashMap的性能。
紅黑樹的相關(guān)知識可以參考:算法導(dǎo)論 第三部分——基本數(shù)據(jù)結(jié)構(gòu)——紅黑樹
3.4 hashtable的擴(kuò)容
這里先列出了HashMap源碼中的幾個(gè)常量:
/*** 默認(rèn)hashtable的長度 16*/static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/*** hashtable的最大長度*/static final int MAXIMUM_CAPACITY = 1 << 30;/*** hashtable的默認(rèn)負(fù)載因子*/static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** 當(dāng)Hashtable中鏈表長度大于該值時(shí),將鏈表轉(zhuǎn)換成紅黑樹*/static final int TREEIFY_THRESHOLD = 8;HashMap構(gòu)造函數(shù)可以傳入table的初始大小和負(fù)載因子的大小:
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);
}這里有一個(gè)很巧妙牛逼的tableSizeFor算法:返回一個(gè)大于等于且最接近 cap 的2的冪次方整數(shù),如給定9,返回2的4次方16。它的具體實(shí)現(xiàn)(全部通過位運(yùn)算完成):
/*** 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;}那么關(guān)鍵的問題,什么時(shí)候會增大table的容量呢?原來table中的Node如何重新散列到新的table中?下面圍繞這兩個(gè)問題展開:
HashMap中有個(gè)成員變量 : threshhold,當(dāng)table中存放的node個(gè)數(shù)大于該值時(shí)就會調(diào)用resize()函數(shù),給table重新分配一個(gè)2倍的容量的數(shù)組(具體可能涉及很多邊界問題),并且將原來table中的元素重新散列到擴(kuò)容的新表中(個(gè)人猜想這過程應(yīng)該是非常耗時(shí)的,所以為了避免HashTable不斷擴(kuò)容的操作,使用者可以在構(gòu)造函數(shù)的時(shí)候預(yù)先設(shè)置一個(gè)較大容量的table)。
那么這個(gè)threshhold的值時(shí)如何計(jì)算的呢?
1、構(gòu)造函數(shù)的時(shí)候賦值: this.threshold = tableSizeFor(initialCapacity);
2、resize()的時(shí)候 threshold也會隨著table容量的翻倍而翻倍。
3、threshold 的初始值: DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY
這里有個(gè)疑問: 通過HashMap()和HashMap(int,int)兩種構(gòu)造函數(shù)得得到的threshold值計(jì)算方法不同,前一種永遠(yuǎn)是table.length * 0.75 第二種是通過tableSizeFor(cap)計(jì)算所得,為table.length 這時(shí)負(fù)載因子似乎失去了意義?
HashTable重新散列:
當(dāng)重新分配了一個(gè)table時(shí),需要將原來table中的Node重新散列到新的table中。源碼中針對hashtable、鏈表、紅黑樹中節(jié)點(diǎn)分別作了處理。
1. 如果是table中的值(next為null):直接映射到大的table中,剛看的時(shí)候沒理解為什么不需要判斷如果新位置已經(jīng)有元素怎么辦?
這里不需要考慮大的table中該節(jié)點(diǎn)已經(jīng)有Node了,比如和value | 1111 的元素只有一個(gè)(table中不是鏈表),那么 value | 11111 的元素也一定只有一個(gè)。(1111為擴(kuò)容前table長度減1,11111位擴(kuò)容后table長度減1)
在擴(kuò)充HashMap的時(shí)候,不需要像JDK1.7的實(shí)現(xiàn)那樣
2、 如果是鏈表中的值,則重新散列后他們可能有兩種不同的值(增加了一個(gè)異或位),需要重新散列到兩個(gè)位置。
java1.8 重新計(jì)算hash,只需要看看原來的hash值新增的那個(gè)bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,HashMap的源碼真的有太多精妙的地方了。
3、如果是紅黑樹中的節(jié)點(diǎn),重新散列后的值也可能出現(xiàn)兩種,需要對紅黑數(shù)進(jìn)行操作,重新散列(這一塊沒有具體看源碼)。
resize()函數(shù)源碼:
View Code
3.5 put方法分析
????? 介紹了上面的這么多下面分析put函數(shù)就不是那么難了:
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為空則創(chuàng)建 10 if ((tab = table) == null || (n = tab.length) == 0) 11 n = (tab = resize()).length; 12 // 步驟②:計(jì)算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 // 步驟③:節(jié)點(diǎn)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轉(zhuǎn)換為紅黑樹進(jìn)行處理 29 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 30 treeifyBin(tab, hash); 31 break; 32 }// key已經(jīng)存在直接覆蓋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 // 步驟⑥:超過最大容量 就擴(kuò)容 50 if (++size > threshold) 51 resize(); 52 afterNodeInsertion(evict); 53 return null; 54 }?
HashMap實(shí)際使用中注意點(diǎn):
當(dāng)HashMap的key值為自定義類型時(shí),需要重寫它的?equals() 和?hashCode() 兩個(gè)函數(shù)才能得到期望的結(jié)果。如下例所示:
| 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; //區(qū)號 ????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; ????} } |
這里有個(gè)疑問: 為什么在put() 一個(gè)元素時(shí),不直接調(diào)用equals() 判斷集合中是否存在相同的元素,而是先調(diào)用 hashCode() 看是否有相同hashCode() 元素再通過equal進(jìn)行確認(rèn)?
答: 這里是從效率的方面考慮的,一個(gè)集合中往往有大量的元素如果一個(gè)個(gè)調(diào)用equals比較必然效率很低。如果兩個(gè)元素相同他們的hashCode必然相等(反之不成立),先調(diào)用hashCode可以過濾大部分元素。
?
HashMap與ArrayMap的區(qū)別
??????? 由于HashMap在擴(kuò)容時(shí)需要重建hash table 是一件比較耗時(shí)的操作,為了優(yōu)化性能Androd的系統(tǒng)中提供了ArrayMap,當(dāng)容量較小時(shí)ArrayMap的性能更優(yōu)。
?????? ArrayMap使用的是數(shù)組存放key值和value值,擴(kuò)容時(shí)只需要重建一個(gè)size*2的數(shù)組讓后將之前的數(shù)據(jù)拷貝進(jìn)去,再新添新數(shù)據(jù)。但是ArrayMap也有缺點(diǎn): 它在每次put數(shù)據(jù)時(shí),如果這個(gè)key值map中不存在,那么都可能會涉及到數(shù)組的拷貝操作。
????? HashMap每次put、delete操作(不涉及擴(kuò)容或者容量重新分配)耗時(shí)較小,但是擴(kuò)容操作時(shí)較耗時(shí)。
????? ArrayMap每次put、delete操作耗時(shí),但是擴(kuò)容操作不那么耗時(shí)。
參考: http://www.cnblogs.com/NeilZhang/p/5657265.html http://www.importnew.com/20386.html ArrayMap :https://blog.csdn.net/hp910315/article/details/48634167夢想不是浮躁,而是沉淀和積累
總結(jié)
以上是生活随笔為你收集整理的java容器类2:Map及HashMap深入解读的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 德普胜诉前妻艾梅柏关键原因被曝光:“海后
- 下一篇: SharePoint 2013 Farm