HashMap和HashSet的内部工作机制
java 的比較基礎的功底就是對于java的集合類底層實現和原理的掌握!!!(不可輕視)
-----------------------------------------------hashmap存儲原理-------------------------------------------
案例代碼:
HashMap hashMap = new HashMap();//line1hashMap.put("one","hello1");//line2hashMap.put("two","hello2");//line3hashMap.put("three","hello3");//line4hashMap.put("four","hello4");//line5hashMap.put("five","hello5");//line6hashMap.put("six","hello6");//line7hashMap.put("seven","hello7");//line8put操作的偽代碼可以表示如下:
public V put(K key, V value){int hash = hash(key);int i = indexFor(hash, table.length);//在table[i]的地方添加一個包含hash,key,value信息的Entry類。 } 下面我們來看上面代碼的過程?1、line1創建了一個HashMap,所以我們來看構造函數
/*** Constructs an empty <tt>HashMap</tt> with the default initial capacity* (16) and the default load factor (0.75).*/public HashMap() {this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);}hashmap初始化和存放,取數據源碼:
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();}void init() {}存放數據的時候,如果未初始化,先進行初始化:
public V put(K key, V value) {if (table == EMPTY_TABLE) {inflateTable(threshold);//如果是空的,加載}if (key == null)return putForNullKey(value);int hash = hash(key);獲取hash值int i = indexFor(hash, table.length);生成索引for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;//遍歷已存在的Entry,如果要存入的key和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;}1.7初始化大小為16
就是一個&操作,這樣返回的值比較小適合我們的數組。
源碼很簡單,先判斷table如果是空的,就初始化數組table,接著如果key是null就單獨處理。否則的話就得到key的hash值再生成索引,這里用了indexFor()方法生成索引是因為:hash值一般都很大,是不適合我們的數組的。來看indexFor方法
/** * Returns index for hash code h. */ static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); } 因為開始table是空數組,所以會進入 inflateTable(threshold)方法,其實這個方法就是出實話數組容量,初始化長度是16,這個長度是在開始的構造方法賦值的。? 所以,現在空數組變成了長度16的數組了,就像下圖一樣。?接著由于我們的key不為null,到了獲取hash值和索引,這里假設int hash = hash(key)和int i = indexFor(hash, table.length)生成的索引i為hash=2306996,i = 4;那么就會在table索引為4的位置新建一個Entry,對應的代碼是addEntry(hash, key, value, i);到此結果如下圖:?
新建的Entry內部的變量分別是,hash,key,value,和指向下一節點的next Entry。
3、繼續來看上面的源碼line3,line3和line2一樣,而且數組不為空直接hash(key)和index。所以直接看圖了?
在這說明下:hashmap 中連表節點中保存的是key 和key對應的hashcode
4、到了line4,這里line4情況有點特殊,我們假設line4里key生成的hashcode產生的index也為4,比如hash(“three”) 的值 63281940?
hash&(15)產生的index為4。這種情況由于之前的位置已經有Entry了,所以遍歷Entry如果key和hashcode都相同,就直接替換,否則新添加一個Entry,來看一下對應源碼
public V put(K key, V 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;}}//for循環里判斷如果hash和key都一樣直接替換。modCount++;addEntry(hash, key, value, i);//沒有重復的話就addEntryreturn null;}上面代碼先判斷是否需要替換,不需要就調用了addEntry方法。來看addEntry
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);}里面又調用了createEntry
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++;//獲取當前節點,然后新建一個含有當前hash,key,value信息的一個節點,并且該節點的Entry指向了前一個Entry并賦值給table[index],成為了最新的節點Entry,同時將size加1。}到這里相信大家很清楚了。來看看圖:?
hashmap 取值過程:
我們通過hashMap.get(K key) 來獲取存入的值,key的取值很簡單了。我們通過數組的index直接找到Entry,然后再遍歷Entry,當hashcode和key都一樣就是我們當初存入的值啦。看源碼:
調用getEntry(key)拿到entry ,然后返回entry的value,來看getEntry(key)方法
final Entry<K,V> getEntry(Object key) {if (size == 0) {return null;}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;}?
思考幾個問題:
問題1、HashMap是基于key的hashcode的存儲的,如果兩個不同的key產生的hashcode一樣取值怎么辦??
看了上面的分析,你肯定知道,再數組里面有鏈表結構的Entry來實現,通過遍歷所有的Entry,比較key來確定到底是哪一個value;
問題2、HashMap是基于key的hashcode的存儲的,如果兩個key一樣產生的hashcode一樣怎么辦??
在put操作的時候會遍歷所有Entry,如果有key相等的則替換。所以get的時候只會有一個
問題3、如果我們使用我們定義的類作為hashMap的key,比如hashset 的實現,那么我們需要做什么
首先hashmap獲取對象的hashcode和equal 判斷是否相等。
-------------------------------------------------------------------------------------------------------------
HashMap 和 HashSet 內部是如何工作的?散列函數(hashing function)是什么?
HashMap?不僅是一個常用的數據結構,在面試中也是熱門話題。
Q1. HashMap 如何存儲數據?
A1. 以鍵/值對(key/value)形式存儲。你可以使用鍵(key)來存、取值。
Q2. HashMap 查詢時間的復雜度是怎樣的?
A2. 是O(n) = O(k * n)。如果 hashCode() 方法能向下面討論的那樣把數據分散到桶(bucket)中,那么平均是O(1)。
Q3. HashMap 內部是如何存儲數據的?
A3. HashMap 使用后臺數組(backing array)作為桶,并使用鏈表(linked list)存儲鍵/值對。
桶的后臺數組:如下所示
1)使用鍵(key)和值(value)將一個對象放入 map 中時,會隱式調用?hashCode()?方法,返回哈希值(hash code value),比如 123。兩個不同的鍵能夠返回一樣的哈希值。良好的哈希算法(hashing algorithm)能夠將數值分散開。在上面的例子中,我們假設 (“John”,01/01/1956) 的鍵和 (“Peter”, 01/01/1995) 的鍵返回相同的哈希值,都是?123。
2)當返回一個 hashCode,例如是 123,初始的 HashMap 容量為 10,它如何知道存儲到后臺數組(backing array)的哪個索引(index)呢?HashMap 內部會調用?hash(int ) 和 indexFor(int h, int length)?方法。這被稱為哈希函數(hashing function)。
簡要解釋下這個函數:
| 1234 | hashCode() % capacity123 % 10 = 3456 % 10 = 6 |
這表示,“hashCode = 123”存儲在備份數組的索引3上。
容量為 10 的情況下,你可能得到的數字在?0?到?9?之間。
一旦 HashMap 達到容量的 75%,也就是哈希因子(hash factor)默認值 0.75,后臺數組(backing array)的容量就會加倍,發生重散列(rehashing)為新的 20 的容量重新分配桶。
| 1234 | hashCode() % capacity123 % 20 = 3456 % 20 = 16 |
上面重散列的取模方法有一個缺陷。如果 hashCode 是負數會怎樣?負索引可不是你想要的。因此,一個改進的哈希公式會移出符號位,然后再用取模(即 %)運算符計算剩余部分。
| 12 | (123? & 0x7FFFFFFF) % 20 = 3(456 & 0x7FFFFFFF) % 20 = 16 |
這確保你得到的索引值為正數。如果你查看 Java 8 的 HashMap 源碼,它的實現使用以下方法:
a).?通過只抽取重要的低位,來防止不良離散值(poorer hashes)。
| 1234567 | static int hash(int h) {?????// 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);} |
b).?根據哈希碼(hashCode)和容量(capacity),來決定索引(index)。
| 123 | static int indexFor(int h, int length) {?????return h & (length-1);} |
實際的名稱值對(name value pairs)作為一個鍵/值對存儲在 LinkedList 中。
如上圖所示,鍵/值對以鏈表形式存儲。兩個不同的鍵可以產生一樣的 hashCode,例如123,并存儲在同一個 bucket 中,理解這點至關重要。例如,上面例子中的 “John, 01/01/1956” 和 “Peter, 01/01/1995“ 。你如何只檢索 “John, 01/01/1956” 呢?此時你的 key 所屬類的?equals()?方法會被調用。它遍歷 bucket 為 “123” 的 LinkedList 中的每個條目,使用 equals() 方法找到并檢索出鍵為 “John, 01/01/1956” 的條目。這就是在你的類中實現?hashCode()?和?equals()?方法重要性的原因。如果你使用一個現有的包裝類,如 Integer 或 String 作為鍵,它們已經實現了這兩個方法。如果你使用自己寫的類作為鍵,如 “John, 01/01/1956” 這樣含有名字和出生日期屬性的“MyKey”,你有責任正確地實現這些方法。
Q5. 為什么恰當地設置 HashMap 的初始容量(initial capacity)是最佳實踐?
A5. 這樣可以減少重散列的發生。
Q6. HashSet 內部如何存儲數據?
A6. HashSet 內部使用 HashMap 。它將元素存儲為鍵和值。(譯者注:HashSet 把存儲的值作為 key)
對于我們如果將一個自定義的類作為key存儲,我們應該做哪些事情?? 一個例子:
private class IntInt { int v,w; @Override public boolean equals(Object object) { if (this == object) return true; if (!(object instanceof IntInt)) return false; IntInt o = (IntInt) object; if (v == o.v && w == o.w) return true; return false; } @Override public int hashCode() { return v * 31 + w; } IntInt (int a, int b) { this.v = a; this.w = b; } } 首先保證我們key的唯一性,所以保證我們存儲的對象(作為key)的唯一性,自定義hashCode方法,自定義equals方法。Q7. 為 Object 實現了一個糟糕的 hashcode() 會有什么影響?
A7. 不同的對象調用 hashCode() 方法應該返回不同的值。如果不同的對象返回相同的值,會導致更多的鍵/值對存儲在同一個 bucket 中。這會降低?HashMap 和 HashSet 的性能。
參考:https://blog.csdn.net/yissan/article/details/50888070
http://www.importnew.com/21841.html
總結
以上是生活随笔為你收集整理的HashMap和HashSet的内部工作机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 就是要你懂 Java 中 volatil
- 下一篇: 163邮箱被盗怎么找回?