JDK 1.6 HashMap 源码分析
前言
? 前段時間研究了一下JDK 1.6 的 HashMap 源碼,把部份重要的方法分析一下,當然HashMap中還有一些值得研究得就交給讀者了,如有不正確之處還望留言指正。
準備
? 需要熟悉數組和鏈表這兩個基本數據結構。如果對鏈表不太熟悉的話,可以來幾道leetcode上的相關的鏈表算法題。熟悉后看 HashMap 就會快很多了。
? 基本原理:HashMap中的基本數據結構是數組加鏈表。table 是一個固定的數組。 數組里面的每個坑里面填的是一個叫Entry類。 其實就是一個固定的Entry數組。如果同一個坑里面存在兩個不同的數據,那么兩個數據就以鏈表的形式連接起來。最新的在最前面,原因是認為最新的容易經常被訪問。
構造函數
? 基本原理知道了。現在直接研究帶參數的構造函數就可以了,其他的構造函數就是調用該方法。
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);// Find a power of 2 >= initialCapacityint capacity = 1;while (capacity < initialCapacity)capacity <<= 1;this.loadFactor = loadFactor;threshold = (int)(capacity * loadFactor);table = new Entry[capacity];init();}? MAXIMUM_CAPACITY = 1 << 30 2的30次方1073741824,也就是HashMap中table數組的大小不能超過該數字。 從上面代碼可以看出來table的坑位只能是2的冪次方。如果你傳入的initialCapacity為7 那么其實table 的大小為8; 也就是table的大小為傳入進來的initialCapacity的數值大于該大小的2的冪次方。threshold 為他的閾值也就是 HashMap 的真正大小不能超過該值,超過了就進行擴容操作。 如果table數組的大小為16時。用它默認的擴容因子0.75f。那么他的閾值就是12。 也就是 table數據,數組中的加上鏈表的不能超過12。
我們看看第二個構造函數。參數為一個Map 我這里順便把HashMap中的嵌套類Entry類說一下。可以自己再源碼上觀看。
put方法
? 為什么要從put方法研究起呢。因為HashMap中最常用得就是put方法。而且里面還涉及到擴容操作。如果把這些看懂了還是會很舒服得。
public V put(K key, V value) {if (key == null)// 如果key為null的話 直接添加到table[0]的位置 for 循環 table[0]上的元素。如果有元素的話 查看該元素的key是不是null 如果是的話 就更新value值,直到table[0]這個鏈表結束。 如果結束后還是沒有的話,就把為null的key 對應的value 頭插法 插入頭部。 可以查看 putForNullKey(value) 方法。return putForNullKey(value);// 計算Hash值 int hash = hash(key.hashCode());// 取key的Hash值得 二進制數得后幾位。 如果key得hash為1101011 。而table這個數組得大小一直都是2的冪次方。 indexFor()方法做的事 key的hash與table.length-1做&運算。假如table數組的大小為16,也就是 11011011 & 1111 會等于 1011 。這個方法的意義也就是只要你得Hash值是隨機的,碰撞性低,那么你在table中位置也就是 碰撞低的。int i = indexFor(hash, table.length);// 查詢該table[i] 位置上的鏈表。for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;// 如果 key相等 那么就更新 否則 下一位。。。。 直至結束。if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}// 修改次數加一modCount++;// 頭插法 并看size是都大于閾值了,如果大于就要擴容了。addEntry(hash, key, value, i);return null;}void addEntry(int hash, K key, V value, int bucketIndex) {Entry<K,V> e = table[bucketIndex];table[bucketIndex] = new Entry<K,V>(hash, key, value, e);if (size++ >= threshold)//擴容操作 2倍擴容resize(2 * table.length);}// 擴容方法 參數為擴容大小void resize(int newCapacity) {Entry[] oldTable = table;int oldCapacity = oldTable.length;if (oldCapacity == MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return;}// 創建一個新得數組 名字叫做newTable length為 newCapacityEntry[] newTable = new Entry[newCapacity];// 擴容操作transfer(newTable);// 重新賦值 table = newTable;// 閾值threshold = (int)(newCapacity * loadFactor);}// 擴容操作void transfer(Entry[] newTable) {// 將原先的table數組 賦值給 srcEntry[] src = table;int newCapacity = newTable.length;// 逐個操作 從 src[0] 位置上的Entry 開始for (int j = 0; j < src.length; j++) {// 將src[j]的值給 e變量。Entry<K,V> e = src[j];// 對這個e 鏈表進行往下操作if (e != null) {// 清空src[j] = null;do {//e 的下面一位 其實就是 next 后移 (這里如果兩個線程同時在這里操作的話,A線程在這里執行這條語句后掛起的話,B線程完成擴容操作后,A線程再喚醒時,有可能發生循環鏈表。然后使用get方法的時候,導致死循環,cpu利用100%)Entry<K,V> next = e.next;// 對e 重新定位。int i = indexFor(e.hash, newCapacity);// 將e.next 從e 斷開 并把e.next的值 指到 newTable[i]的值e.next = newTable[i];// 將 e 賦值給 newTable[i] newTable[i] = e;// e 往后移e = next;} while (e != null);}}}舒服了舒服了。 如果想看怎么發生死循環的可以看小灰的文章 高并發下的HashMap 。
get方法
get方法相對而言就比較簡單了。
public V get(Object key) {if (key == null)// 直接查詢table[0] 上鏈表key為 null的值return getForNullKey();// 定位table上的位置int hash = hash(key.hashCode());// 鏈表的查詢 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.equals(k)))return e.value;}return null;}remove方法
remove方法相對而言,只要你會鏈表的刪除操作,就很好理解了。如果有不明白的可以。將鏈表這個數據結構好好學習一下。
public V remove(Object key) {// 移除元素方法Entry<K,V> e = removeEntryForKey(key);return (e == null ? null : e.value);}// 這里其實就是鏈表的刪除操作 。final Entry<K,V> removeEntryForKey(Object key) {int hash = (key == null) ? 0 : hash(key.hashCode());// 定位位置int i = indexFor(hash, table.length);// 將table[i] 這個鏈表賦值給prev Entry<K,V> prev = table[i];// prev 賦值給 eEntry<K,V> e = prev;while (e != null) {// 下面一位Entry<K,V> next = e.next;Object k;// key是否相等if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) {modCount++;size--;// 如果要刪除的時table[i]的頭部數據 if (prev == e)// table[i] 等于next 刪除頭部table[i] = next;else// 否則 刪除這個 prev.next = next;e.recordRemoval(this);return e;}prev = e;e = next;}return e;}總結
? HashMap中的學問,遠不止這些。 其中還涉及到設計模式,迭代器等等。上面這些只是常用的。個人非常推薦把數組和鏈表這個兩個非常基礎的數據結構好好練習一下。雖然說早就把JDK 1.6的HashMap 源碼看了一下,順便把 ConcurrentHashMap中的一些源碼也看了。但是寫下來的時候,再看一遍,印象果然深刻多了。先把1.6的看了,在看1.8的吧。
轉載于:https://www.cnblogs.com/Krloypower/p/10675686.html
總結
以上是生活随笔為你收集整理的JDK 1.6 HashMap 源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Runtime Errors:CALL_
- 下一篇: Django中Ajax提交数据的CSRF