在集合系列的第一章,我們瞭解到,Map 的實現類有 HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、Hashtable、Properties等等。html
應該有不少人不知道 IdentityHashMap 的存在,其中不乏工做不少年的 Java 開發者,本文主要從數據結構和算法層面,探討 IdentityHashMap 的實現。java
IdentityHashMap 的數據結構很簡單,底層實際就是一個 Object 數組,可是在存儲上並無使用鏈表來存儲,而是將 K 和 V 都存放在 Object 數組上。算法
當添加元素的時候,會根據 Key 計算獲得散列位置,若是發現該位置上已經有改元素,直接進行新值替換;若是沒有,直接進行存放。當元素個數達到必定閾值時,Object 數組會自動進行擴容處理。數組
打開 IdentityHashMap 的源碼,能夠看到 IdentityHashMap 繼承了AbstractMap 抽象類,實現了Map接口、可序列化接口、可克隆接口。數據結構
public class IdentityHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, java.io.Serializable, Cloneable { /**默認容量大小*/ private static final int DEFAULT_CAPACITY = 32; /**最小容量*/ private static final int MINIMUM_CAPACITY = 4; /**最大容量*/ private static final int MAXIMUM_CAPACITY = 1 << 29; /**用於存儲實際元素的表*/ transient Object[] table; /**數組大小*/ int size; /**對Map進行結構性修改的次數*/ transient int modCount; /**key爲null所對應的值*/ static final Object NULL_KEY = new Object(); ...... }
能夠看到類的底層,使用了一個 Object 數組來存放元素;在對象初始化時,IdentityHashMap 容量大小爲64
;數據結構和算法
public IdentityHashMap() { //調用初始化方法 init(DEFAULT_CAPACITY); }
private void init(int initCapacity) { //數組大小默認爲初始化容量的2倍 table = new Object[2 * initCapacity]; }
put 方法是將指定的 key, value 對添加到 map 裏。該方法首先會對map作一次查找,經過==
判斷是否存在key
,若是有,則將舊value
返回,將新value
覆蓋舊value
;若是沒有,直接插入,數組長度+1
,返回null
。ide
源碼以下:函數
public V put(K key, V value) { //判斷key是否爲空,若是爲空,初始化一個Object爲key final Object k = maskNull(key); retryAfterResize: for (;;) { final Object[] tab = table; final int len = tab.length; //經過key、length獲取數組小編 int i = hash(k, len); //循環遍歷是否存在指定的key for (Object item; (item = tab[i]) != null; i = nextKeyIndex(i, len)) { //經過==判斷,是否數組中是否存在key if (item == k) { V oldValue = (V) tab[i + 1]; //新value覆蓋舊value tab[i + 1] = value; //返回舊value return oldValue; } } //數組長度 +1 final int s = size + 1; //判斷是否須要擴容 if (s + (s << 1) > len && resize(len)) continue retryAfterResize; //更新修改次數 modCount++; //將k加入數組 tab[i] = k; //將value加入數組 tab[i + 1] = value; size = s; return null; } }
maskNull 函數,判斷 key 是否爲空測試
private static Object maskNull(Object key) { return (key == null ? NULL_KEY : key); }
hash 函數,經過 key 獲取 hash 值,結合數組長度經過位運算獲取數組散列下標code
private static int hash(Object x, int length) { int h = System.identityHashCode(x); // Multiply by -127, and left-shift to use least bit as part of hash return ((h << 1) - (h << 8)) & (length - 1); }
nextKeyIndex 函數,經過 hash 函數計算獲得的數組散列下標,進行加2;由於一個 key、value 都存放在數組中,因此一個 map 對象佔用兩個數組下標,因此加2。
private static int nextKeyIndex(int i, int len) { return (i + 2 < len ? i + 2 : 0); }
resize 函數,經過數組長度,進行擴容處理,擴容以後的長度爲當前長度的2倍
private boolean resize(int newCapacity) { //擴容後的數組長度,爲當前數組長度的2倍 int newLength = newCapacity * 2; Object[] oldTable = table; int oldLength = oldTable.length; if (oldLength == 2 * MAXIMUM_CAPACITY) { // can't expand any further if (size == MAXIMUM_CAPACITY - 1) throw new IllegalStateException("Capacity exhausted."); return false; } if (oldLength >= newLength) return false; Object[] newTable = new Object[newLength]; //將舊數組內容轉移到新數組 for (int j = 0; j < oldLength; j += 2) { Object key = oldTable[j]; if (key != null) { Object value = oldTable[j+1]; oldTable[j] = null; oldTable[j+1] = null; int i = hash(key, newLength); while (newTable[i] != null) i = nextKeyIndex(i, newLength); newTable[i] = key; newTable[i + 1] = value; } } table = newTable; return true; }
get 方法根據指定的 key 值返回對應的 value。一樣的,該方法會循環遍歷數組,經過==
判斷是否存在key
,若是有,直接返回value,由於 key、value 是相鄰的存儲在數組中,因此直接在當前數組下標+1
,便可獲取 value;若是沒有找到,直接返回null
。
值得注意的地方是,在循環遍歷中,是經過==
判斷當前元素是否與key
相同,若是相同,則返回value
。我們都知道,在 java 中,==
對於對象類型參數,判斷的是引用地址
,確切的說,是堆內存地址,因此,這裏判斷的是key
的引用地址是否相同,若是相同,則返回對應的 value;若是不相同,則返回null
。
源碼以下:
public V get(Object key) { Object k = maskNull(key); Object[] tab = table; int len = tab.length; int i = hash(k, len); //循環遍歷數組,直到找到key或者,數組爲空爲值 while (true) { Object item = tab[i]; //經過==判斷,當前數組元素與key相同 if (item == k) return (V) tab[i + 1]; //數組爲空 if (item == null) return null; i = nextKeyIndex(i, len); } }
remove 的做用是經過 key 刪除對應的元素。該方法會循環遍歷數組,經過==
判斷是否存在key
,若是有,直接將key
、value
設置爲null
,對數組進行從新排列,返回舊 value。
源碼以下:
public V remove(Object key) { Object k = maskNull(key); Object[] tab = table; int len = tab.length; int i = hash(k, len); while (true) { Object item = tab[i]; if (item == k) { modCount++; //數組長度減1 size--; V oldValue = (V) tab[i + 1]; //將key、value設置爲null tab[i + 1] = null; tab[i] = null; //刪除該元素後,須要把原來有衝突日後移的元素移到前面來 closeDeletion(i); return oldValue; } if (item == null) return null; i = nextKeyIndex(i, len); } }
closeDeletion 函數,刪除該元素後,須要把原來有衝突日後移的元素移到前面來,對數組進行重寫排列;
private void closeDeletion(int d) { // Adapted from Knuth Section 6.4 Algorithm R Object[] tab = table; int len = tab.length; Object item; for (int i = nextKeyIndex(d, len); (item = tab[i]) != null; i = nextKeyIndex(i, len) ) { int r = hash(item, len); if ((i < r && (r <= d || d <= i)) || (r <= d && d <= i)) { tab[d] = item; tab[d + 1] = tab[i + 1]; tab[i] = null; tab[i + 1] = null; d = i; } } }
IdentityHashMap
的實現不一樣於HashMap
,雖然也是數組,不過IdentityHashMap
中沒有用到鏈表,解決衝突的方式是計算下一個有效索引,而且將數據key
和value
緊挨着存在map
中,即table[i]=key
、table[i+1]=value
;
IdentityHashMap
容許key
、value
都爲null
,當key
爲null
的時候,默認會初始化一個Object
對象做爲key
;
IdentityHashMap
在保存、刪除、查詢數據的時候,以key
爲索引,經過==
來判斷數組中元素是否與key
相同,本質判斷的是對象的引用地址,若是引用地址相同,那麼在插入的時候,會將value
值進行替換;
IdentityHashMap 測試例子:
public static void main(String[] args) { Map<String, String> identityMaps = new IdentityHashMap<String, String>(); identityMaps.put(new String("aa"), "aa"); identityMaps.put(new String("aa"), "bb"); identityMaps.put(new String("aa"), "cc"); identityMaps.put(new String("aa"), "cc"); //輸出添加的元素 System.out.println("數組長度:"+identityMaps.size() + ",輸出結果:" + identityMaps); }
輸出結果:
數組長度:4,輸出結果:{aa=aa, aa=cc, aa=bb, aa=cc}
儘管key
的內容是同樣的,可是key
的堆地址都不同,因此在插入的時候,插入了4條記錄。
一、JDK1.7&JDK1.8 源碼
二、簡書 - 騎着烏龜去看海 - IdentityHashMap源碼解析
三、博客園 - leesf - IdentityHashMap源碼解析
做者:炸雞可樂
出處:www.pzblog.cn