Java equals()和hashCode()

1、引言

        Java技術面試的時候咱們總會被問到這類的問題:重寫equals()方法爲何必定要重寫hashCode()方法?兩個不相等的對象能夠有相同的散列碼嗎?... 曾經對這些問題我也感到很困惑。 equals()和hasCode()方法是Object類中的兩個基本方法。在實際的應用程序中這兩個方法起到了很重要的做用,好比在集合中查找元素,咱們常常會根據實際須要重寫這兩個方法。 下面就對equals()方法和hashCode()方法作一個詳細的分析說明,但願對於有一樣疑惑的人有些許幫助。 java

2、重寫equals()方法

      一、爲何要重寫equals()方法

      咱們都知道比較兩個對象引用是否相同用==運算符,且只有當兩個引用都引用相同對象時,使用==運算符纔會返回true。而比較相同類型的兩個不一樣對象的內容是否相同,可使用equals()方法。可是Object中的equals()方法只使用==運算符進行比較,其源碼以下: 程序員

public boolean equals(Object obj) {
   return (this == obj);
}

       若是咱們使用Object中的equals()方法判斷相同類型的兩個不一樣對象是否相等,則只有當兩個對象引用都引用同一對象時才被視爲相等。那麼就存在這樣一個問題:若是咱們要在集合中查找該對象, 在咱們不重寫equals()方法的狀況下,除非咱們仍然持有這個對象的引用,不然咱們永遠找不到相等對象。 面試

        代碼清單-1
算法

List<String> test = new ArrayList<String>();
test.add("aaa");
test.add("bbb");
System.out.println(test.contains("bbb"));

          分析: ArrayList遍歷它全部的元素並執行 "bbb".equals(element)來判斷元素是否和參數對象"bbb"相等。最終是由String類中重寫的equals()方法來判斷兩個字符串是否相等

      二、怎樣實現正確的equals()方法

       首先,咱們須要遵照Java API文檔中equals()方法的約定,以下: 數組

自反性:對於任何非空引用值 x,x.equals(x) 都應返回 true。 
對稱性:對於任何非空引用值 x 和 y,當且僅當 y.equals(x) 返回 true 時,x.equals(y) 才應返回 true。
 傳遞性:對於任何非空引用值 x、y 和 z,若是 x.equals(y) 返回 true,而且 y.equals(z) 返回 true,那麼 x.equals(z) 應返回 true。 
一致性:對於任何非空引用值 x 和 y,屢次調用 x.equals(y) 始終返回 true 或始終返回 false,前提是對象上 equals 比較中所用的信息沒有被修改。 
對於任何非空引用值 x,x.equals(null) 都應返回 false。
            其次,當咱們重寫equals()方法時 , 不一樣類型的屬性比較方式不一樣,以下:

屬性是Object類型, 包括集合: 使用equals()方法。
屬性是類型安全的枚舉: 使用equals()方法或==運算符(在這種狀況下,它們是相同的)。
屬性是可能爲空的Object類型: 使用==運算符和equals()方法。
屬性是數組類型: 使用Arrays.equals()方法。
屬性是除float和double以外的基本類型: 使用==運算符。
屬性是float: 使用Float.floatToIntBits方法轉化成int,而後使用 ==運算符。
屬性是double: 使用Double.doubleToLongBits方法轉化成long , 而後使用==運算符。

       值得注意的是,若是屬性是基本類型的包裝器類型(Integer, Boolean等等), 那麼equals方法的實現就會簡單一些,由於只須要遞歸調用equals()方法。 安全

      在equals()方法中,一般先執行最重要屬性的比較,即最有可能不一樣的屬性先進行比較。可使用短路運算符&&來最小化執行時間。  數據結構

      三、一個簡單的Demo     

        代碼清單-2 ide

/**
* 根據上面的策略寫的一個工具類
*
*/
public final class EqualsUtil {

    public static boolean areEqual(boolean aThis, boolean aThat) {
        return aThis == aThat;
    }

    public static boolean areEqual(char aThis, char aThat) {
        return aThis == aThat;
    }

    public static boolean areEqual(long aThis, long aThat) {
        //注意byte, short, 和 int 能夠經過隱式轉換被這個方法處理
        return aThis == aThat;
    }

    public static boolean areEqual(float aThis, float aThat) {
        return Float.floatToIntBits(aThis) == Float.floatToIntBits(aThat);
    }

    public static boolean areEqual(double aThis, double aThat) {
        return Double.doubleToLongBits(aThis) == Double.doubleToLongBits(aThat);
    }

