據說面試時講自己讀過源碼會很不一樣
個人理解散列表大概就是可以通過某些函數計算出不同key在存儲的數組中對應的index,刷題的時候我們也會經常遇到需要存儲26個字母的情況,于是新建數組
int[] letter = new int[26];
letter[(char)item- 'a']++;
從而實現對字母的計數。這里字母letter對應的index可以由letter-'a'計算得到,a對應0,b對應1.......也算是一個散列表吧。。后來讀了書、 算法導論上叫他直接尋址表
在剛才特定場景下,我們知道這個數組只需要存儲26個數字,因此能夠直接新建數組,但是當全域非常大時,這樣的直接尋址表明顯會很浪費。于是推出主角散列表。
散列表中需要有散列函數將key值全域映射到存儲散列表的數組的各個位置上。
于是首要的問題就變成了
可能不同的key值會被映射到相同的位置上
plan A 鏈接法:將存儲數組變為List[ ]的形式,使用散列函數得到在數組中的index后,將當前key-value對插入該List中。數組中每個元素都是一個可長可短的鏈表。查詢一個key值最多需要遍歷最長的那個鏈表一次
plan B 開放尋址法(散列函數2.0):將散列函數的輸入擴充為key值和訪問次數,當數組對應的index中key值與要查找的不同時,下次會生成新的index。
基礎知識就到這里吧。。散列函數怎么寫。。目前我感覺是一個過于算法的問題了。。書上也講了很多
HashMap 繼承AbstractMap, 實現Map<K,V>, Cloneable, Serializable 三個接口
/**
* The number of key-value mappings contained in this map.
*/
transient int size; // 所有key-value對的個數
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;
這個table的length程序寫的一定要是2的整數冪,至于為什么之后會提到。
使用HashMapEntry類型的數組
這個類型中有key、value、next、hash。這里明顯用到了書上講的第一個方法了,使用鏈表存儲
get(Object key) 方法
對于get(Object key) 方法,會調用getForNullKey(key == null) 或getEntry(key);
在getEntry(key)中
/**
* Returns the entry associated with the specified key in the
* HashMap. Returns null if the HashMap contains no mapping
* for the key.
*/
final Entry<K,V> getEntry(Object key) {
if (size == 0) { //根本沒有元素存儲在里面
return null;
}
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
//一個感覺帶了人名的hash函數
for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) { //當前HashMapEntry的next
Object k;
if (e.hash == hash && //判斷hash值
((k = e.key) == key || (key != null && key.equals(k))))//判斷key值
return e;
}
return null;
}
如果return e說明找到了Entry<K,V>,順利返回。
這個判斷key值的環節
(k = e.key) == key 可以判斷出e.key與 當前key是否== 如果兩者本就是相同的引用,則會返回true。因此很明顯不要更改一個引用再把它多次作為key賦值。。會出事情的,像下面這樣。我們會覺得aa和bb其實不相等的。
StringBuilder aa = new StringBuilder("a");
StringBuilder bb = aa;
bb.setCharAt(0, 'b');
System.out.println(aa == bb); // true
不過真實情況下還有hash值同樣作為判斷條件
put(K key, V value) 方法
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold); // 只有一個空表
}
if (key == null)
return putForNullKey(value);
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
for (HashMapEntry<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; //找到了需要更改key值對應value
}
}
modCount++;
addEntry(hash, key, value, i); // 沒找到,增加這個key-val對。這里面會size++的
return null;
}
我們關心一下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);// length為2的整數冪
}
在這里length為2的整數冪可以保證不論hash值具體為多少,訪問數組總不會越界。(比如當length為8(1000),length-1 為(111) h & (length-1)返回h的低三位)另一個信息就是即使在同一個index下,他們的hash值不會完全相同。只是低位相同
但是這個indexFor函數還是讓人挺不放心的
int i = indexFor(hash, table.length);
當前元素的index是隨著hash值以及table.length變化的。。。這個問題還是挺尷尬的。。畢竟hash值我們認為只要函數不變每個key生成的hash值就一定不變。。但是table.length得要變的。。
當添加key-value對到達門限,會進行resize(2 * table.length);直接將table擴充一倍
/**
* Rehashes the contents of this map into a new array with a
* larger capacity. This method is called automatically when the
* number of keys in this map reaches its threshold.
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
*/
void resize(int newCapacity) {
HashMapEntry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
HashMapEntry[] newTable = new HashMapEntry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
其中ransfer(HashMapEntry[] newTable) 將原表內數據遷移,根據HashMapEntry中的hash重新計算在新表中的index,構造新的鏈表數組進行存儲。
/**
* Transfers all entries from current table to newTable.
*/
void transfer(HashMapEntry[] newTable) {
int newCapacity = newTable.length;
for (HashMapEntry<K,V> e : table) {
while(null != e) {
HashMapEntry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity); //存儲element時必須保存hash值
e.next = newTable[i]; //將當前元素放在該index的頭部,放在尾部會增加時間復雜度
newTable[i] = e;
e = next;
}
}
}