簡體   English   中英

在 Java 中重寫 equals 和 hashCode 時應該考慮哪些問題?

[英]What issues should be considered when overriding equals and hashCode in Java?

覆蓋equalshashCode時必須考慮哪些問題/陷阱?

理論(適用於語言律師和數學愛好者):

equals() ( javadoc ) 必須定義一個等價關系(它必須是自反的對稱的傳遞的)。 另外,它必須是一致的(如果對象沒有被修改,那么它必須保持返回相同的值)。 此外, o.equals(null)必須始終返回 false。

hashCode() ( javadoc ) 也必須一致(如果對象沒有根據equals()修改,它必須保持返回相同的值)。

這兩種方法的關系是:

每當a.equals(b) ,那么a.hashCode()必須與b.hashCode()相同。

在實踐中:

如果您覆蓋一個,那么您應該覆蓋另一個。

使用用於計算equals()的相同字段集來計算hashCode()

使用Apache Commons Lang庫中優秀的輔助類EqualsBuilderHashCodeBuilder 一個例子:

public class Person {
    private String name;
    private int age;
    // ...

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 31). // two randomly chosen prime numbers
            // if deriving: appendSuper(super.hashCode()).
            append(name).
            append(age).
            toHashCode();
    }

    @Override
    public boolean equals(Object obj) {
       if (!(obj instanceof Person))
            return false;
        if (obj == this)
            return true;

        Person rhs = (Person) obj;
        return new EqualsBuilder().
            // if deriving: appendSuper(super.equals(obj)).
            append(name, rhs.name).
            append(age, rhs.age).
            isEquals();
    }
}

還要記住:

使用基於散列的集合映射(例如HashSetLinkedHashSetHashMapHashtableWeakHashMap )時,請確保放入集合的關鍵對象的 hashCode() 在對象在集合中時不會更改。 確保這一點的萬無一失的方法是使您的密鑰不可變,這還有其他好處

如果您正在處理使用像 Hibernate 這樣的對象關系映射器 (ORM) 持久化的類,如果您認為這已經不合理地復雜了,那么有一些問題值得注意!

延遲加載的對象是子類

如果您的對象使用 ORM 持久化,在許多情況下您將處理動態代理以避免從數據存儲中過早加載對象。 這些代理作為您自己類的子類實現。 這意味着this.getClass() == o.getClass()將返回false 例如:

Person saved = new Person("John Doe");
Long key = dao.save(saved);
dao.flush();
Person retrieved = dao.retrieve(key);
saved.getClass().equals(retrieved.getClass()); // Will return false if Person is loaded lazy

如果您正在處理 ORM,那么使用o instanceof Person是唯一可以正確運行的方法。

延遲加載的對象具有空字段

ORM 通常使用 getter 來強制加載延遲加載的對象。 這意味着如果person延遲加載,則person.name將為null ,即使person.getName()強制加載並返回“John Doe”。 根據我的經驗,這在hashCode()equals()出現得更頻繁。

如果您正在處理 ORM,請確保始終使用 getter,並且從不使用hashCode()equals()字段引用。

保存一個對象會改變它的狀態

持久化對象通常使用一個id字段來保存對象的鍵。 首次保存對象時,此字段將自動更新。 不要在hashCode()使用 id 字段。 但是您可以在equals()使用它。

我經常使用的一種模式是

if (this.getId() == null) {
    return this == other;
}
else {
    return this.getId().equals(other.getId());
}

但是:您不能在hashCode()包含getId() hashCode() 如果這樣做,當一個對象被持久化時,它的hashCode改變。 如果對象在HashSet ,您將“永遠”找不到它。

在我的Person示例中,我可能會使用getName()作為hashCodegetId()加上getName() (僅用於偏執)作為equals() 如果hashCode()有一些“沖突”的風險是可以的,但對於equals()永遠沒有問題。

hashCode()應該使用equals()不變的屬性子集

關於obj.getClass() != getClass()

此語句是equals()繼承不友好的結果。 JLS(Java 語言規范)指定如果A.equals(B) == trueB.equals(A)也必須返回true 如果您忽略繼承覆蓋equals() (並更改其行為)的語句將破壞此規范。

