細說equals()方法和hashCode()方法

EnjoyAndroid發表於2017-11-24

一、前言

       對於這兩個方法的研究,源於一道比較經典的面試題:“x.equals(y)==true;x,y可有不同的hashcode對嗎?”,其實這道題的關鍵在於考我們對equals()方法和hashCode()方法的理解,網上看了不少文章,有說對的,也有說不對的。在我看來對也不對,具體原因,我們下面慢慢分析。

二、equals()方法

       equals()方法是Object中定義的方法,任何類都可以重寫,但是需要遵循一定的規範,我們看一下Object中的預設實現

    public boolean equals(Object obj) {
        return (this == obj);
    }複製程式碼

可以看到就是對兩個物件的比較,也就是兩個物件的地址值是否相等。然而有的時候,預設的比較方式並不能滿足我們的需求,比如要我們判斷兩個學生物件是否是同一個,需要從學號、姓名等方面來比較,這時候就要重寫equals()方法了,這個重寫規則,JAVA是有明確的規範的:

2.1 自反性:x.equals(x)必須為true;
2.2 對稱性:x.equals(y)和y.equals(x)返回值必須相等;
2.3 傳遞性:x.equals(y)為true,和y.equals(z)為true,那麼x.equals(z)也必須為true;
2.4 一致性:如果物件x和y在equals()中使用的資訊沒有改變,那麼x.equals(y)的值始終不變;
2.5 非null : x不是null,y是null,那麼x.equals(y)必須為false複製程式碼

三、hashCode()方法

       這是Android SDK中Object類的hashCode()方法實現:

   /**
     * Returns a hash code value for the object. This method is
     * supported for the benefit of hash tables such as those provided by
     * {@link java.util.HashMap}.
     * <p>
*/
  public int hashCode() {
        int lockWord = shadow$_monitor_;
        final int lockWordStateMask = 0xC0000000;  // Top 2 bits.
        final int lockWordStateHash = 0x80000000;  // Top 2 bits are value 2 (kStateHash).
        final int lockWordHashMask = 0x0FFFFFFF;  // Low 28 bits.
        if ((lockWord & lockWordStateMask) == lockWordStateHash) {
            return lockWord & lockWordHashMask;
        }
        return System.identityHashCode(this);
    }

    public static native int identityHashCode(Object x);複製程式碼

通過註釋我們可以得知一個很重要的資訊,那就是hashCode()這個方法主要是為了更好的支援雜湊表(如HashMap、HashSet、HashTable等)。說到這,我們有必要了解一下雜湊表的儲存原理了:

       當我們向雜湊表(如HashMap、HashSet、HashTable等)插入一個object時,首先呼叫hashcode()方法獲得該物件的雜湊碼,通過該雜湊碼可以直接定位object在雜湊表中的位置(一般是雜湊碼對雜湊表大小取餘),如果該位置沒有物件,那麼直接將object插入到該位置;如果該位置有物件(可能有多個,通過連結串列實現),則呼叫equals()方法將這些物件與object比較,如果相等,則不需要儲存object,否則,將該物件插入到該連結串列中。

       這也就解釋了,為什麼equals()相等,那麼hashCode()必須相等。因為,如果兩個物件的equals()方法返回true,則它們在雜湊表中只應該出現一次;如果hashCode()不相等,那麼它們會被雜湊到表中不同的位置,雜湊表中不止出現一次。

       JAVA建議,如果我們重寫了equals()方法,那麼也要重寫hashCode()方法,因為預設的hashCode()方法返回的是該物件記憶體中的地址。還是上面的例子,我們進行兩個學生的比較,依據是學號和姓名,如果都相等,那麼認為是同一個物件,有如下程式碼:

public class Student {

    private int id;
    private String name;


    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

public class Main {

    public static void main(String[] args) {

        Student student1 = new Student();
        student1.setId(1);
        student1.setName("name1");

        Student student2 = new Student();
        student2.setId(1);
        student2.setName("name1");

        System.out.println(student1.equals(student2));

    }
}複製程式碼

這時候返回的是false,肯定不是我們想要的,所以我們需要重寫equals()方法,讓它符合我們的業務需求,重寫後的程式碼如下:

public class Student {

    private int id;
    private String name;


    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (getId() != student.getId()) return false;
        return getName() != null ? getName().equals(student.getName()) : student.getName() == null;
    }


public class Main {

    public static void main(String[] args) {

        Student student1 = new Student();
        student1.setId(1);
        student1.setName("name1");

        Student student2 = new Student();
        student2.setId(1);
        student2.setName("name1");

        System.out.println(student1.equals(student2));

    }
}

}複製程式碼

通過重寫equals()方法之後,列印結果變成了true,但是這樣就完了嗎?我們進行另外一種測試:

public class Main {

    public static void main(String[] args) {

        Student student1 = new Student();
        student1.setId(1);
        student1.setName("name1");

        Student student2 = new Student();
        student2.setId(1);
        student2.setName("name1");

        System.out.println(student1.equals(student2));


        Set<Student> set = new HashSet<>();
        set.add(student1);
        set.add(student2);

        System.out.println("setSize: " + set.size());

    }
}複製程式碼

上面student1.equals(student2)返回的是true,setSize理應返回的結果是1才對,但是返回的確是2,這是因為它們的hashCode是不一樣的,所以Set集合認為它們是兩個不同的物件,因此新增了兩次。這時候hashCode()就排上用場了,我們需要重寫hashCode()方法,這就是為什麼JDK說,如果重寫了equals()方法,必須要重寫hashCode()方法的原因,:

public class Student {

    private int id;
    private String name;


    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (getId() != student.getId()) return false;
        return getName() != null ? getName().equals(student.getName()) : student.getName() == null;
    }


    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + name.hashCode();
        return result;
    }
}複製程式碼

這樣才算完事了。

hashCode()方法重寫的一些原則

a.如果重寫equals()方法,檢查條件“兩個物件通過equals()方法判斷相等,那麼它們的hashCode()也應該相等”是否成立,如果不成立,則重寫hashCode()方法。
b.hashCode()不能太簡單,否則容易造成hash衝突;
c.hashCode()不能太複雜,否則會影響效能。複製程式碼

但是一般來說我們不需要自己去寫,這裡有幾種便捷的實現方式

(1) Google的Guava專案裡有處理hashCode()和equals()的工具類com.google.common.base.Objects;
(2) Apache Commons也有類似的工具類EqualsBuilder和HashCodeBuilder;
(3) Java 7 也提供了工具類java.util.Objects;
(4) 常用IDE都提供hashCode()和equals()的程式碼生成。複製程式碼

至此,我們已經可以解答最開始的問題:“x.equals(y)==true;x,y可有不同的hashcode對嗎?”

嚴格來說,如果兩個物件x.equals(y)==true,那麼它們的hashCode()也要相等,如果不相等,當把它們新增到雜湊表中時就會出現資料混亂(如重複新增);但是,由於hashCode()方法並不是強制實現的,所以,是可能存在不同的hashcode的。

三、總結

       如果想寫出優秀的程式碼又不想踩坑就遵循官方的規範吧。

相關文章