做者: dreamcatcher-cx 出處: <http://www.cnblogs.com/chengxiao/>
原文:https://www.cnblogs.com/chengxiao/p/6059914.html#undefined
本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在頁面明顯位置給出原文連接。
哈希表(hash table)也叫散列表,是一種很是重要的數據結構,應用場景及其豐富,許多緩存技術(好比memcached)的核心其實就是在內存中維護一張大的哈希表,本文會對java集合框架中的對應實現HashMap的實現原理進行講解,而後會對JDK7的HashMap源碼進行分析。html
在介紹哈希表之間,先介紹一些其餘常見的數據結構在增長,刪除,和查找上的性能。java
數組:採用一段連續的存儲單元來存儲數據。對於指定下標的查找,時間複雜度爲O(1);經過給定值進行查找,須要遍歷數組,逐一比對給定關鍵字和數組元素,時間複雜度爲O(n),固然,對於有序數組,則可採用二分查找,插值查找,斐波那契查找等方式,可將查找複雜度提升爲O(logn);對於通常的插入刪除操做,涉及到數組元素的移動,其平均複雜度也爲O(n)。數組
線性鏈表:對於鏈表的新增,刪除等操做(在找到指定操做位置後),僅需處理結點間的引用便可,時間複雜度爲O(1),而查找操做須要遍歷鏈表逐一進行比對,複雜度爲O(n)。緩存
二叉樹:對一棵相對平衡的有序二叉樹,對其進行插入,查找,刪除等操做,平均複雜度均爲O(logn)。數據結構
哈希表:相比上述幾種數據結構,在哈希表中進行添加,刪除,查找等操做,性能十分之高,不考慮哈希衝突的狀況下,僅需一次定位便可完成,時間複雜度爲O(1),接下來咱們就來看看哈希表是如何實現達到驚豔的常數階O(1)的。併發
咱們知道,數據結構的物理存儲結構只有兩種:順序存儲結構和鏈式存儲結構(像棧,隊列,樹,圖等是從邏輯結構去抽象的,映射到內存中,也這兩種物理組織形式),而在上面咱們提到過,在數組中根據下標查找某個元素,一次定位就能夠達到,哈希表利用了這種特性,哈希表的主幹就是數組。框架
好比咱們要新增或查找某個元素,咱們經過把當前元素的關鍵字 經過某個函數映射到數組中的某個位置,經過數組下標一次定位就可完成操做。
ide
存儲位置 = f(關鍵字)memcached
其中,這個函數f通常稱爲哈希函數,這個函數的設計好壞會直接影響到哈希表的優劣。舉個例子,好比咱們要在哈希表中執行插入操做:函數
查找操做同理,先經過哈希函數計算出實際存儲地址,而後從數組中對應地址取出便可。
然而萬事無完美,若是兩個不一樣的元素,經過哈希函數得出的實際存儲地址相同怎麼辦?也就是說,當咱們對某個元素進行哈希運算,獲得一個存儲地址,而後要進行插入的時候,發現已經被其餘元素佔用了,其實這就是所謂的哈希衝突,也叫哈希碰撞。前面咱們提到過,哈希函數的設計相當重要,好的哈希函數會盡量地保證 計算簡單和散列地址分佈均勻,可是,咱們須要清楚的是,數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證獲得的存儲地址絕對不發生衝突。那麼哈希衝突如何解決呢?
哈希衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被佔用的存儲地址),再散列函數法,鏈地址法,而HashMap便是採用了鏈地址法,也就是數組+鏈表的方式,
HashMap的主幹是一個Entry類型名爲table的數組。Entry是HashMap的基本組成單元,每個Entry包含一個key-value鍵值對,Entry是HashMap中的一個靜態內部類。
//HashMap的主幹數組,能夠看到就是一個Entry數組,初始值爲空數組{},主幹數組的長度必定是2的次冪,至於爲何這麼作,後面會有詳細分析。 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
Entry靜態內部類部分代碼:
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構 int hash;//對key的hashcode值進行hash運算後獲得的值,存儲在Entry,避免重複計算 /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
HashMap的存儲總體應該是這樣的:
簡單來講,HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,若是定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操做很快,僅需一次尋址便可;若是定位到的數組包含鏈表,對於添加操做,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,不然新增;對於查找操做來說,仍需遍歷鏈表,而後經過key對象的equals方法逐一比對查找。因此,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。
HashMap中的put方法:
public V put(K key, V value) { //若是table數組爲空數組{},進行數組填充(爲table分配實際內存空間),入參爲threshold,此時threshold爲initialCapacity 默認是1<<4(24=16) if (table == EMPTY_TABLE) { inflateTable(threshold); } //若是key爲null,存儲位置爲table[0]或table[0]的衝突鏈上 if (key == null) return putForNullKey(value); int hash = hash(key);//對key的hashcode進一步計算,確保散列均勻 int i = indexFor(hash, table.length);//獲取在table中的實際位置 for (Entry<K,V> e = table[i]; e != null; e = e.next) { //若是該對應數據已存在,執行覆蓋操做。用新value替換舊value,並返回舊value 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++;//保證併發訪問時,若HashMap內部結構發生變化,快速響應失敗 addEntry(hash, key, value, i);//新增一個entry return null; }
解釋:
第一次put數據的時候,會先建立名爲table的entry類型的數組;默認長度是16,負載因子是0.75;
先判斷傳入的key是否是null,若是是null的話,就插入到索引值index爲0的位置上,若已經存在數據,那麼就會覆蓋原來的數據,而且返回被覆蓋數據。
若是key不爲null;
1.使用int hash=hash(key.hashCode());獲取hash值(ps:這個hash(Object k)是HashMap中的一個神奇的方法;屢次使用了位運算以及二進制位的調整,來保證後面獲取的index索引值儘量的均勻分佈);
2.再使用int index=indexFor(hash,table.length)來獲取該key存放再table中的索引值(下標);由於不知道是否存在哈希衝突(也就是不知道該索引值對應的位置是否存在數據),因此遍歷該索引值所對應的鏈表,判斷是否存在相同的key(這裏的‘相同’指的是:key的hash相同而且equals相同);若是相同則覆蓋原來的數據而且返回被覆蓋數據的value,若是最後沒有發現相同的key,那麼就插入數據到最後(哈希衝突的狀況下)/直接插入到索引值對應的位置(不存在哈希衝突);而且返回null。
總的流程大概是這樣的:
HashMap中的get方法:
public V get(Object key) { //若是key爲null,則直接去table[0]處去檢索便可。 if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
Entry中的GetEntry方法:
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } //經過key的hashcode值計算hash值 int hash = (key == null) ? 0 : hash(key); //indexFor (hash&length-1) 獲取最終數組索引,而後遍歷鏈表,經過equals方法比對找出對應記錄 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; }
解析:
get方法是根據key值來獲取value;首先判斷傳入的key是否是null,若是爲null,那麼就直接返回索引值index爲0的值;若是不爲空,調用getEntry方法,而且返回entry.getValue()值。
get方法的實現相對簡單,key(hashcode)-->hash-->indexFor-->最終索引位置,找到對應位置table[i],再查看是否有鏈表,遍歷鏈表,經過key的equals方法比對查找對應的記錄。要注意的是,有人以爲上面在定位到數組位置以後而後遍歷鏈表的時候,e.hash == hash這個判斷不必,僅經過equals判斷就能夠。其實否則,試想一下,若是傳入的key對象重寫了equals方法卻沒有重寫hashCode,而恰巧此對象定位到這個數組位置,若是僅僅用equals判斷多是相等的,但其hashCode和當前對象不一致,這種狀況,根據Object的hashCode的約定,不能返回當前對象,而應該返回null,後面的例子會作出進一步解釋。
補充2個方法——hash()和indexFor()
hash()
//這是一個神奇的函數,用了不少的異或,移位等運算,對key的hashcode進一步進行計算以及二進制位的調整等來保證最終獲取的存儲位置儘可能分佈均勻 final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
indexFor()
/** * 返回數組下標 */ static int indexFor(int h, int length) { return h & (length-1); }
咱們再重寫equals方法的時候,必定也要同時重寫hashCode方法;下面演示只重寫equals方法而不重寫hashCode方法會出現什麼狀況。
/** * Created by chengxiao on 2016/11/15. */ public class MyTest { private static class Person{ int idCard; String name; public Person(int idCard, String name) { this.idCard = idCard; this.name = name; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()){ return false; } Person person = (Person) o; //兩個對象是否等值,經過idCard來肯定 return this.idCard == person.idCard; } } public static void main(String []args){ HashMap<Person,String> map = new HashMap<Person, String>(); Person person = new Person(1234,"喬峯"); //put到hashmap中去 map.put(person,"天龍八部"); //get取出,從邏輯上講應該能輸出「天龍八部」 System.out.println("結果:"+map.get(new Person(1234,"蕭峯"))); } }
實際輸出: null
若是咱們已經對HashMap的原理有了必定了解,這個結果就不難理解了。儘管咱們在進行get和put操做的時候,使用的key從邏輯上講是等值的(經過equals比較是相等的),但因爲沒有重寫hashCode方法,因此put操做時,key(hashcode1)-->hash-->indexFor-->最終索引位置 ,而經過key取出value的時候 key(hashcode2)-->hash-->indexFor-->最終索引位置,因爲hashcode1不等於hashcode2,致使沒有定位到一個數組位置而返回邏輯上錯誤的值null(也有可能碰巧定位到一個數組位置,可是也會判斷其entry的hash值是否相等,上面get方法中有提到。)
因此,在重寫equals的方法的時候,必須注意重寫hashCode方法,同時還要保證經過equals判斷相等的兩個對象,調用hashCode方法要返回一樣的整數值。而若是equals判斷不相等的兩個對象,其hashCode能夠相同(只不過會發生哈希衝突,應儘可能避免)。