Effective Java之改寫equals時總要改寫hashCode

改寫equals時總要改寫hashCode

hashCode,就是哈希值,可以理解爲一個對象的標識(好的hash,能確保不同的對象有不同的hash值),Object含有hashCode方法,用來返回對象的hash值。hashCode方法多用在基於散列值的集合類,比如HashMap、HashSet和Hashtable。

下面是hashCode的約束規範,

在一個應用程序執行期間,如果一個對象的equals方法做比較所用到的信息沒有被修改的話,那麼,對該對象調用hashCode方法多次,它必須返回同一個整數。在同一個應用多次執行過程中,這個整數可以不同。

如果兩個對象根據equlas方法是相等的,那麼調用這兩個對象的hashCode方法必須產生同樣的整數結果。

如果兩個對象根據equals方法是不相等的。那麼調用這兩個對象的hashCode方法,不要求必須產生不同的整數結果。

如果你重寫了類的equals方法,那麼必須也重寫hashCode方法。否則,就違反了上述的規範。這是因爲,兩個在邏輯上相等的對象(調用equals相等),必須擁有相同的hashCode,但是根據Object的hashCode,它們僅僅是兩個對象,沒有共同的地方。所以違背了規範2. 此時,我們就要重寫hashCode方法。
實例代碼1.

public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point))
            return false;
        Point p = (Point) obj;
        return p.x == x && p.y == y;
    }

    public static void main(String[] args) {
        Point p1 = new Point(2, 5);
        Point p2 = new Point(2, 5);
        System.out.println("p1 equals p2? " + p1.equals(p2));
        System.out.println(p1.hashCode());
        System.err.println(p2.hashCode());
    }
}

在上面的例子上,我們重寫了equals方法,而且能成功判斷p1和p2是相等的,但是沒有重寫hashCode方法,所以我們調用p1.hashCode和p2.hashCode返回的值是不一樣的。

那麼,hashCode方法應該是怎麼樣的呢?編寫一個合法的hashCode並不難,比如,

    public int hashCode() {
        return 41;
    }

由於hashCode規範,並沒有要求不同的對象必須有不同的hashCode,所以我們可以給每個對象都返回一個相同的值。雖然這樣,並沒有違背hashCode的規範。但是在一些散列值存儲中(HashSet、HashMap以及HashTable),卻帶來了災難。

或許,你並不太瞭解散列值存儲,我們以HashMap爲例,HashMap提供了鍵值對(key-value)的存儲,使用範例如下,

        Point p1 = new Point(2, 5);
        Point p2 = new Point(2, 5);
        HashMap<Point, String> hm = new HashMap<Point, String>();
        hm.put(p1, "p1");
        hm.put(p2, "p2");

        System.out.println(hm.get(p1));
        System.out.println(hm.get(p2));

那麼,hashCode對於HashMap的作用是什麼呢?
我們知道,在HashMap中,不允許兩個存在兩個相同的對象,那麼如何判斷兩個對象是否相等呢?你或許會說,肯定是調用equals,是的,調用equals沒有問題,但是,如果HashMap含有數萬條數據,對每個對象都調用equals方法,效率肯定是一個問題。

此時hashCode方法的作用就體現出來了,當集合要添加新的對象時,先調用這個對象的hashCode方法,得到對應的hashcode值,實際上在HashMap的具體實現中會用一個table保存已經存進去的對象的hashcode值,如果table中沒有該hashcode值,它就可以直接存進去,不用再進行任何比較了;如果存在該hashcode值, 就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就散列其它的地址,所以這裏存在一個衝突解決的問題,這樣一來實際調用equals方法的次數就大大降低了,說通俗一點:Java中的hashCode方法就是根據一定的規則將與對象相關的信息(比如對象的存儲地址,對象的字段等)映射成一個數值,這個數值稱作爲散列值。下面這段代碼是java.util.HashMap的中put方法的具體實現:

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
}

put方法是用來向HashMap中添加新的元素,從put方法的具體實現可知,會先調用hashCode方法得到該元素的hashCode值,然後查看table中是否存在該hashCode值,如果存在則調用equals方法重新確定是否存在該元素,如果存在,則更新value值,否則將新的元素添加到HashMap中。從這裏可以看出,hashCode方法的存在是爲了減少equals方法的調用次數,從而提高程序效率。

注意,HashMap在插入的時候,判斷的是key的值是否相同

問題來了,如果我們沒有重寫hashCode方法,那麼即使對於兩個相同的對象,hashCode的結果也是不一樣的(例子1),那麼往HashMap中插入數據的時候,就會重複插入(注意,此時的Point並沒有實現hashCode方法),

        Point p1 = new Point(2, 5);
        Point p2 = new Point(2, 5);
        HashMap<Point, String> hm = new HashMap<Point, String>();
        hm.put(p1, "p1");
        hm.put(p2, "p2");

        System.out.println("HashMap size: " + hm.size());
        System.out.println(hm.get(p1));
        System.out.println(hm.get(p2));

運行程序,我們可以發現,HashMap的大小是2. p1和p2都可以從表中取出。

如果一個類是非可變的,並且計算hashCode的代價比較大,那麼應該考慮把hashCode緩存在對象內部,而不是每次都重新計算,如果對於該類的大多數對象都被用於散列鍵,那麼可以在實例被創建的時候就計算hashCode。否則的話,可以選擇遲緩初始化hashCode,一直到hashCode第一次使用才初始化。

對於前者,代碼可以如下,

    private int hashCode;
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
        /**
         * 在此處初始化hashCode
         */
    }

    @Override
    public int hashCode() {
        return hashCode();
    }

在對象初始化的時候,就計算hashCode,然後在hashCode()方法中,直接返回hashCode。

對於後者,代碼可以寫成這樣,

    private int hashCode = -1;
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public int hashCode() {
        if (hashCode() == -1) {
            /**
             *此處計算hashCode 
             */
        }
        return hashCode();
    }

這樣就可以保證,hashCode只計算一次,防止多次調用hashCode帶來的大量計算量。

好了,就寫到這裏吧,下次介紹toString的一些問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章