https://www.cnblogs.com/chengxiao/p/6059914.htmlhtml
哈希表是根據關鍵碼值而直接進行訪問的數據結構。也就是說,它能經過把關鍵碼值映射到表中的一個位置來訪問。這個映射函數就叫作散列函數,存放記錄的數組就叫散列表。java
給定表M,存在函數f(key),對任意給定的關鍵字值key,代入函數後若能獲得包含該關鍵字的記錄在表中的地址,則稱表M爲哈希(Hash)表,函數f(key)爲哈希(Hash) 函數。算法
在哈希表中進行添加,刪除,查找等操做,性能十分之高,不考慮哈希衝突的狀況下,僅需一次定位便可完成,時間複雜度爲O(1)。數組
若是兩個不一樣的元素,經過哈希函數得出的實際存儲地址相同怎麼辦?也就是說,當咱們對某個元素進行哈希運算,獲得一個存儲地址,而後要進行插入的時候,發現已經被其餘元素佔用了,其實這就是所謂的哈希衝突,也叫哈希碰撞。前面咱們提到過,哈希函數的設計相當重要,好的哈希函數會盡量地保證計算簡單和散列地址分佈均勻,可是,咱們須要清楚的是,數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證獲得的存儲地址絕對不發生衝突。那麼哈希衝突如何解決呢?哈希衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被佔用的存儲地址),再散列函數法,鏈地址法,而HashMap便是採用了鏈地址法,也就是數組+鏈表的方式。數據結構
HashMap的主幹是一個Entry數組,Entry是HashMap的基本組成單元,每個Entry包含一個key-value鍵值對還有下一個節點,所以Entry是一個單向鏈表。代碼以下:併發
static class Entry implements Map.Entry { final K key; V value; Entry next; int hash; Entry(int h, K k, V v, Entry 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 (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode()); } public final String toString() { return getKey() + "=" + getValue(); } void recordAccess(HashMap m) { } void recordRemoval(HashMap m) { } }
HashMap的總體結構以下圖:app
能夠看出,HashMap是有數組和鏈表組成的,數組是HashMap的主體,鏈表是爲了解決哈希衝突而存在的。由上面結構能夠看出來,若是不存在鏈表,HashMap的查詢修改刪除性能都很是好,若是存在鏈表不少,即存在不少哈希衝突,則性能會下降不少,由於到指定位置後還要遍歷整個鏈表。ide
HashMap繼承於AbstractMap,實現了Map<K,V>函數
java.lang.Object ↳ java.util.AbstractMap ↳ java.util.HashMap public class HashMap extends AbstractMap implements Map, Cloneable, Serializable { }
源碼以下:源碼分析
// 默認的初始容量是16,必須是2的冪。 static final int DEFAULT_INITIAL_CAPACITY = 16; // 最大容量(必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換) static final int MAXIMUM_CAPACITY = 1 << 30; // 默認加載因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; / 存儲數據的Entry數組,長度是2的冪。 // HashMap是採用拉鍊法實現的,每個Entry本質上是一個單向鏈表 transient Entry[] table; // HashMap的大小,它是HashMap保存的鍵值對的數量 transient int size; // HashMap的閾值,用於判斷是否須要調整HashMap的容量(threshold = 容量*加載因子) int threshold; // 加載因子實際大小 final float loadFactor; // HashMap被改變的次數,fail-fast用於快速拋出異常ConcurrentModificationException transient int modCount; static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
HashMap有4個構造器,其餘構造器若是用戶沒有傳入initialCapacity 和loadFactor這兩個參數,會使用默認值
initialCapacity默認爲16,loadFactory默認爲0.75,關鍵的一個代碼以下:
public HashMap(int initialCapacity, float loadFactor) { //對傳入的初始容量、加載因子進行校驗,初始容量不能小於0,最大隻能是1<<30(2³°) 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 // 找到一個最接近初始容量的是2的倍數的容量值 int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; //根絕容量和加載因子算擴容的閥值 threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //初始化一個空的Entry的數組 table = new Entry[capacity]; //是否使用備用Hash算法,默認不開啓 //Holder.ALTERNATIVE_HASHING_THRESHOLD 不配置的話默認是最大值 useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); //空方法 init(); }
初始化後,看下如何添加值的,看put()的源碼,以下
public V put(K key, V value) { //null值的話單獨處理,null值放在數組table[0]的位置,後面會講到 if (key == null) return putForNullKey(value); //計算key的hash值,比較重要後面單獨列出來 int hash = hash(key); //獲取在table中的實際位置 int i = indexFor(hash, table.length); for (Entry e = table[i]; e != null; e = e.next) { Object k; //若是該對應數據已存在,執行覆蓋操做。用新value替換舊value,並返回舊value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this);//空的方法 return oldValue; } } //保證併發訪問時,若HashMap內部結構發生變化,快速響應失敗 modCount++; //新增一個Entry addEntry(hash, key, value, i); return null; }
private V putForNullKey(V value) { //key爲NULL時,默認放在table[0]的位置,或者table[0]的衝突鏈上,其餘操做同上面put(); for (Entry 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; }
final int hash(Object k) { int h = 0; //若是使用備用hash算法,String類型會單獨採用stringHash32,而且使用hashSeed,默認hashSeed==0 if (useAltHashing) { if (k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h = hashSeed; } //用了不少的異或,移位等運算,對key的hashcode進一步進行計算以及二進制位的調整等來保證最終獲取的存儲位置儘可能分佈均勻 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); }
//返回數組的下標,採用位運算不用取模,對於計算機位運算效率更高 static int indexFor(int h, int length) { return h & (length-1); }
void addEntry(int hash, K key, V value, int bucketIndex) { ////當size超過臨界閾值threshold,而且即將發生哈希衝突時進行擴容 if ((size >= threshold) && (null != table[bucketIndex])) { //擴容方法,下面會單獨列出來 resize(2 * table.length); //計算下標和hash值 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } //建立Entry,單獨列出來 createEntry(hash, key, value, bucketIndex); }
擴容方法,比較重要,能夠解釋爲何數組長度必定是2的次冪,代碼以下:
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); //計算是否須要從新計算Hash值 boolean rehash = oldAltHashing ^ useAltHashing; //賦值給新的數組,下面會單獨列出來 transfer(newTable, rehash); table = newTable; //計算新的閥值 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
/** * Transfers all entries from current table to newTable. */ void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //for循環中的代碼,逐個遍歷鏈表,從新計算索引位置,將老數組數據複製到新數組中去 for (Entry e : table) { while(null != e) { Entry next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); //將當前entry的next鏈指向新的索引位置,newTable[i]有可能爲空,有可能也是個entry鏈,若是是entry鏈,直接在鏈表頭部插入。 e.next = newTable[i]; newTable[i] = e; e = next; } } }
這個方法將老數組中的數據逐個鏈表地遍歷,扔到新的擴容後的數組中,咱們的數組索引位置的計算是經過 對key值的hashcode進行hash擾亂運算後,再經過和 length-1進行位運算獲得最終數組索引位置。
1.hashMap的數組長度必定保持2的次冪,好比16的二進制表示爲 10000,那麼length-1就是15,二進制爲01111,同理擴容後的數組長度爲32,二進制表示爲100000,length-1爲31,二進制表示爲011111。從下圖能夠咱們也能看到這樣會保證低位全爲1,而擴容後只有一位差別,也就是多出了最左位的1,這樣在經過 h&(length-1)的時候,只要h對應的最左邊的那一個差別位爲0,就能保證獲得的新的數組索引和老數組索引一致(大大減小了以前已經散列良好的老數組的數據位置從新調換)。
2.數組長度保持2的次冪,length-1的低位都爲1,會使得得到的數組索引index更加均勻,好比:
咱們看到,上面的&運算,高位是不會對結果產生影響的(hash函數採用各類位運算可能也是爲了使得低位更加散列),咱們只關注低位bit,若是低位所有爲1,那麼對於h低位部分來講,任何一位的變化都會對結果產生影響,也就是說,要獲得index=21這個存儲位置,h的低位只有這一種組合。這也是數組長度設計爲必須爲2的次冪的緣由。
!
若是不是2的次冪,也就是低位不是全爲1此時,要使得index=21,h的低位部分再也不具備惟一性了,哈希衝突的概率會變的更大,同時,index對應的這個bit位不管如何不會等於1了,而對應的那些數組位置也就被白白浪費了。
這個方法比較簡單沒什麼東西,就是簡單建立個Entry,賦值到數組中
void createEntry(int hash, K key, V value, int bucketIndex) { Entry e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
public V get(Object key) { if (key == null) return getForNullKey(); Entry entry = getEntry(key); return null == entry ? null : entry.getValue(); } private V getForNullKey() { //key爲NULL時,存在table[0]位置 for (Entry e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; } final Entry getEntry(Object key) { int hash = (key == null) ? 0 : hash(key); for (Entry 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(hashcode)-->hash-->indexFor-->最終索引位置,找到對應位置table[i],再查看是否有鏈表,遍歷鏈表,經過key的equals方法比對查找對應的記錄。要注意的是,有人以爲上面在定位到數組位置以後而後遍歷鏈表的時候,e.hash == hash這個判斷不必,僅經過equals判斷就能夠。其實否則,試想一下,若是傳入的key對象重寫了equals方法卻沒有重寫hashCode,而恰巧此對象定位到這個數組位置,若是僅僅用equals判斷多是相等的,但其hashCode和當前對象不一致,這種狀況,根據Object的hashCode的約定,不能返回當前對象,而應該返回null,後面的例子會作出進一步解釋。
關於HashMap的源碼分析就介紹到這兒了,最後咱們再聊聊老生常談的一個問題,各類資料上都會提到,「重寫equals時也要同時覆蓋hashcode」,咱們舉個小例子來看看,若是重寫了equals而不重寫hashcode會發生什麼樣的問題
public class Demo { private String name; private int idCde; public Demo(String name, int idCde) { this.name = name; this.idCde = idCde; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getIdCde() { return idCde; } public void setIdCde(int idCde) { this.idCde = idCde; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Demo demo = (Demo) o; return idCde == demo.idCde && Objects.equals(name, demo.name); } // @Override // public int hashCode() { // // return Objects.hash(name, idCde); // } public static void main(String[] args) { HashMap map = new HashMap(); Demo demo = new Demo("張三",123456789); map.put(demo,"測試"); System.out.println("結果:"+map.get(demo)); System.out.println("結果:"+map.get(new Demo("張三",123456789))); } }
輸出結果爲:
結果:測試 結果:null
儘管咱們在進行get和put操做的時候,使用的key從邏輯上講是等值的(經過equals比較是相等的),但因爲沒有重寫hashCode方法,因此put操做時,key(hashcode1)-->hash-->indexFor-->最終索引位置 ,而經過key取出value的時候 key(hashcode1)-->hash-->indexFor-->最終索引位置,因爲hashcode1不等於hashcode2,致使沒有定位到一個數組位置而返回邏輯上錯誤的值null(也有可能碰巧定位到一個數組位置,可是也會判斷其entry的hash值是否相等,上面get方法中有提到。)
因此,在重寫equals的方法的時候,必須注意重寫hashCode方法,同時還要保證經過equals判斷相等的兩個對象,調用hashCode方法要返回一樣的整數值。而若是equals判斷不相等的兩個對象,其hashCode能夠相同(只不過會發生哈希衝突,應儘可能避免)。