【源碼】HashMap源碼剖析,hashmap源碼剖析
//-----------------------------------------------------------------------------------
轉載需註明出處:http://blog.csdn.net/chdjj
//-----------------------------------------------------------------------------------
註:以下源碼基於jdk1.7.0_11
之前的幾篇文章介紹了List集合中一些比較常見的類,比如ArrayList、LinkedList、Vector等。本篇文章將介紹集合架構的另一塊內容——Map集合。本文主要介紹HashMap。先回顧下雜湊表。
雜湊表定義:根據設定的hash函數和處理衝突的方式(開放定址、公用溢出區、鏈地址、重雜湊...)將一組關鍵字映射到一個有限的連續的地址集上(即bucket數組或桶數組),並以關鍵字在地址集中的“像”作為記錄在表中的儲存位置,這種表稱為hash表,這一映射過程稱為散列,所得儲存位置稱為雜湊地址或散列地址。hash表具有良好的尋找效能,在衝突機率很小的情況下,時間複雜度為O(1)。
裝載因子:loadfactor = 表中填入的記錄數/雜湊表的長度.所以loadfactor標誌著雜湊表的裝滿程度。直觀的看,裝載因子越小,發生衝突的機率越小(因為桶中還沒裝幾個資料,就需要擴容),也就是尋找效能越好,但同時浪費的空間就變大。相反,裝載因子越大,發生衝突的機率越大(等到桶快填滿時才能擴容,比如,採用鏈表法處理衝突,在此種情況下,會導致鏈表過長),尋找效能越差,同時浪費的空間會減少。後面我們會看到,
HashMap預設的裝載因子為0.75.
下面,依然是自頂向下分析,
首先看Map介面:
public interface Map<K,V> {//Map集合的頂級介面定義 // Query Operations int size(); boolean isEmpty(); boolean containsKey(Object key);//是否包含指定鍵 boolean containsValue(Object value);//是否包含指定值 V get(Object key); // Modification Operations V put(K key, V value); V remove(Object key); // Bulk Operations void putAll(Map<? extends K, ? extends V> m);//批量放置元素 void clear(); // Views //三種視圖 Set<K> keySet();//擷取鍵集 Collection<V> values();//擷取值集 Set<Map.Entry<K, V>> entrySet();//擷取索引值集合 interface Entry<K,V> {//Map的內部介面,代表一個索引值對 K getKey();//擷取鍵 V getValue(); //擷取值 V setValue(V value);//設定值 boolean equals(Object o); int hashCode(); } // Comparison and hashing boolean equals(Object o); int hashCode();}
Map介面定義了Map集合的操作規範,具體實現交由實作類別完成,其內部有個介面Entry,代表一個鍵值對.
AbstractMap是一個抽象類別,其實現了Map介面中的大部分函數。
public abstract class AbstractMap<K,V> implements Map<K,V>
下面來看幾個方法:
public boolean containsKey(Object key) { Iterator<Map.Entry<K,V>> i = entrySet().iterator();//獲得迭代器 if (key==null) {//判斷key是否為空白,分別處理 while (i.hasNext()) { Entry<K,V> e = i.next(); if (e.getKey()==null)//為空白的話使用等號判斷 return true; } } else { while (i.hasNext()) { Entry<K,V> e = i.next(); if (key.equals(e.getKey()))//不為空白的話使用equals方法判斷 return true; } } return false; } public boolean containsValue(Object value) { Iterator<Entry<K,V>> i = entrySet().iterator(); if (value==null) { while (i.hasNext()) { Entry<K,V> e = i.next(); if (e.getValue()==null) return true; } } else { while (i.hasNext()) { Entry<K,V> e = i.next(); if (value.equals(e.getValue())) return true; } } return false; }首先是containsKey和containsValue方法,需要注意的是這裡的key和value是允許為空白的,也就是說其子類預設是支援null鍵和值的。這裡的entrySet方法是個抽象方法:
public abstract Set<Entry<K,V>> entrySet();
abstractMap並沒有實現put方法,而簡單的拋出了異常,這要求子類必須複寫此方法:
public V put(K key, V value) { throw new UnsupportedOperationException(); }
但是其卻實現了get方法:
public V get(Object key) { Iterator<Entry<K,V>> i = entrySet().iterator(); if (key==null) {//依然是根據索引值是否為null做不同處理 while (i.hasNext()) { Entry<K,V> e = i.next(); if (e.getKey()==null) return e.getValue(); } } else { while (i.hasNext()) { Entry<K,V> e = i.next(); if (key.equals(e.getKey())) return e.getValue(); } } return null; }
下面來看HashMap。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
HashMap繼承了AbstractMap類,並實現了Map介面以及Cloneable、Serializable介面。其成員變數如下:
static final int DEFAULT_INITIAL_CAPACITY = 16;//預設初始容量 static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量為2的30次方 static final float DEFAULT_LOAD_FACTOR = 0.75f;//預設裝載因子為0.75 transient Entry<K,V>[] table;//桶數組,存放索引值對 transient int size;//實際儲存的索引值對個數 int threshold;//HashMap的閾值,用於判斷是否需要調整HashMap的容量(threshold = 容量*載入因子) final float loadFactor;//裝載因子 transient int modCount;//hashmap被改變的次數 static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
這裡我們得到如下資訊:
1.HashMap的預設大小為16,即桶數組的預設長度為16;2.HashMap的預設裝載因子是0.75;3.HashMap內部的桶數組儲存的是Entry對象,也就是鍵值對對象。再看構造器:
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); // Find a power of 2 >= initialCapacity //找出“大於initialCapacity”的最小的2的冪 int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //初始化桶數組 table = new Entry[capacity]; useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); init();//一個鉤子函數,預設實現是空 } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() {//使用預設的初始容量和預設的載入因子構造HashMap this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); putAllForCreate(m); } // internal utilities void init() { }
需要注意的點:
1.構造器支援指定初始容量和裝載因子,為避免數組擴容帶來的效能問題,建議根據需求指定初始容量。裝載因子盡量不要修改,0.75是個比較靠譜的值。2.實際的容量capacity一般情況下是大於我們傳進去的initialCapacity的,因為內部會通過一個迴圈去找到一個比initialCapacity大且為2的整數次冪的一個數作為實際容量。除非傳進去的數恰好為2的整數次方(capacity取2的整數次冪,是為了使不同hash值發生碰撞的機率較小,這樣就能使元素在雜湊表中均勻地散列。)。
通過前面的分析,我們知道了HashMap內部通過Entry數組儲存鍵值對,那麼這個Entry是怎麼實現的呢?接下來我們看下
Entry的實現:
static class Entry<K,V> implements Map.Entry<K,V> {//實現Map.Entry介面 final K key;//鍵,final類型,不可更改/ V value;//值 Entry<K,V> next;//HashMap通過鏈表法解決衝突,這裡的next指向鏈表的下一個元素 int hash;//hash值 /** * Creates new entry. */ //構造器需指定鏈表的下一個結點,所有衝突結點放到一個鏈表上 Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } public final K getKey() { return key; } public final V getValue() { return value; } //允許設定value public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; //保證索引值都相等 Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public final int hashCode() {//鍵為空白則hash值為0,否則通過通過hashcode計算 return (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode()); } public final String toString() { return getKey() + "=" + getValue(); } /** * This method is invoked whenever the value in an entry is * overwritten by an invocation of put(k,v) for a key k that's already * in the HashMap. */ void recordAccess(HashMap<K,V> m) { } /** * This method is invoked whenever the entry is * removed from the table. */ void recordRemoval(HashMap<K,V> m) { } }
需要注意的點:
1.HashMap內部數組儲存的是鍵值對,也就是Entry對象;2.Entry對象儲存了鍵、值,並持有一個next指標指向下一個Entry對象(HashMap通過鏈表法解決衝突);3.Entry可以通過setValue設定值,但不允許設定鍵.
下面我們研究下HashMap中比較重要的方法。從put開始:
public V put(K key, V value) {//向集合中添加一個索引值對 if (key == null)//如果鍵為空白,則調用putForNullKey return putForNullKey(value); int hash = hash(key);//否則根據key產生一個hash索引值 int i = indexFor(hash, table.length);//在根據索引值找到插入位置 //迴圈遍曆指定位置的Entry鏈表,若找到一個鍵與當前鍵完全一致的Entry,那麼覆蓋原來的鍵所對應的值,並返回原值 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; //hash值相同且鍵相同 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value;//替換原值 e.recordAccess(this); return oldValue; } } //若沒有找到這樣的鍵,則將當前索引值插入該位置,並使其位於鏈表頭部. modCount++; addEntry(hash, key, value, i); return null; }
put方法是向hashMap中添加一個鍵值對,這個方法需要注意的有:
1.允許鍵為null。put方法會針對null鍵做相應的處理,調用pullforNullKey方法:
private V putForNullKey(V value) { //空鍵,其hash值為0,必然儲存在數組的0索引位置上。//我們需要遍曆0位置的entry鏈表,如果已經有一個null鍵了,那麼也是覆蓋 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } }//若沒有,則插入 modCount++; addEntry(0, null, value, 0); return null; }
2.不允許兩個鍵相同,如果鍵相同,那麼後插入的鍵所對應的值會覆蓋之前的值。3.HashMap是通過調用hash()方法獲得鍵的hash值,並通過indexFor方法找到實際插入位置,具體代碼如下:
final int hash(Object k) {//根據鍵產生hash值 int h = 0; if (useAltHashing) { if (k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h = hashSeed; } h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }//根據hash值計算鍵在桶數組的位置 static int indexFor(int h, int length) { return h & (length-1);//由put方法可知,這個length就是數組長度,而且由構造器發現數組長度始終為2的整數次方,那麼這個&操作實際上就是是h%length的高效表示方式,可以使結果小於數組長度. }
4.put方法通過addEntry方法將鍵值插到合適位置:5.當容量超過閾值(threshold)時,會發生擴容,擴容後的數組是原數組的兩倍。
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) {//容量超過閾值 resize(2 * table.length);//數組擴容為原來的兩倍 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex];//擷取原來在該位置上的Entry對象 table[bucketIndex] = new Entry<>(hash, key, value, e);//將當前的索引值插到該位置,並作為鏈表的起始結點。其next指標指向先前的Entry size++; }
這個resize方法就是擴容方法:
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity];//建立新數組 boolean oldAltHashing = useAltHashing; useAltHashing |= sun.misc.VM.isBooted() && (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean rehash = oldAltHashing ^ useAltHashing; transfer(newTable, rehash);//將原數組中所有索引值對轉移至新數組 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) {//需遍曆每個Entry,耗時 while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } }
6.擴容操作需要開闢新數組,並對原數組中所有鍵值對重新散列,非常耗時。我們應該盡量避免HashMap擴容。
再來看get方法:
public V get(Object key) { if (key == null)//若鍵為空白 return getForNullKey(); Entry<K,V> entry = getEntry(key);//擷取Entry對象//未找到就返回null,否則返回鍵所對應的值 return null == entry ? null : entry.getValue(); }
這個getForNullKey方法就是在數組0索引位上的鏈表去尋找null鍵:
private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
而這個getEntry方法就是通過鍵產生hash值,然後得到其在數組的索引位,尋找該位置的鏈表,找到第一個滿足的鍵,並返會Entry對象:
final Entry<K,V> getEntry(Object key) { int hash = (key == null) ? 0 : hash(key); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
再看下remove方法:
public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e.value); } final Entry<K,V> removeEntryForKey(Object key) { int hash = (key == null) ? 0 : hash(key);//先計算hash值 int i = indexFor(hash, table.length);//找到索引位 Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null) {//遍曆鏈表找到該鍵,並修改鏈表相關指標指向 Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
最後看下clear方法:
public void clear() { modCount++; Entry[] tab = table; for (int i = 0; i < tab.length; i++)//遍曆數組 tab[i] = null;//置空 size = 0; }
總結:
1.HashMap的預設大小為16,即桶數組的預設長度為16;2.HashMap的預設裝載因子是0.75;3.HashMap內部的桶數組儲存的是Entry對象,也就是鍵值對對象。
4.構造器支援指定初始容量和裝載因子,為避免數組擴容帶來的效能問題,建議根據需求指定初始容量。裝載因子盡量不要修改,0.75是個比較靠譜的值。5.桶數組的長度始終是2的整數次方(大於等於指定的初始容量),這樣做可以減少衝突機率,提高尋找效率。(可以從indexfor函數中看出,h&(length-1),若length為奇數,length-1為偶數那麼h&(length-1)結果的最後一位必然為0,也就是說所有鍵都被散列到數組的偶數下標位置,這樣會浪費近一半空間。另外,length為2的整數次方也保證了h&(length-1)與h%length等效).6.HashMap接受null鍵;7.HashMap不允許鍵重複,但是值是可以重複的。若鍵重複,那麼新值會覆蓋舊值。8.HashMap通過鏈表法解決衝突問題,每個Entry都有一個next指標指向下一個Entry,衝突元素(不是鍵相同,而是hash值相同)會構成一個鏈表。並且最新插入的鍵值對始終位於鏈表首部。9.當容量超過閾值(threshold)時,會發生擴容,擴容後的數組是原數組的兩倍。擴容操作需要開闢新數組,並對原數組中所有鍵值對重新散列,非常耗時。我們應該盡量避免HashMap擴容。10.HashMap非安全執行緒。
HashMap源碼中put方法裡面ehash == hash && ((k = ekey) == key || keyequals(k))
如果兩個對象相同,那麼它們的hashCode值一定要相同,所以先e.hash == hash,但是這個只是必要條件,而不是充分條件,也就是說,如果兩個對象的hashCode相同,它們並不一定是同一個對象,所以後面還要key.equals(k)
java查看hashmap的源碼發現並沒有向entrySet中裝入元素,而去可以如下遍曆
協助文檔上說:返回此映射所包含的映射關係的 collection 視圖。在返回的集合中,每個元素都是一個 Map.Entry。
entrySet僅僅是一個視圖而已,沒有具體的資料,其實還是從HashMap中擷取資料的。具體可以看entry和entrySet的原始碼就知道資料其實還是來自於table。