    /**
     * 可能爲空的對象屬性
     * 包括類型安全的枚舉和集合, 可是不包含數組
     */
    public static boolean areEqual(Object aThis, Object aThat) {
        return aThis == null ? aThat == null : aThis.equals(aThat);
    }
}
          Car 類使用 EqualsUtil 來實現其 equals ()方法 .

      代碼清單-3 工具

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

public final class Car {

    private String fName;
    private int fNumDoors;
    private List<String> fOptions;
    private double fGasMileage;
    private String fColor;
    private Date[] fMaintenanceChecks;

    public Car(String aName, int aNumDoors, List<String> aOptions, double aGasMileage, String aColor, Date[] aMaintenanceChecks) {
        fName = aName;
        fNumDoors = aNumDoors;
        fOptions = new ArrayList<String>(aOptions);
        fGasMileage = aGasMileage;
        fColor = aColor;
        fMaintenanceChecks = new Date[aMaintenanceChecks.length];
        for (int idx = 0; idx < aMaintenanceChecks.length; ++idx) {
            fMaintenanceChecks[idx] = new Date(aMaintenanceChecks[idx].getTime());
        }
    }

    @Override
    public boolean equals(Object aThat) {
        //檢查自身
        if (this == aThat) return true;

        //這裏使用instanceof 而不是getClass有兩個緣由
        //1. 若是須要的話, 它能夠匹配任何超類型,而不只僅是一個類;
        //2. 它避免了冗餘的校驗"that == null" , 由於它已經檢查了null - "null instanceof [type]" 老是返回false
        if (!(aThat instanceof Car)) return false;
        //上面一行的另外一種寫法 :
        //if ( aThat == null || aThat.getClass() != this.getClass() ) return false;

        //如今轉換成本地對象是安全的(不會拋出ClassCastException)
        Car that = (Car) aThat;

        //逐個屬性的比較
        return
                EqualsUtil.areEqual(this.fName, that.fName) &&
                        EqualsUtil.areEqual(this.fNumDoors, that.fNumDoors) &&
                        EqualsUtil.areEqual(this.fOptions, that.fOptions) &&
                        EqualsUtil.areEqual(this.fGasMileage, that.fGasMileage) &&
                        EqualsUtil.areEqual(this.fColor, that.fColor) &&
                        Arrays.equals(this.fMaintenanceChecks, that.fMaintenanceChecks);
    }

    /**
     * 測試equals()方法.
     */
    public static void main(String... aArguments) {
        List<String> options = new ArrayList<String>();
        options.add("sunroof");
        Date[] dates = new Date[1];
        dates[0] = new Date();

        //建立一堆Car對象,僅有one和two應該是相等的
        Car one = new Car("Nissan", 2, options, 46.3, "Green", dates);

        //two和one相等
        Car two = new Car("Nissan", 2, options, 46.3, "Green", dates);

        //three僅有fName不一樣
        Car three = new Car("Pontiac", 2, options, 46.3, "Green", dates);

        //four 僅有fNumDoors不一樣
        Car four = new Car("Nissan", 4, options, 46.3, "Green", dates);

        //five僅有fOptions不一樣
        List<String> optionsTwo = new ArrayList<String>();
        optionsTwo.add("air conditioning");
        Car five = new Car("Nissan", 2, optionsTwo, 46.3, "Green", dates);

        //six僅有fGasMileage不一樣
        Car six = new Car("Nissan", 2, options, 22.1, "Green", dates);

        //seven僅有fColor不一樣
        Car seven = new Car("Nissan", 2, options, 46.3, "Fuchsia", dates);

        //eight僅有fMaintenanceChecks不一樣
        Date[] datesTwo = new Date[1];
        datesTwo[0] = new Date(1000000);
        Car eight = new Car("Nissan", 2, options, 46.3, "Green", datesTwo);

        System.out.println("one = one: " + one.equals(one));
        System.out.println("one = two: " + one.equals(two));
        System.out.println("two = one: " + two.equals(one));
        System.out.println("one = three: " + one.equals(three));
        System.out.println("one = four: " + one.equals(four));
        System.out.println("one = five: " + one.equals(five));
        System.out.println("one = six: " + one.equals(six));
        System.out.println("one = seven: " + one.equals(seven));
        System.out.println("one = eight: " + one.equals(eight));
        System.out.println("one = null: " + one.equals(null));
    }
}

       輸出結果以下: 性能

one = one: true 
one = two: true 
two = one: true 
one = three: false 
one = four: false 
one = five: false 
one = six: false 
one = seven: false 
one = eight: false 
one = null: false

