JDK1.7和JDK1.8HashMap差別很大。
本次主要討論JDK1.7的HashMap。
1、原理(原理采用 數組+鏈表存儲數據 原理如下圖)
對於一個元素的插入,首先要考慮它在數組中的位置。 常用散列函數存放其位置。
常用的散列函數的設計 由以下幾種
1、直接賦值法(是多少就放數組中那個位置 如100就放數組中第100個位置。)
2、數據分析法 (根據一個數據進行分析如:身份證430521199810143314 直接取后四位3314 存放數組位置3314 )
3、平方取中法(一個數據進行平方進行存放)
4、取余法(如 上圖中的28 28%數組長度 8 -》》4 所以存放數組第四個位置。)
HashMap中也采用的是取余法。
Hash算法有一個弊端,會造成沖突。也叫Hash沖突。
如果二個元素取余結果相同(如上圖中的 28、4 )這時候就會造成Hash沖突
Hash沖突有幾種常見的解決方法。
1:開發定值法:對於沖突的元素往數組 前移或者后移一位。或者移動多個位置(缺點,容易導致數據集合在一塊)
2:鏈表法。
HashMap采用的是鏈表法,把Hash沖突的數據 用鏈表依次存儲。
2、源碼分析(只關注常見的方法和成員變量)
上面說到 HashMap主要采用 數組加上鏈表 存儲數據。
//數組
transient Entry<K,V>[] table;
//鏈表 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; }
一些關鍵的成員變量
//實際存儲的key-value鍵值對的個數 transient int size; //負載因子,代表了table的填充度有多少,默認是0.75 final float loadFactor; //threshold一般為 capacity*loadFactory。HashMap在進行擴容時需要參考threshold int threshold;
HashMap有四個構造函數
public HashMap(int initialCapacity, float loadFactor) { //此處對傳入的初始容量進行校驗,最大不能超過MAXIMUM_CAPACITY = 1<<30(230) 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();//init方法在HashMap中沒有實際實現 }
initialCapacity默認為16,loadFactory默認為0.75
接下來看一下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; }
inflateTable方法
private void inflateTable(int toSize) { int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次冪 toSize為12 返回 的是16 toSize為16返回的為16 toSize為 17返回的是32 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此處為threshold賦值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值 table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }
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); }
/** * 對hash取模 返回數組下標 等同於 h%(length) 只不過效率更高。 */ static int indexFor(int h, int length) { return h & (length-1); }
h&(length-1)保證獲取的index一定在數組范圍內,舉個例子,默認容量16,length-1=15,h=25 換成二進制計算為9。
addEntry
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);//當size超過臨界閾值threshold,並且即將發生哈希沖突時進行擴容 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
數組擴容
resize()
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]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
我們先來看看transfer這個方法
如果數組進行擴容,數組長度發生變化,而存儲位置 index = h&(length-1),index也可能會發生變化,需要重新計算index
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //for循環中的代碼,逐個遍歷鏈表,重新計算索引位置,將老數組數據復制到新數組中去 for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> 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;//鏈表向后 } } }
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(); }
getEntry方法
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } //通過key的hashcode值計算hash值 int hash = (key == null) ? 0 : hash(key); //根據key 找到對應存儲鏈表 遍歷鏈表 根據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; }