HashSet and HashMap
總體介紹
之所以把HashSet和HashMap放在一起講解,是因?yàn)槎咴贘ava里有著相同的實(shí)現(xiàn)蜓斧,前者僅僅是對(duì)后者做了一層包裝仓蛆,也就是說(shuō)HashSet里面有一個(gè)HashMap(適配器模式)。因此本文將重點(diǎn)分析HashMap挎春。
HashMap實(shí)現(xiàn)了Map接口看疙,即允許放入key
為null
的元素豆拨,也允許插入value
為null
的元素;除該類未實(shí)現(xiàn)同步外能庆,其余跟Hashtable
大致相同施禾;跟TreeMap不同,該容器不保證元素順序搁胆,根據(jù)需要該容器可能會(huì)對(duì)元素重新哈希弥搞,元素的順序也會(huì)被重新打散,因此不同時(shí)間迭代同一個(gè)HashMap的順序可能會(huì)不同丰涉。 根據(jù)對(duì)沖突的處理方式不同拓巧,哈希表有兩種實(shí)現(xiàn)方式,一種開放地址方式(Open addressing)一死,另一種是沖突鏈表方式(Separate chaining with linked lists)肛度。Java HashMap采用的是沖突鏈表方式。
從上圖容易看出投慈,如果選擇合適的哈希函數(shù)承耿,put()
和get()
方法可以在常數(shù)時(shí)間內(nèi)完成。但在對(duì)HashMap進(jìn)行迭代時(shí)伪煤,需要遍歷整個(gè)table以及后面跟的沖突鏈表加袋。因此對(duì)于迭代比較頻繁的場(chǎng)景,不宜將HashMap的初始大小設(shè)的過(guò)大抱既。
有兩個(gè)參數(shù)可以影響HashMap的性能:初始容量(inital capacity)和負(fù)載系數(shù)(load factor)职烧。初始容量指定了初始table
的大小,負(fù)載系數(shù)用來(lái)指定自動(dòng)擴(kuò)容的臨界值防泵。當(dāng)entry
的數(shù)量超過(guò)capacity*load_factor
時(shí)蚀之,容器將自動(dòng)擴(kuò)容并重新哈希。對(duì)于插入元素較多的場(chǎng)景捷泞,將初始容量設(shè)大可以減少重新哈希的次數(shù)足删。
將對(duì)象放入到HashMap或HashSet中時(shí),有兩個(gè)方法需要特別關(guān)心:hashCode()
和equals()
锁右。hashCode()方法決定了對(duì)象會(huì)被放到哪個(gè)bucket里失受,當(dāng)多個(gè)對(duì)象的哈希值沖突時(shí),equals()方法決定了這些對(duì)象是否是“同一個(gè)對(duì)象”咏瑟。所以拂到,如果要將自定義的對(duì)象放入到HashMap
或HashSet
中,需要@OverridehashCode()
和equals()
方法码泞。
方法剖析
get()
get(Object key)
方法根據(jù)指定的key
值返回對(duì)應(yīng)的value
兄旬,該方法調(diào)用了getEntry(Object key)
得到相應(yīng)的entry
,然后返回entry.getValue()
浦夷。因此getEntry()
是算法的核心辖试。 算法思想是首先通過(guò)hash()
函數(shù)得到對(duì)應(yīng)bucket
的下標(biāo),然后依次遍歷沖突鏈表劈狐,通過(guò)key.equals(k)
方法來(lái)判斷是否是要找的那個(gè)entry
罐孝。
hash(k)&(table.length-1)
等價(jià)于hash(k)%table.length
,原因是HashMap要求table.length
必須是2的指數(shù)肥缔,因此table.length-1
就是二進(jìn)制低位全是1莲兢,跟hash(k)
相與會(huì)將哈希值的高位全抹掉,剩下的就是余數(shù)了续膳。
//getEntry()方法
final Entry<K,V> getEntry(Object key) {
......
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[hash&(table.length-1)];//得到?jīng)_突鏈表
e != null; e = e.next) {//依次遍歷沖突鏈表中的每個(gè)entry
Object k;
//依據(jù)equals()方法判斷是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
put()
put(K key, V value)
方法是將指定的key, value
對(duì)添加到map
里改艇。該方法首先會(huì)對(duì)map
做一次查找,看是否包含該元組坟岔,如果已經(jīng)包含則直接返回谒兄,查找過(guò)程類似于getEntry()
方法;如果沒(méi)有找到社付,則會(huì)通過(guò)addEntry(int hash, K key, V value, int bucketIndex)
方法插入新的entry
承疲,插入方式為頭插法。
//addEntry()
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//自動(dòng)擴(kuò)容鸥咖,并重新哈希
hash = (null != key) ? hash(key) : 0;
bucketIndex = hash & (table.length-1);//hash%table.length
}
//在沖突鏈表頭部插入新的entry
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
remove()
remove(Object key)
的作用是刪除key
值對(duì)應(yīng)的entry
燕鸽,該方法的具體邏輯是在removeEntryForKey(Object key)
里實(shí)現(xiàn)的。removeEntryForKey()
方法會(huì)首先找到key
值對(duì)應(yīng)的entry
啼辣,然后刪除該entry
(修改鏈表的相應(yīng)引用)啊研。查找過(guò)程跟getEntry()
過(guò)程類似。
//removeEntryForKey()
final Entry<K,V> removeEntryForKey(Object key) {
......
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);//hash&(table.length-1)
Entry<K,V> prev = table[i];//得到?jīng)_突鏈表
Entry<K,V> e = prev;
while (e != null) {//遍歷沖突鏈表
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {//找到要?jiǎng)h除的entry
modCount++; size--;
if (prev == e) table[i] = next;//刪除的是沖突鏈表的第一個(gè)entry
else prev.next = next;
return e;
}
prev = e; e = next;
}
return e;
}
HashSet
前面已經(jīng)說(shuō)過(guò)HashSet是對(duì)HashMap的簡(jiǎn)單包裝鸥拧,對(duì)HashSet的函數(shù)調(diào)用都會(huì)轉(zhuǎn)換成合適的HashMap方法党远,因此HashSet的實(shí)現(xiàn)非常簡(jiǎn)單,只有不到300行代碼住涉。這里不再贅述麸锉。
//HashSet是對(duì)HashMap的簡(jiǎn)單包裝
public class HashSet<E>
{
......
private transient HashMap<E,Object> map;//HashSet里面有一個(gè)HashMap
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
......
public boolean add(E e) {//簡(jiǎn)單的方法轉(zhuǎn)換
return map.put(e, PRESENT)==null;
}
......
}