1. 程式人生 > >再談HashMap-由一個實際問題引發的對HashMap設計吐嘈

再談HashMap-由一個實際問題引發的對HashMap設計吐嘈

前言

       這一篇主要想講一講HashMap在設計上的缺陷以及在使用的過程中留下的一些隱患。也是在實際專案中可能需要注意的一些地方。比如說我下面要介紹的一個containsKey方法,以及List裡面其實有一個toArray[]方法返回的是一個Object[]陣列的,其實都不是很好用的一種設計,在泛型裡有點不倫不類的感覺。

專案背景

       事情的起因是在專案中因為漏改可能引起的一個故障想起的。這裡大概介紹一下整個過程:有這樣一種場景,定義了一個HashMap<Integer,Long>型別的變數,Key是Integer型別的,後來因為某些原因把這個變數改成了HashMap<Long
,Long>,這裡注意一下這個Key,由Integer變成Long型別,講到這裡還沒有什麼問題。問題出在哪裡,程式碼裡面有一個邏輯是需要判斷裡面的Key是否存在(也就是會呼叫containsKey(Object key)這個方法),但是這裡因為某些原因傳的值仍然是Integer型別的。問題就出來了,這樣會發現從HashMap是拿不到value的,結果就是containsKey()這樣一個方法後返回的是false。接下來會用一個例項來還原這樣我上面所說的場景:

例項

    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。

總結

       上面我從一個例項入手來講用HashMap中遇到的一些坑,反映出HashMap設計不完美之處。這樣設計其實也有一些無奈之舉,這也是因為HashMap的泛型是JDK 1.5引入的,為了讓之前的版本得到比較好的相容才保留了之前原有的介面。最後講到一個設計原則就是能把問題留在編譯器就不要把問題留在執行期,越早發現越好。再講講最上面那個專案的事情,幸好這個問題發現了,不然後果將會非常嚴重,心有餘悸。