3、重寫hashCode()方法

      一、爲何要重寫hashCode()方法

       在每一個重寫了equals()方法的類中也必需要重寫hashCode()方法,若是不這樣作就會違反Java API中Object類的hashCode()方法的約定,從而致使該類沒法很好的用於基於散列的數據結構(HashSet、HashMap、Hashtable、LinkedHashSet、LinkedHashMap等等)。

      下面是約定內容:

在 Java 應用程序執行期間,若是沒有修改對象的equals()方法的比較操做所用到的信息,那麼不管何時在同一對象上屢次調用 hashCode 方法時,必須一致地返回同一個整數。同一應用程序的屢次執行過程當中,每次返回的整數能夠不一致。 
若是兩個對象根據 equals(Object) 方法進行比較是相等的,那麼調用這兩個對象中任意一個對象的hashCode() 方法都必須產生相同的整數結果。 
若是兩個對象根據 equals(java.lang.Object) 方法進行比較是不相等的,那麼調用這兩個對象中任意一個對象的hashCode() 方法則不必定要產生不一樣的整數結果。可是,程序員應該知道,爲不相等的對象產生不一樣整數結果可能會提升哈希表的性能。

      因沒有重寫hashCode()方法而違反的約定是第二條:相等的對象必須具備相同的散列碼。

       咱們來看看Object類中的hashCode()方法: public native int hashCode()。它默認老是爲每一個不一樣的對象產生不一樣的整數結果。即便咱們重寫equals()方法讓類的兩個大相徑庭的實例是相等的,可是根據Object.hashCode()方法,它們是徹底不一樣的兩個對象,即若是對象的散列碼不能反映它們相等,那麼對象怎麼相等也沒用。

      下面是一段測試代碼:

       代碼清單-4

public class EqualsAndHashcode {
    static class Person {
        private String name;
        private Integer age;
        public Person(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Person)) return false;
            Person person = (Person) o;
            if (!name.equals(person.name)) return false;
            return true;
        }
    }
    public static void main(String[] args) {
        Map<Person, String> map = new HashMap<Person, String>();
        Person person1 = new Person("aaa", 22);
        map.put(person1, "aaa");
        Person person2 = new Person("aaa", 11);
        System.out.println(person1.equals(person2));
        System.out.println(map.get(person2));
    }
}
         輸出結果以下:    

true
null

      二、具備相同散列碼的對象必定相等嗎?

      若是散列碼不一樣,元素就會被放入集合中不一樣的bucket(桶)中。在實際的哈希算法中,在同一個桶(Bucket)內有多個元素的情形很是廣泛,由於它們的散列碼相同。這時候哈希檢索就是一個兩步的過程:

          1) 使用hashCode()找到正確的桶(bucket)。
        2) 使用equals()在桶內找到正確的元素。
        因此除非使用equals()方法比較是相等的,不然相同散列碼的對象仍是不相等。

       所以爲了定位一個對象,查找對象和集合內的對象兩者都必須具備相同的散列碼,而且equals()方法也返回true。因此重寫equals()方法也必需要重寫hashCode()方法才能保證對象能夠用做基於散列的集合。

      三、如何實現性能好的hashCode()方法

        不管全部實例變量是否相等都爲其返回相同值,這樣的hashCode()仍然是合法的,也是適當的。

      代碼清單-5

Override
public int hashCode() { 
   return 1492;
}

        它雖然不違反hashCode()方法的約定,可是它很是低效,由於全部的對象都放在一個bucket內,仍是要經過equals()方法費力的找到正確的對象。

       一個好的hashCode()方法一般傾向於「爲不相等的對象產生不相等的散列碼」。這正是hashCode()方法的第三條約定的含義。理想狀況下,hashCode()方法應該產生均勻分佈的散列碼,將不相等的對象均勻分佈到全部可能的散列值上。若是散列碼都集中在一起,那麼基於散列的集合在某些bucket的負載會很重。

      在《Effective Java》一書中,Joshua Bloch對於怎樣寫出好的hashCode()方法給出了很好的指導,以下:

       一、把某個非零的常數值,好比說17,保存在一個名爲resultint類型的變量中。

       二、對於對象中每一個關鍵域(equals方法中涉及的每一個域),完成如下步驟:

             a.    爲該域計算int類型的散列碼c:
                    i.    若是該域是boolean類型,則計算(f?1:0)
                    ii.   若是該域是bytecharshort或者int類型,則計算(int)f。
                    iii.  若是該域是long類型,則計算(int)(f ^ (f >>> 32))
                    iv.  若是該域是float類型,則計算Float.floatToIntBits(f)
                    v.   若是該域是double類型,則計算Double.doubleToLongBits(f),而後按照步驟2.a.iii,爲獲得的long類型值計算散列值。
                    vi.  若是該域是一個對象引用,而且該類的equals方法經過遞歸地調用equals的方式來 比較這個域,則一樣爲這個域遞歸地調用hashCode。若是須要更復雜的比較,則 爲這個域計算一個範式(canonical representation)」,而後針對這個範式調用 hashCode。若是這個域的值爲null,則返回0 (或者其餘某個常數,但一般是0)
                    vii. 若是該域是一個數組,則要把每個元素當作單獨的域來處理。也就是說,遞歸地應用上述規則,對每一個重要的元素計算一個散列碼,而後根據步驟2.b中的作法把這些散列值組合起來。若是數組域中的每一個元素都很重要,能夠利用發行版本1.5中增長的其中一個Arrays.hashCode方法。

            b.   按照下面的公式,把步驟2.a中計算獲得的散列碼c合併到result中: result * 31 * result c;
        三、返回result

        四、寫完了hashCode方法以後,問問本身相等的實例是否都具備相等的散列碼。要編寫單元測試來驗證你的推斷。若是相等的實例有着不相等的散列碼,則要找出緣由,並修正錯誤。

        下面是遵循這些指導的一個代碼示例

       代碼清單-6

