分析HashMap以前先介紹下什麼Hashcode(散列碼)。它是一個int,每一個對象都會有一個hashcode,它在內存的存放位置是放在對象的頭部(對象頭部存放的信息有hashcode,指向Class的引用,和一些有關垃圾回收信息)。須要注意的是,若是在你的類中覆蓋了Object的equals(Object)方法,那麼你必須覆蓋hashCode方法,否則,當你使用HashMap,HashSet,HashTable時會出現問題。這個問題在《effective Java中文版》這本書裏面有詳細的介紹。下面只是簡要摘除來3個主要的緣由 (摘自Object規範):
(1)在程序的執行期間,只要對象的equals方法的比較操做所用到的信息沒有被修改,那麼對這同一對象調用屢次,hashCode方法必須始終如一的返回同一個值,在同一個應用的屢次執行過程當中,每次執行所返回的值能夠不一樣。
(2)若是兩個對象,根據equas方法比較是想法的,那麼掉哦你給這兩個對象中任意一個對象的hashCode方法都必須產生相同的整數結果。
(3)若是兩個對象根據equals方法比較是不相等的,那麼調用這兩個對象中任意一個對象的hashCode方法,則不必定要產生不一樣的整數結果,可是程序員應該知道,給不一樣的對象產生大相徑庭的整數結果,有可能提升散列表的性能。
下面以String的hashCode舉個列子:
String類重寫了Object類中的equals和hashCode方法,緣由很簡單,Object中的equals方法是指比較兩個對象是否是指向同一個引用對象,而String類指須要比較內容相不相等就能夠了。因此String覆蓋了equals方法,同時覆蓋了hashCode方法(具體爲何同時覆蓋二者參考以上3點以及http://blog.csdn.net/michaellufhl/article/details/5833188)。
String中的hashcode算法很簡單以下:html
@Override public int hashCode() { int hash = hashCode; if (hash == 0) { if (count == 0) { return 0; } for (int i = 0; i < count; ++i) { hash = 31 * hash + charAt(i); } hashCode = hash; } return hash; }
好比一個字符串「abc」(a的ascii碼是97),它的hashcode算法是:
h = 31 * 0 + 97 ==> h = 97;
h = 31 * 97 + 98 ==> h = 3105;
h = 31 * 3105 + 99 ==> h = 96354;
因此「abc」的hashCode就是96354 。java
以上內容參考文檔:程序員
HashMap是以鏈法表的形式存儲的,與其對應的是開放地址發。兩種方法的比較:鏈表法和開放地址法。鏈表法就是將相同hash值的對象組織成一個鏈表放在hash值對應的槽位;開放地址法是經過一個探測算法,當某個槽位已經被佔據的狀況下繼續查找下一個可使用的槽位。java.util.HashMap採用的鏈表法的方式,鏈表是單向鏈表,所以在刪除過程當中要本身維持prev節點。
HashMap的存儲結構圖(來自網絡):
HashMap的功能是經過「鍵(key)」可以快速的找到「值」。下面咱們分析下HashMap存數據的基本流程:
一、 當調用put(key,value)時,首先獲取key的hashcode。
二、 再把hash經過一下運算獲得一個int h.
hash ^= (hash >>> 20) ^ (hash >>> 12);
int h = hash ^ (hash >>> 7) ^ (hash >>> 4);
爲何要通過這樣的運算呢?這就是HashMap的高明之處。先看個例子,一個十進制數32768(二進制1000 0000 0000 0000),通過上述公式運算以後的結果是35080(二進制1000 1001 0000 1000)。看出來了嗎?或許這樣還看不出什麼,再舉個數字61440(二進制1111 0000 0000 0000),運算結果是65263(二進制1111 1110 1110 1111),如今應該很明顯了,它的目的是讓「1」變的均勻一點,散列的本意就是要儘可能均勻分佈。那這樣有什麼意義呢?看第3步。
三、 獲得h以後,把h與HashMap的承載量(HashMap的默認承載量length是16,能夠自動變長。在構造HashMap的時候也能夠指定一個長度。這個承載量就是上圖所描述的數組的長度。)進行邏輯與運算,即 h & (length-1),這樣獲得的結果就是一個比length小的正數,咱們把這個值叫作index。其實這個index就是索引將要插入的值在數組中的位置。第2步那個算法的意義就是但願可以得出均勻的index,這是HashTable的改進,HashTable中的算法只是把key的hashcode與length相除取餘,即hash % length,這樣有可能會形成index分佈不均勻。還有一點須要說明,HashMap的鍵能夠爲null,它的值是放在數組的第一個位置。
四、 咱們用table[index]表示已經找到的元素須要存儲的位置。先判斷該位置上有沒有元素(這個元素是HashMap內部定義的一個類Entity,基本結構它包含三個類,key,value和指向下一個Entity的next),沒有的話就建立一個Entity數組
static class Entry implements Map.Entry { final K key; V value; Entry next; final int hash; ...//More code goes here }
每當往hashmap裏面存放key-value對的時候,都會爲它們實例化一個Entry對象,這個Entry對象就會存儲在前面提到的Entry數組table中。如今你必定很想知道,上面建立的Entry對象將會存放在具體哪一個位置(在table中的精確位置)。答案就是,根據key的hashcode()方法計算出來的hash值(來決定)。hash值用來計算key在Entry數組的索引。網絡
原文地址:數據結構
HashMap的構造方法:
無參構造方法:會使用默認的初始容量和加載因子初始化map,默認初始化大小是16,加載因子0.75f。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操做(即重建內部數據結構),從而哈希表將具備大約兩倍的桶數。app
/** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f; public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
自定義初始化大小ide
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
自定義初始化大小和加載因子
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ 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; threshold = initialCapacity; init(); }
總結:當 建立 HashMap 時,有一個默認的負載因子(load factor),其默認值爲 0.75,這是時間和空間成本上一種折衷:增大負載因子能夠減小 Hash 表(就是那個 Entry 數組)所佔用的內存空間,但會增長查詢數據的時間開銷,而查詢是最頻繁的的操做(HashMap 的 get() 與 put() 方法都要用到查詢);減少負載因子會提升數據查詢的性能,但會增長 Hash 表所佔用的內存空間。咱們能夠在建立 HashMap 時根據實際須要適當地調整 load factor 的值;若是程序比較關心空間開銷、內存比較緊張,能夠適當地增長負載因子;若是程序比較關心時間開銷,內存比較寬裕則能夠適當的減小負載因子。一般狀況 下,無需改變負載因子的值。
HashMap最經常使用的put方法,代碼以下:
/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) */ public V put(K key, V value) { //若是數組爲空,初始化 if (table == EMPTY_TABLE) { inflateTable(threshold); } //若是key爲空,則調用putForNullKey進行處理 if (key == null) return putForNullKey(value); int hash = hash(key);//計算key的hashcode值 int i = indexFor(hash, table.length);//計算key在hash表中的索引,此處的table是一個Entry<k,v>數組 //遍歷數組,比較Entry是否一致(hash值相等,即在hash表中的同一位置),而且key值相等,則直接用新的value替換舊的value並返回value,key值不用替換。若是不知足條件,則將key和value添加到i索引處 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //將key和value添加到i索引處 addEntry(hash, key, value, i); return null; }
上面的put方法中用到了一個重要的內部類HashMap$Entry,每一個 Entry 其實就是一個 key-value 對。從上面程序中能夠看出:當系統決定存儲 HashMap 中的 key-value 對時,徹底沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每一個 Entry 的存儲位置。當決定了 key 的存儲位置以後,value 隨之保存在那裏便可,Entry源碼以下:
static class Entry<K,V> implements Map.Entry<K,V> { final K key;//key值 V value;//value值 Entry<K,V> next;//Entry鏈指向 int hash;//key的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; } 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() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } 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) { } }
put方法中調用了一個計算Hash碼的方法hash()來返回key的哈希碼,這個方法是一個純粹的數學計算,其方法以下:
final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
對於任意給定的對象,只要它的 hashCode() 返回值相同,那麼程序調用 hash(int h) 方法所計算獲得的 Hash 碼值老是相同的。接下來程序會調用 indexFor(int h, int length) 方法來計算該對象應該保存在 table 數組的哪一個索引處。indexFor(int h, int length) 方法的代碼以下:
//h爲key的hash值,length爲數組的長度 static int indexFor(int h, int length) { return h & (length-1); }
這個方法很是巧妙,它老是經過 h &(table.length -1) 來獲得該對象的保存位置,而HashMap底層數組的長度老是2的n次方,這一點可參看前面關於HashMap構造器的介紹。
當length老是2的倍數時,h&(length-1)將是一個很是巧妙的設計:假設 h=5,length=16, 那麼h&(length - 1) 將獲得5;若是h=6,length=16, 那麼h&(length - 1)將獲得6 ;若是h=15,length=16, 那麼h&(length - 1)將獲得15;可是當h=16時 ,length=16時,那麼h&(length - 1)將獲得0了;當 h=17 時 , length=16 時,那麼h&(length - 1) 將獲得1了……這樣保證計算獲得的索引值老是位於 table 數組的索引以內。
從put 方法的源代碼能夠看出,當程序試圖將一個 key-value 對放入 HashMap 中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:若是兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。存儲位置相同會分爲兩種狀況:
(1).若是這兩個 Entry 的 key 經過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但 key 不會覆蓋。
(2).若是這兩個 Entry 的 key 經過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 造成 Entry 鏈,並且新添加的 Entry 位於 Entry 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。
存儲位置不一樣,則將key和value直接添加到i索引處。
addEntyr方法,源碼以下:
void addEntry(int hash, K key, V value, int bucketIndex) { //若是容量大於閾值,而且索引bucketIndex處的元素不爲空 if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);//擴容爲原來數組長度的兩倍 hash = (null != key) ? hash(key) : 0;//從新計算key的hash值 bucketIndex = indexFor(hash, table.length);//從新計算元素在新table中的索引 } //建立新的entry對象並放到table的bucketIndex索引處,並讓新的entry指向原來的entry createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
上面createEntry方法包含了一個很是優雅的設計:老是將新添加的 Entry 對象放入 table 數組的 bucketIndex 索引處——若是 bucketIndex 索引處已經有了一個 Entry 對象,那新添加的 Entry 對象指向原有的 Entry 對象(產生一個 Entry 鏈),若是 bucketIndex 索引處沒有 Entry 對象,上面程序 e 變量是 null,也就是新放入的 Entry 對象指向 null,也就是Entry內部類中的next屬性爲null,也就是沒有產生 Entry 鏈。,能夠對比Entry類看。
解釋幾個名詞:
桶:對 於 HashMap 及其子類而言,它們採用 Hash 算法來決定集合中元素的存儲位置。當開始初始化 HashMap 時,會建立一個長度爲 capacity 的 Entry 數組,這個數組裏能夠存儲元素的位置被稱爲「桶(bucket)」,每一個 bucket 都有其指定索引,系統能夠根據其索引快速訪問該 bucket 裏存儲的元素。
Entry鏈:不管什麼時候,HashMap 的每一個「桶」只存儲一個元素(也就是一個 Entry),因爲 Entry 對象能夠包含一個引用變量(就是 Entry 構造器的的最後一個參數next)用於指向下一個 Entry,所以可能出現的狀況是:HashMap 的 bucket 中只有一個 Entry,但這個 Entry 指向另外一個 Entry ——這就造成了一個 Entry 鏈。下圖爲我簡單的畫了一個HashMap的存儲結構:
經過Java HashMap的存取方式來學習Hash存儲機制
HashMap最經常使用的get方法,源碼以下:
public V get(Object key) { //若是key爲null,則調用getForNullKey得到value if (key == null) return getForNullKey(); //不然調用getEntry方法 Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } //計算key的hash值 int hash = (key == null) ? 0 : hash(key); //直接經過key的hash值獲取該Entry在數組中的下標,從而獲取該Entry對象並遍歷entry鏈,直到找到相等的key,而後取出該key對應的value。 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; }
從上面代碼中能夠看出,若是 HashMap 的每一個 bucket 裏只有一個 Entry 時,HashMap 能夠根據索引、快速地取出該 bucket 裏的 Entry;在發生「Hash 衝突」的狀況下,單個 bucket 裏存儲的不是一個 Entry,而是一個 Entry 鏈,只能按順序遍歷每一個 Entry,直到找到想搜索的 Entry 爲止——若是剛好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最先放入該 bucket 中),那必須循環到最後才能找到該元素。因此,當 HashMap 的每一個 bucket 裏存儲的 Entry 只是單個 Entry ,也就是沒有經過指針產生 Entry 鏈時,此時的 HashMap 具備最好的性能:當程序經過 key 取出對應 value 時,只要先計算出該 key 的 hashCode() 返回值,在根據該 hashCode 返回值找出該 key 在 table 數組中的索引,而後取出該索引處的 Entry,最後返回該 key 對應的 value 便可。
算是本身學習的Map的網上資料的一個彙總(當是備忘錄了吧)。
附上本身感受比較好的文檔:
http://carmen-hongpeng.iteye.com/blog/1706415#
http://www.oracle.com/technetwork/cn/articles/maps1-100947-zhs.html