再談HashMap-由一個實際問題引發的對HashMap設計吐嘈
阿新 • • 發佈:2019-01-03
前言
這一篇主要想講一講HashMap在設計上的缺陷以及在使用的過程中留下的一些隱患。也是在實際專案中可能需要注意的一些地方。比如說我下面要介紹的一個containsKey方法,以及List裡面其實有一個toArray[]方法返回的是一個Object[]陣列的,其實都不是很好用的一種設計,在泛型裡有點不倫不類的感覺。專案背景
事情的起因是在專案中因為漏改可能引起的一個故障想起的。這裡大概介紹一下整個過程:有這樣一種場景,定義了一個HashMap<Integer,Long>型別的變數,Key是Integer型別的,後來因為某些原因把這個變數改成了HashMap<Long例項
public static void main(String[] args) { Map<Long,Long> map = new HashMap<Long,Long>(); map.put(new Long(1), new Long(1));//這裡的key是Long型別 Integer key = new Integer(1); boolean result = map.containsKey(key);//這裡的key是Integer型別 System.out.println(result); }
先看一看輸出結果,沒錯,是false,沒有任何疑問,這裡可以看看HashMap是如何查詢value的:HashMap會先比較key的hashCode,然後會直接比較這個key值是否相等(== || equals),這裡可以參考我另一篇文章《HashMap原始碼分析》。下面我把HashMap獲取Entry的方法給貼出來,重點看一下里面查詢Key的方式。
final Entry<K,V> getEntry(Object key) { int hash = (key == null) ? 0 : 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 != null && key.equals(k)))) return e; } return null; }
接著我把Integer和Long的equals方法貼出來,看看這兩個類對於equals方法的實現,這樣就可以比較清晰的知道為什麼在HashMap中containsKey如果裡面本來存的key是Integer型別,而呼叫containsKey傳的值是Long型別後得到的結果是false。
Integer的equals方法
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
Long的equals方法
public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}
從上面Integer和Long的equals方法可以得出如果比較的兩個值都是Integer型別或者Long型別,只要它們的值(intValue/longValue相等),那麼equalsr後的結果是true,但是Integer型別和Long型別的兩個物件進行比較,即使值(intValue/longValue)相同,equals後是false。這就可以解釋上面的一個結論為什麼是false了。
吐槽
講到這裡估計有些人有疑問了。上面講的有什麼問題嗎?這些大家都知道啊。這不是最基本的知識點嗎?等等,好戲才剛剛開始。這裡注意看一下HashMap中containsKey這個方法(下面是原始碼): public boolean containsKey(Object key) {
return getEntry(key) != null;
}
傳進去的引數是Object型別,這個方法的設定就有點不倫不類,因為HashMap使用了泛型,按照我們一般人的理解,應該會是這樣(下面是我改進的方法):
public boolean containsKey(K key) {
return getEntry(key) != null;
}
我所做的改動僅僅是把裡面的Object換成了泛型K。這樣的設計有什麼好處:再回到上面的那個例項,如果containsKey方法值的是泛型,那麼在傳值的時候就會約束成Long型別,是不允許傳Integer值,這樣在編譯的時候就會拋錯,而不會給整個邏輯留下一個不可預知的隱患,也符合越早發現問題越好的一個設計原則:能把問題留在編譯期就不要把問題帶到執行期甚至不可發現。這也是為什麼要用泛型進行約束,除了程式碼規範外,另外一個好處就是可以消除因為編碼引入的bug。