public class CountedString {

    private static List<String> created = new ArrayList<String>();
    private String s;
    private int id = 0;
    public CountedString(String str) {
        this.s = str;
        created.add(str);
        for (String s2 : created) {
            if (s2.equals(s)) {
                id++;
            }
        }
    }

    @Override
    public String toString() {
        return "String: " + s + ", id=" + id + " hashCode(): " + hashCode();
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof CountedString && s.equals(((CountedString) o).s) && id == ((CountedString) o).id;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 37 * result + s.hashCode();
        result = 37 * result + id;
        return result;
    }

    public static void main(String[] args) {
        Map<CountedString, Integer> map = new HashMap<CountedString, Integer>();
        CountedString[] cs = new CountedString[5];
        for (int i = 0; i < cs.length; i++) {
            cs[i] = new CountedString("hi");
            map.put(cs[i], i);
        }
        System.out.println(map);
        for (CountedString cstring : cs) {
            System.out.println("Looking up " + cstring);
            System.out.println(map.get(cstring));
        }
    }
}
          輸出結果以下:

{String: hi, id=1 hashCode(): 146447=0, String: hi, id=2 hashCode(): 146448=1, String: hi, id=3 hashCode(): 146449=2, String: hi, id=4 hashCode(): 146450=3, String: hi, id=5 hashCode(): 146451=4}
Looking up String: hi, id=1 hashCode(): 146447
0
Looking up String: hi, id=2 hashCode(): 146448
1
Looking up String: hi, id=3 hashCode(): 146449
2
Looking up String: hi, id=4 hashCode(): 146450
3
Looking up String: hi, id=5 hashCode(): 146451
4

      四、一個致使hashCode()方法失敗的情形

        咱們都知道,序列化可保存對象,在之後能夠經過反序列化再次獲得該對象,可是對於transient變量,咱們沒法對其進行序列化,若是在hashCode()方法中包含一個transient變量,可能會致使放入集合中的對象沒法找到。參見下面這個示例:

        代碼清單-7

public class SaveMe implements Serializable {

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof SaveMe)) return false;
        SaveMe saveMe = (SaveMe) o;
        if (x != saveMe.x) return false;
        if (y != saveMe.y) return false;
        return true;
    }

    @Override
    public int hashCode() {
        return x ^ y;
    }

    @Override
    public String toString() {
        return "SaveMe{" + "x=" + x + ", y=" + y + '}';
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SaveMe a = new SaveMe(9, 5);
        // 打印對象
        System.out.println(a);
        Map<SaveMe, Integer> map = new HashMap<SaveMe, Integer>();
        map.put(a, 10);
        // 序列化a
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(a);
        oos.flush();
        // 反序列化a
        ObjectInputStream ois= new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        SaveMe b = (SaveMe)ois.readObject();
        // 打印反序列化後的對象
        System.out.println(b);
        // 使用反序列化後的對象檢索對象
        System.out.println(map.get(b));
    }
}

      輸出結果以下:

SaveMe{x=9, y=5}
SaveMe{x=0, y=5}
null

      從上面的測試能夠知道,對象的transient變量反序列化後具備一個默認值,而不是對象保存(或放入HashMap)時該變量所具備的值。

5、總結

        當重寫equals()方法時,必需要重寫hashCode()方法,特別是當對象用於基於散列的集合中時。

6、參考資料

        http://www.ibm.com/developerworks/library/j-jtp05273/

       http://www.javapractices.com/topic/TopicAction.do?Id=17

      《Effective Java》

      《Thinking in Java》

相關文章
相關標籤/搜索