考慮以下省略語句時發生的情況的示例:

    class A {
      int field1;

      A(int field1) {
        this.field1 = field1;
      }

      public boolean equals(Object other) {
        return (other != null && other instanceof A && ((A) other).field1 == field1);
      }
    }

    class B extends A {
        int field2;

        B(int field1, int field2) {
            super(field1);
            this.field2 = field2;
        }

        public boolean equals(Object other) {
            return (other != null && other instanceof B && ((B)other).field2 == field2 && super.equals(other));
        }
    }    

執行new A(1).equals(new A(1))此外, new B(1,1).equals(new B(1,1))結果應該是正確的。

這看起來都很好,但是看看如果我們嘗試使用這兩個類會發生什么:

A a = new A(1);
B b = new B(1,1);
a.equals(b) == true;
b.equals(a) == false;

顯然,這是錯誤的。

如果要保證對稱條件。 a=b 如果 b=a 和 Liskov 替換原則不僅在B實例的情況下調用super.equals(other) ,而且在A實例之后檢查:

if (other instanceof B )
   return (other != null && ((B)other).field2 == field2 && super.equals(other)); 
if (other instanceof A) return super.equals(other); 
   else return false;

這將輸出:

a.equals(b) == true;
b.equals(a) == true;

其中,如果a不的參考B ,那么它可能是一個是類的引用A (因為你擴展它),在這種情況下,你叫super.equals()

對於繼承友好的實現,請查看 Tal Cohen 的解決方案, 如何正確實現 equals() 方法?

概括:

在他的《 Effective Java Programming Language Guide》 (Addison-Wesley,2001 年)一書中,Joshua Bloch 聲稱“根本沒有辦法在保留 equals 約定的同時擴展可實例化的類並添加方面。” 塔爾不同意。

他的解決方案是通過雙向調用另一個非對稱的blinlyEquals()來實現equals()。 BlindlyEquals() 被子類覆蓋,equals() 是繼承的,永遠不會被覆蓋。

例子:

class Point {
    private int x;
    private int y;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point)o;
        return (p.x == this.x && p.y == this.y);
    }
    public boolean equals(Object o) {
        return (this.blindlyEquals(o) && o.blindlyEquals(this));
    }
}

class ColorPoint extends Point {
    private Color c;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint)o;
        return (super.blindlyEquals(cp) && 
        cp.color == this.color);
    }
}

請注意,如果要滿足Liskov 替換原則,equals() 必須跨繼承層次工作。

仍然感到驚訝的是,沒有人為此推薦番石榴庫。

 //Sample taken from a current working project of mine just to illustrate the idea

    @Override
    public int hashCode(){
        return Objects.hashCode(this.getDate(), this.datePattern);
    }

    @Override
    public boolean equals(Object obj){
        if ( ! obj instanceof DateAndPattern ) {
            return false;
        }
        return Objects.equal(((DateAndPattern)obj).getDate(), this.getDate())
                && Objects.equal(((DateAndPattern)obj).getDate(), this.getDatePattern());
    }

超類中有兩個方法java.lang.Object。 我們需要將它們覆蓋為自定義對象。

public boolean equals(Object obj)
public int hashCode()

相等的對象只要相等就必須產生相同的散列碼,但不相等的對象不需要產生不同的散列碼。

public class Test
{
    private int num;
    private String data;
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if((obj == null) || (obj.getClass() != this.getClass()))
            return false;
        // object must be Test at this point
        Test test = (Test)obj;
        return num == test.num &&
        (data == test.data || (data != null && data.equals(test.data)));
    }

    public int hashCode()
    {
        int hash = 7;
        hash = 31 * hash + num;
        hash = 31 * hash + (null == data ? 0 : data.hashCode());
        return hash;
    }

    // other methods
}

如果您想獲得更多,請檢查此鏈接為http://www.javaranch.com/journal/2002/10/equalhash.html

這是另一個例子, http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html

玩得開心! @.@

在檢查成員相等性之前,有幾種方法可以檢查類相等性,我認為這兩種方法在正確的情況下都很有用。

  1. 使用instanceof運算符。
  2. 使用this.getClass().equals(that.getClass())

我在final equals 實現中使用 #1,或者在實現為 equals 規定算法的接口時(如java.util集合接口——使用(obj instanceof Set)或您正在實現的任何接口進行檢查的正確方法) . 當 equals 可以被覆蓋時,這通常是一個糟糕的選擇,因為這會破壞對稱性。

