HashMap基於hash表的Map接口實現,它實現了Map接口中的全部操做。HashMap容許存儲null鍵和null值。這是它與Hashtable的區別之一(另一個區別是Hashtable是線程安全的)。另外,HashMap中的鍵值對是無序的。下面,咱們從HashMap的源代碼來分析HashMap的實現,如下使用的是Jdk1.7.0_51。java
1、HashMap的存儲實現算法
HashMap底層採用的是數組和鏈表這兩種數據結構.當咱們把key-value對put到HashMap時,系統會根據hash算法計算key的hash值,根據hash值決定key-value對存放在數組的哪一個位置(也就是散列表中的」桶」位).若是該位置已經存放Entry,則該位置上的Entry造成Entry鏈.下面咱們從源代碼入手分析.數組
public V put(K key, V value) { //① 若是table爲空,調用inflateTable()初始化table數組 if (table == EMPTY_TABLE) { inflateTable(threshold); } // 若是key爲null,調用putForNullKey()處理 if (key == null) return putForNullKey(value); // ② 調用hash算法,算出key的hash值 int hash = hash(key); // ③ 根據hash值和table的長度計算在table中的存放位置 int i = indexFor(hash, table.length); // 若是key存在,則替換以前的value值 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; } } // 模數自增,用於實現fail-fast機制 modCount++; // ④ 添加key-value對 addEntry(hash, key, value, i); return null; }
上面的程序中用到了一個重要的內部接口:Map.Entry,每一個Map.Entry其實就是一個封裝了key-value屬性的對象.從上面的代碼中也能夠看出:系統決定HashMap中的key-value對時,沒有考慮Entry中的value,僅僅是根據key來計算並決定每一個Entry的存儲位置.安全
從①處代碼能夠看到,調用put方法時會檢查table數組的容量.若是table數組爲空數組,會先初始化table數組,咱們看下HashMap是如何初始化table數組的。數據結構
private void inflateTable(int toSize) { // 找到大於toSize的最小的2的n次方 int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); // 初始化table數組 table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }
從上面的代碼中能夠知道,HashMap中table數組的長度必定是2的n次方.實際上,這是一個很優雅的設計,在後面咱們還會提到。若是key不爲null,系統會調用hash()算法算出key的hash值,並據此來計算key的的存放位置.多線程
final int hash(Object k) { int h = hashSeed; // 若是key爲字符串,調用stringHash32()處理 // 由於字符串的hashCode碼同樣的可能性大,形成hash衝突的可能性也大 if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } // 根據key的hashCode值算hash值 h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
獲得key的hash值後,從④處的代碼知道,此時系統會根據hash值和table的長度來計算key在table數組中的存放位置.ide
static int indexFor(int h, int length) { return h & (length-1); }
這個方法的設計很是巧妙,經過h&(table.length-1)來獲得該key的保存位置,而上面說到了HashMap底層數組長度老是2的n次方.當length老是2的n次方時,h&(length-1)能保證計算獲得的值老是位於table數組的索引以內.假設h=5,length=16,h&(length-1)=5;h=6,length=16,h&(length-1)=6…函數
接下來,若是key已經存在,則替換其value值.若是不存在則調用addEntry()處理.源碼分析
void addEntry(int hash, K key, V value, int bucketIndex) { // 檢查HashMap容量是否達到極限(threshold)值 if ((size >= threshold) && (null != table[bucketIndex])) { // 擴充table數組的容量爲以前的1倍 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } // 調用createEntry()添加key-value對到HahsMap createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) { // 獲取table[bucketIndex]的Entry Entry<K,V> e = table[bucketIndex]; // 根據key-value建立新的Entry對象,並把新建立的Entry存放到table[bucketIndex]處 // 新Entry對象保存e對象(以前table[bucketIndex]的Entry對象)的引用,從而造成Entry鏈 table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
系統老是將新添加的Entry對象放入table[bucketIndex]—若是bucketIndex處已經有一個Entry對象,那新添加的Entry對象指向原有的Entry對象(Entry持有一個指向原Entry對象的引用,產生一個Entry鏈),若是bucketIndex處沒有Entry對象,即上面代碼中e爲null,也就是新添加的Entry對象持有一個null引用,並無產生Entry鏈.性能
從上面整個put方法的分析來看,咱們能夠知道HashMap存儲元素的基本流程:首先根據算出key的hash值,根據hash值和table的長度計算該key的存放位置.若是key相同,則新值替換舊值.若是key不一樣,則在table[i]桶位造成Entry鏈,並且新添加的Entry位於Entry鏈的頭部(table[i]).
上面的代碼有點多,附上put(K key,V value)方法的流程圖:
下面是HashMap的存儲示意圖:
2、HashMap的讀取實現
當HashMap的每一個buckete裏存儲的Entry只是單個Entry—也就是沒有經過指針產生Entry鏈(沒有產生hash衝突)時,此時HashMap具備最好的性能(底層結構僅僅是數組,沒有產生鏈表):當程序經過key取出對應的value時,系統先計算出hash(key)值找到key在table數組的存放位置,而後取出該桶位的Entry鏈,遍歷找到key對應的value.如下是get(K key)方法的源代碼:
public V get(Object key) { // 若是key爲null,調用getForNullKey()處理 if (key == null) return getForNullKey(); // 獲取key所對應的Entry Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } // 計算hash(key)值 int hash = (key == null) ? 0 : hash(key); // 遍歷Entry鏈,找到key所對應的Entry 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能夠根據hash(key)值快速取出table[bucket]的Entry.在發生」Hash衝突」的狀況下,table[bucket]存放的不是一個Entry,而是一個Entry鏈,系統只能按順序遍歷Entry鏈,直到找到key相等的Entry,若是要搜索的Entry位於Entry鏈的最末端(該Entry最先放入bucket),那麼系統必須循環到最後才能找到該Entry.
概括起來簡單地說,HashMap在底層將key-value當成一個總體進行處理,這個總體就是一個Entry對象.HashMap底層採用一個Entry[]數組來保存全部的key-value對,當存儲一個Entry對象時,會根據Hash算法來決定其存儲位置;當須要取出一個Entry時,也會根據Hash算法找到其存儲位置,再取出該Entry.因而可知:HashMap快速存取的基本原理是:不一樣的東西放在不一樣的位置,須要時才能快速找到它.
3、Hash算法的性能選項
HashMap中定義瞭如下幾個成員變量:
① size:HashMap中存放的Entry數量
② loadFactor:HashMap的負載因子
③ threshold:HashMap的極限容量,當HashMap的容量達到該值時,HashMap會自動擴容(threshold=loadFactory*table.length)
HashMap默認的構造函數會建立一個初始容量爲16,負載因子爲0.75的HashMap對象.固然,咱們也能夠經過其餘構造函數指定HashMap的初始容量和負載因子.從上面的源碼分析中,咱們知道建立HashMap時的實際容量一般比initialCapacity大一些,除非咱們指定的initialCapacity參數值正好是2的n次方.固然,知道這個之後,應該在建立HashMap時將initialCapacity參數值指定爲2的n次方,這樣能夠減小系統的計算開銷.
當建立HashMap時,有一個默認的負載因子(load factor),其默認值爲0.75,這是時間和空間成本上的一種折衷:增大負載因子能夠減小Hash表(Entry數組)所佔用的內存空間,但會增長查詢數據的時間開銷,而查詢時最頻繁的操做(HashMap的get()與put()方法都要用到查詢);減小負載因子會提升數據查詢的性能,但會增長Hash表所佔用的內存空間.
若是可以預估HashMap會保存Entry的數量,能夠再建立HashMap時指定初始容量,若是HashMap的size一直不會超過threshold(capacity*loadFactory),就無需調用resize()從新分配table數組,resize()是很耗性能的,由於要對全部的Entry從新分配位置.固然,開始就將初始化容量設置過高可能會浪費空間(系統須要建立一個長度爲capacity的Entry數組),所以建立HashMap時初始化容量也須要當心設置.
4、細數HashMap中的優雅的設計
目前就發現這麼多,之後發現了再繼續補上.
都說好的設計是成功的一半,HashMap的設計者展現了一種設計美感.
5、HashMap使用注意問題
以本人目前的經驗來看,HashMap使用過程當中應注意兩大類問題,其一,線程安全問題,由於HashMap是非同步的,在多線程狀況下請使用ConcurrentHashMap。其二,內存泄露問題.咱們這裏只討論第二種問題.由上面的分析能夠知道,存放到HashMap的對象,強烈建議覆寫equals()和hashCode().但hashCode值的改變可能會形成內存泄露問題.看代碼:
public class HashCodeDemo { public static void main(String[] args) { User user = new User("zhangsan",22); Map<User,Object> map = new HashMap<User,Object>(); map.put(user, "user is exists"); // user is exists System.out.println(map.get(user)); // 改變age值,將會改變hashCode值 user.setAge(23); // null,由於user.hashCode值變化了,此時,咱們可能永遠也沒法取出該Entry對象,但HashMap持有該Entry對象的引用,這就形成了內存泄露 System.out.println(map.get(user)); } } class User{ private String name; private Integer age; public User() { } public User(String name, Integer age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public int hashCode() { return name.hashCode()*age.hashCode(); } @Override public boolean equals(Object obj) { if(obj instanceof User){ User user = (User)obj; return this.name.equals(user.name)&&this.age==user.getAge()?true:false; } return false; } }
6、自定義HashMap實現
這裏只作簡單模擬,加深對HashMap的理解.
第一步,建立MyEntry類,用於封裝key-value屬性.
class MyEntry<K, V> { private final K key; private V value; private MyEntry<K, V> next; private final int hash; /** 構造函數 **/ public MyEntry(K key, V value, MyEntry<K, V> next, int hash) { this.key = key; this.value = value; this.next = next; this.hash = hash; } /** 返回Entry.key **/ public K getKey() { return this.key; } /** 返回Entry.value **/ public V getValue() { return this.value; } /** 替換Entry.value **/ public V setValue(V val) { V oldVal = value; this.value = val; return oldVal; } public MyEntry next(){ return next; } public int hash(){ return hash; } @Override public String toString() { return this.key + "=" + this.value; } public void setNext(MyEntry myEntry) { this.next = myEntry; } }
第二步,實現MyHashMap,底層採用數組+鏈表結構.到這裏,咱們會發現,其實實現HashMap關鍵點有如下幾個:
① HashMap容量的管理和性能參數的設置
② hash()算法的實現,理想的hash算法是不會產生"hash衝突的"(HashMap底層僅僅是數組),在這種狀況下,HashMap能達到最好的存取性能.
HashMap的設計者很好的解決了這兩個問題,關於這兩個問題,能夠參考源碼.
以上就是我對HashMap源碼的學習總結,有不正確或不許確的地方,請你們指出來!很是歡迎你們一塊兒交流學習!
以上內容參考:http://www.ibm.com/developerworks/cn/java/j-lo-hash/?ca=drs-tp4608