總體介紹之所以把HashSet和HashMap放在一起講解,,是因?yàn)槎咴贘ava里有著相同的實(shí)現(xiàn),,前者僅僅是對(duì)后者做了一層包裝,,也就是說HashSet里面有一個(gè)HashMap(適配器模式),。因此本文將重點(diǎn)分析HashMap,。 HashMap實(shí)現(xiàn)了Map接口,,允許放入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è)的過大,。 有兩個(gè)參數(shù)可以影響HashMap的性能:初始容量(inital capacity)和負(fù)載系數(shù)(load factor),。初始容量指定了初始table 的大小,負(fù)載系數(shù)用來指定自動(dòng)擴(kuò)容的臨界值,。當(dāng)entry 的數(shù)量超過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 中,,需要@Override hashCode() 和equals() 方法。 方法剖析get()get(Object key) 方法根據(jù)指定的key 值返回對(duì)應(yīng)的value ,,該方法調(diào)用了getEntry(Object key) 得到相應(yīng)的entry ,,然后返回entry.getValue() 。因此getEntry() 是算法的核心,。
算法思想是首先通過hash() 函數(shù)得到對(duì)應(yīng)bucket 的下標(biāo),,然后依次遍歷沖突鏈表,通過key.equals(k) 方法來判斷是否是要找的那個(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 getEntry(Object key) { ...... int hash = (key == null) ? 0 : hash(key); for (Entry 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)包含則直接返回,,查找過程類似于getEntry() 方法,;如果沒有找到,則會(huì)通過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 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)指針),。查找過程跟getEntry() 過程類似,。
//removeEntryForKey()final Entry removeEntryForKey(Object key) { ...... int hash = (key == null) ? 0 : hash(key); int i = indexFor(hash, table.length);//hash&(table.length-1) Entry prev = table[i];//得到?jīng)_突鏈表 Entry e = prev; while (e != null) {//遍歷沖突鏈表 Entry 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)說過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{ ...... private transient HashMap 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; } ......}
|