選項#2 允許在不覆蓋等號或破壞對稱性的情況下安全地擴展類。

如果您的類也是Comparable ,則equalscompareTo方法也應該保持一致。 這是Comparable類中 equals 方法的模板:

final class MyClass implements Comparable<MyClass>
{

  …

  @Override
  public boolean equals(Object obj)
  {
    /* If compareTo and equals aren't final, we should check with getClass instead. */
    if (!(obj instanceof MyClass)) 
      return false;
    return compareTo((MyClass) obj) == 0;
  }

}

對於平等,請查看Angelika Langer 的Secrets of Equals 我非常愛它。 她也是關於Java 泛型的一個很好的常見問題解答。 在此處查看她的其他文章(向下滾動到“Core Java”),在那里她還繼續討論第 2 部分和“混合類型比較”。 祝您閱讀愉快!

equals() 方法用於確定兩個對象的相等性。

因為 int 值 10 總是等於 10。但是這個 equals() 方法是關於兩個對象的相等性。 當我們說對象時,它將具有屬性。 為了確定相等性,要考慮這些屬性。 不必考慮所有屬性來確定相等性,並且可以根據類定義和上下文來決定。 然后可以覆蓋 equals() 方法。

每當我們覆蓋 equals() 方法時,我們都應該始終覆蓋 hashCode() 方法。 如果不是,會發生什么? 如果我們在應用程序中使用哈希表,它將不會按預期運行。 由於 hashCode 用於確定存儲的值的相等性,因此它不會為鍵返回正確的對應值。

給出的默認實現是 Object 類中的 hashCode() 方法使用對象的內部地址並將其轉換為整數並返回它。

public class Tiger {
  private String color;
  private String stripePattern;
  private int height;

  @Override
  public boolean equals(Object object) {
    boolean result = false;
    if (object == null || object.getClass() != getClass()) {
      result = false;
    } else {
      Tiger tiger = (Tiger) object;
      if (this.color == tiger.getColor()
          && this.stripePattern == tiger.getStripePattern()) {
        result = true;
      }
    }
    return result;
  }

  // just omitted null checks
  @Override
  public int hashCode() {
    int hash = 3;
    hash = 7 * hash + this.color.hashCode();
    hash = 7 * hash + this.stripePattern.hashCode();
    return hash;
  }

  public static void main(String args[]) {
    Tiger bengalTiger1 = new Tiger("Yellow", "Dense", 3);
    Tiger bengalTiger2 = new Tiger("Yellow", "Dense", 2);
    Tiger siberianTiger = new Tiger("White", "Sparse", 4);
    System.out.println("bengalTiger1 and bengalTiger2: "
        + bengalTiger1.equals(bengalTiger2));
    System.out.println("bengalTiger1 and siberianTiger: "
        + bengalTiger1.equals(siberianTiger));

    System.out.println("bengalTiger1 hashCode: " + bengalTiger1.hashCode());
    System.out.println("bengalTiger2 hashCode: " + bengalTiger2.hashCode());
    System.out.println("siberianTiger hashCode: "
        + siberianTiger.hashCode());
  }

  public String getColor() {
    return color;
  }

  public String getStripePattern() {
    return stripePattern;
  }

  public Tiger(String color, String stripePattern, int height) {
    this.color = color;
    this.stripePattern = stripePattern;
    this.height = height;

  }
}

示例代碼輸出:

bengalTiger1 and bengalTiger2: true 
bengalTiger1 and siberianTiger: false 
bengalTiger1 hashCode: 1398212510 
bengalTiger2 hashCode: 1398212510 
siberianTiger hashCode: –1227465966

邏輯上我們有:

a.getClass().equals(b.getClass()) && a.equals(b)a.hashCode() == b.hashCode()

反之則不然!

我發現的一個問題是兩個對象包含彼此的引用(一個例子是父/子關系,父/子關系使用父級上的便捷方法來獲取所有子級)。
例如,在進行 Hibernate 映射時,這類事情相當普遍。

如果在 hashCode 或 equals 測試中包含關系的兩端,則可能會進入以 StackOverflowException 結尾的遞歸循環。
最簡單的解決方案是不在方法中包含 getChildren 集合。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM