一道面試題看 HashMap 的儲存方式
我們公司招人喜歡問演算法題和一些基礎知識。今天我們一個面試官在面試候選人之前在辦公室對我們說他準備問一個這樣的問題:
在HashMap 中存放的一系列鍵值對,其中鍵為某個我們自定義的型別。放入 HashMap 後,我們在外部把某一個 key 的屬性進行更改,然後我們再用這個 key 從 HashMap 裡取出元素,這時候 HashMap 會返回什麼?
我們辦公室幾個人答案都不一致,有的說返回null,有的說能正常返回value。但不論答案是什麼都沒有確鑿的理由。我覺得這個問題挺有意思的,就寫了程式碼測試。結果是返回null。需要說明的是我們自定義的類重寫了 hashCode 方法。我想這個結果還是有點意外的,因為我們知道 HashMap 存放的是引用型別,我們在外面把 key 更新了,那也就是說 HashMap 裡面的 key 也更新了,也就是這個 key 的 hashCode 返回值也會發生變化。這個時候 key 的 hashCode 和 HashMap 對於元素的 hashCode 肯定一樣,equals也肯定返回true,因為本來就是同一個物件,那為什麼不能返回正確的值呢?
先來看看一段測試程式碼:
先解釋一下測試程式碼做到事。定義了一個person類,就兩個屬性。重寫了 hashCode 方法,還有一套geter和seter,沒什麼特別。測試類裡面先建立了三個person物件作為 key 。列印各個 key 的 hashCode 值。然後三個元素放到 HashMap ,接著更新其中一個 key 的name屬性,最後去取這個 key 的value。
public class Person { private String name; private int height; @Override public int hashCode() { System.out.println(this.name + ": HashCode() invoked!"); return this.name.hashCode() + this.height; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } @Override public String toString() { return "Name:" + this.name + "; height:" + this.height; } }
import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; public class HashmapTest { public static void main(String[] args) { Map<Person, String> testMap = new HashMap<Person, String>(); Person p1 = new Person(); p1.setName("Jakie"); p1.setHeight(165); Person p2 = new Person(); p2.setName("Jerry"); p2.setHeight(175); Person p3 = new Person(); p3.setName("Torres"); p3.setHeight(160); System.out.println(p1 + ";hashcode:" + p1.hashCode() + "\n"); System.out.println(p2 + ";hashcode:" + p2.hashCode() + "\n"); System.out.println(p3 + ";hashcode:" + p3.hashCode() + "\n"); System.out.println("************************"); System.out.println("putting object into map"); testMap.put(p1, "p1"); testMap.put(p2, "p2"); testMap.put(p3, "p3"); System.out.println("************************"); p2.setName("Jerry is now kelly"); System.out.println("P2 hashcode after update:"); System.out.println(p2 + ";hashcode:" + p2.hashCode() + "\n"); System.out.println("**************************"); System.out.println("Hash Code of elements in HashMap"); for (Entry<Person, String> entry : testMap.entrySet()) { System.out.println(entry.getKey() + ":" + entry.getValue() + ":" + entry.getKey().hashCode()); System.out.println(); if (entry.getKey().getName().equals("Jakie")) { System.out.println("Jakie in map is the original jakie " + (entry.getKey() == p1)); } else if (entry.getKey().getName().equals("Jerry is now kelly")) { System.out .println("Jerry is now kelly in map is the original Jerry " + (entry.getKey() == p2)); } } System.out.println("**********************"); String p = testMap.get(p2); System.out.println("Final Result:" + p); } }
輸出:
Name:Jakie; height:165;hashcode:71336629 Name:Jerry; height:175;hashcode:71462829 Name:Torres; height:160;hashcode:-1784098647 ************************ putting object into map ************************ P2 hashcode after update: Name:Jerry is now kelly; height:175;hashcode:-711681872 ************************** Hash Code of elements in HashMap Name:Jerry is now kelly; height:175:p2:-711681872 Jerry is now kelly in map is the original Jerry true Name:Jakie; height:165:p1:71336629 Jakie in map is the original jakie true Name:Torres; height:160:p3:-1784098647 ********************** Final Result:null
從輸出我們可以知道, key 更新後 hashCode 確實更新了。而且 HashMap 裡面的物件就是我們原來的物件。最後的結果是null。
我們來看一下 HashMap 的get方法原始碼:
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); 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.equals(k))) return e.value; } return null; }
可以看到先取得了一個table,這個table實際上是個陣列。然後在table裡面找對應 key 的value。找的標準就是hash等於傳入引數的hash, 並且滿足另外兩個條件之一:k = e.key,也就是說他們是同一個物件,或者傳入的 key 的equal目標的 key 。我們的問題出在那個hash(key.hashCode()),可以看到 HashMap 在儲存元素時是把 key 的 hashCode 再做了一次hash。得到的hash將最終作為元素儲存位置的依據。對應到我們的情況:第一次儲存時,hash函式採用key.hashCode作為引數得到了一個值,然後根據這個值把元素存到了某個位置。
當我們再去取元素的時候,key.hashCode的值已經出現了變化,所以這裡的hash函式結果也發生了變化,所以當它嘗試去獲得這個 key 的儲存位置時就不能得到正確的值,導致最終找不到目標元素。要想能正確返回,很簡單,把Person類的 hashCode 方法改一下,讓它的 hashCode 不依賴我們要修改的屬性,但實際開發中肯定不能這麼幹,我們總是希望當兩個物件的屬性不完全相同時能返回不同的 hashCode 值。所以結論就是當把物件放到 HashMap 後,不要去修改 key 的屬性。
以上都是很基礎的東西,但或許我們很多時候都沒注意到,瞭解這些基礎可以避免一些很詭異的bug。純屬拋磚引玉,如有謬誤請海涵和指出。
本文作者: 伯樂線上 - 梧桐
相關文章
- 一道以前看過的面試題面試題
- 一道關於:ArrayList、Vector、LinkedList的儲存效能和特性 的面試題面試題
- HashMap面試題HashMap面試題
- 一道面試題面試題
- 一道面試題的分析面試題
- java面試題-HashMap的工作原理Java面試題HashMap
- 一道面試題引起的思考面試題
- 分享一道昨天的面試題面試題
- 一道sql面試題的解答SQL面試題
- mysql一道面試題MySql面試題
- HashMap常見面試題整理HashMap面試題
- SAS 數值儲存方式和精度問題
- 日期的正確儲存方式
- 分享一道Go面試必考的題Go面試
- 一道面試題引發的“血案”面試題
- 一道面試題引發的思考面試題
- 一道sql面試題的求解方法SQL面試題
- android 儲存方式Android
- 【理解】一道 JS 面試題JS面試題
- 一道騷面試題目面試題
- 解析一道JS面試題JS面試題
- 從根源揭祕HashMap的資料儲存過程HashMap儲存過程
- Java HashMap原理及內部儲存結構JavaHashMap
- 從itpub看來的一道組合題
- 資料儲存的三種方式
- Java常見的本地儲存方式Java
- JavaScript本地儲存的方式有哪些JavaScript
- 一道與 for 相關的字串面試題字串面試題
- 關於PHP字串的一道面試題PHP字串面試題
- 聊聊一道簡單的javascript面試題JavaScript面試題
- 從 Google 的一道面試題說起·Go面試題
- 一道 JS 面試題引發的思考JS面試題
- 一道面試題目引發的思考面試題
- 資料儲存的方式(只說三種方式)
- 一道面試題:去重排序面試題排序
- iOS 每天一道面試題iOS面試題
- 一道柯里化面試題面試題
- 一道java面試基礎題Java面試