Item 9 Always override hashCode when you override equals HASHMAP扩展
這個item的意思是,如果兩個對象在你的equals的方法中「邏輯相等」了,那么就要讓hashCode方法處理這兩個對象的時候,也返回同樣的hash。否則的話會造成一個問題,就是如果用Map,Set存儲這種對象,會產生對不上號的情況。
比如,a1.equals(a2) 是 true,然后僅僅把a1放進map里:Map.put(a1,a1);,然后Map.get(a2);,按理說,a1,a2作為key來講是一致的,那Map.get(a2);也應該拿到a1的key對于的value吧。然后這么做返回的卻是NullPointerException。
17.11.20 review 之所以上面那種情況會報空指針異常,是因為在用戶角度,會認為equals的兩個對象自然是應該對應同一個bucket的同一個slot的,但hashmap存/取的時候,用的是key的hash,而不是key原本的值。
這需要了解Map的存儲機制。我去復習了一下當年考研時學的數據結構,當時學到了很多散列算法,比如「拉鏈法」,就是用單鏈表存放一個bucket,解決沖突;還有一種是「開放地址法」,就是發生沖突之后往后跳,找下一個可用的坑,填進去。 引用百度知道上的一個人的解釋,
哈希計算就是努力的把比較大的數據存放到相對較小的空間中。 最常見的哈希算法是取模法。 下面簡單講講取模法的計算過程。 比如:數組的長度是5。這時有一個數據是6。那么如何把這個 6存放到長度只有5的數組中呢。按照取模法,計算 6%5,結果是1,那么就把6放到數組下標是1的位置。
Map用的就是Hash算法。
HashMap 采用「Hash 算法」來決定每個元素的存儲位置。當程序執行 map.put(String,Obect)方法 時,系統將調用String的 hashCode() 方法得到其 hashCode 值——每個 Java 對象都有 hashCode() 方法,都可通過該方法獲得它的 hashCode 值。
這也就是說,map存儲key的時候,保存的實際上是key的hash,而hash的算法就由自定義的那個對象提供,沒有的話只能用Object類的默認hashCode方法了。這就是為什么上面Map.get(a2);get不到a1的value,因為a1和a2的hashCode不一樣呀,HashMap去key對應的hashCode集合里面去找a2的hashCode當然找不到了。
下面我仔細讀了一下put方法,中間我非常的confused,完全被散列搞混了,直到我百度了一下indexFor這函數的作用。
public V put(K key, V value) {if (table == EMPTY_TABLE) {inflateTable(threshold);}if (key == null)return putForNullKey(value);//把key拿去hash一下,這個方法看成是對象的hashCode()方法就行了int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);//indexFor相當于原來的把key的hash拿去mod一下。這也是為什么我們看到書上常用h% length而不是key%length。資料說h%length與h&(length-1得到的值是一致的http://blog.csdn.net/lyandyhk/article/details/51147012。//這個i就是得到的坑位了,table[i]其實應該指向一個bucketint i = indexFor(hash, table.length);for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {//table 是一個HashMapEntry<K,V>型數組,這個數組的每個元素就是一個bucket,因為HashMapEntry有個next屬性,相當于單鏈表。Object k;//e.hash == hash的意思是,傳進來的key對應的hash在這個坑位已經有了,e.key == key的意思是,不但hash相同,key也相同。因為不同的hash可能對應一個key的,畢竟hashCode()方法是人為定的。//如果key/hash都相同,就把value覆蓋吧。返回這個value。if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}//如果hash相同,key不同(不同key可能對應同一個hash),那就產生了hash沖突。按照一些資料的說法,hashmap采用的是拉鏈法,也就是在table[i]中存放一個單鏈表形式的bucket。注意下面買的modCount++,這個我以為是采用了把mod值增加來找新坑位的方法,但是這個mod其實是modify的意思,stackoverflow上說它是**The number of times this list has been structurally modified**,跟散列并無瓜葛。modCount++;addEntry(hash, key, value, i);return null;} 復制代碼那么既然是單鏈表,又是怎么準確找到對應的value的?思考一下其實很容易啦,還是按照put的方法,既然只有在key不同,hash相同的情況下才會把hash用拉鏈法存儲,那么分三步:
下面我們讀一下get方法源碼,它主要調用了getEntry:
/*** 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); //for語句第一個條件,從hash讀取到對應的bucket,在這個bucket里面遍歷,bucket的每一個元素都是一個Entry!一個entry就包含了K和V。for (HashMapEntry<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;} 復制代碼下面是我畫的一張圖,用來警醒自己,Map的Entry里,value只是一個附屬物品,散列解決沖突,保存的是key的hash,而不是value。
最后我分析一下hash的意義。為什么不同的key會對應同一個hash,因為hashCode()是人訂的。比如定義Person類,他們的id不同,但name可以相同,那么我們如果用名字來區分人,就可以在hashCode里這樣寫: return name * 47;,如此一來, map.put(person1.id,person1.name); map.put(person2.id,person2.name); 存儲在map中就遇到了key不同,hash相同的問題。所以,看能不能覆蓋一個entry,不能看key,而是要看key的hash,當然這在我們常用的String之類的數據類型里是沒有這個問題的因為不同的key一定對應不同的hash(是這樣吧。。)。到底有什么意義呢,我能想到就是節省存儲空間了,單鏈表比多開辟bucket要節省空間。
文章分析到這兒。 see also: http://xiaolu123456.iteye.com/blog/1485349 http://billyshao.iteye.com/blog/1826320 http://stackoverflow.com/questions/11833058/modcount-in-map-and-list
2017.11.29 寫了一篇新的,移步HashMap擴展/ConcurrentHashMap/LinkedHashMap
轉載于:https://juejin.im/post/5a31314e51882546d71f57a2
總結
以上是生活随笔為你收集整理的Item 9 Always override hashCode when you override equals HASHMAP扩展的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python爬虫 搜索并下载图片
- 下一篇: 对象的构造1.0