Vấn đề nào cần được xem xét khi ghi đè bằng và hashCode trong Java?


617

Những vấn đề / cạm bẫy phải được xem xét khi ghi đè equalshashCode?

Câu trả lời:


1439

Lý thuyết (dành cho các luật sư ngôn ngữ và nghiêng về mặt toán học):

equals()( javadoc ) phải xác định mối quan hệ tương đương (nó phải là phản xạ , đối xứngbắc cầu ). Ngoài ra, nó phải nhất quán (nếu các đối tượng không được sửa đổi, thì nó phải tiếp tục trả về cùng một giá trị). Hơn nữa, o.equals(null)phải luôn luôn trả lại sai.

hashCode()( javadoc ) cũng phải nhất quán (nếu đối tượng không được sửa đổi về mặt equals(), nó phải tiếp tục trả về cùng một giá trị).

Các mối quan hệ giữa hai phương pháp là:

Bất cứ khi nào a.equals(b), sau đó a.hashCode()phải giống như b.hashCode().

Trong thực tế:

Nếu bạn ghi đè lên một, thì bạn nên ghi đè lên cái khác.

Sử dụng cùng một bộ các trường mà bạn sử dụng để tính toán equals()để tính toánhashCode() .

Sử dụng các lớp trình trợ giúp tuyệt vời EqualsBuilderHashCodeBuilder từ thư viện Apache Commons Lang . Một ví dụ:

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();
    }
}

Cũng nhớ:

Khi sử dụng Bộ sưu tập hoặc Bản đồ dựa trên hàm băm , chẳng hạn như Hashset , LinkedHashset , HashMap , Hashtable hoặc WeakHashMap , hãy đảm bảo rằng hashCode () của các đối tượng chính mà bạn đưa vào bộ sưu tập không bao giờ thay đổi trong khi đối tượng nằm trong bộ sưu tập. Cách chống đạn để đảm bảo điều này là làm cho chìa khóa của bạn trở nên bất biến, cũng có những lợi ích khác .


12
Điểm bổ sung về appendSuper (): bạn nên sử dụng nó trong hàm hashCode () và bằng () khi và chỉ khi bạn muốn kế thừa hành vi bình đẳng của siêu lớp. Chẳng hạn, nếu bạn xuất phát trực tiếp từ Object, không có điểm nào vì tất cả các Đối tượng đều khác biệt theo mặc định.
Antti Kissaniemi

312
Bạn có thể nhận được Eclipse để tạo hai phương thức cho bạn: Nguồn> Tạo hàm hashCode () và bằng ().
Rok Strniša

27
Điều này cũng đúng với Netbeans: developmentality.wordpress.com/2010/08/24/...
seinecle

6
@Darthenius Eclipse được tạo bằng bằng cách sử dụng getClass () có thể gây ra sự cố trong một số trường hợp (xem mục Java hiệu quả 8)
AndroidGecko

7
Việc kiểm tra null đầu tiên là không cần thiết với thực tế là instanceoftrả về false nếu toán hạng đầu tiên của nó là null (Java lại hiệu quả).
izaban

295

Có một số vấn đề đáng chú ý nếu bạn đang xử lý các lớp vẫn tồn tại khi sử dụng Trình ánh xạ mối quan hệ đối tượng (ORM) như Hibernate, nếu bạn không nghĩ rằng điều này đã phức tạp một cách vô lý!

Các đối tượng tải lười biếng là các lớp con

Nếu các đối tượng của bạn được duy trì bằng ORM, trong nhiều trường hợp, bạn sẽ xử lý các proxy động để tránh tải đối tượng quá sớm từ kho lưu trữ dữ liệu. Các proxy này được triển khai như các lớp con của lớp của riêng bạn. Điều này có nghĩa là this.getClass() == o.getClass()sẽ trở lại false. Ví dụ:

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

Nếu bạn đang xử lý ORM, sử dụng o instanceof Personlà điều duy nhất sẽ hành xử chính xác.

Các đối tượng tải lười biếng có các trường rỗng

Các ORM thường sử dụng các getters để buộc tải các đối tượng tải lười biếng. Điều này có nghĩa rằng person.namesẽ nullnếu personlà lười biếng nạp, ngay cả khi person.getName()lực lượng bốc và lợi nhuận "John Doe". Theo kinh nghiệm của tôi, cây trồng này thường xuyên hơn trong hashCode()equals() .

Nếu bạn đang xử lý ORM, hãy đảm bảo luôn sử dụng getters và không bao giờ tham chiếu trường trong hashCode()equals().

Lưu một đối tượng sẽ thay đổi trạng thái của nó

Các đối tượng liên tục thường sử dụng một idtrường để giữ khóa của đối tượng. Trường này sẽ được cập nhật tự động khi một đối tượng được lưu lần đầu tiên. Đừng sử dụng một trường id trong hashCode(). Nhưng bạn có thể sử dụng nó trong equals().

Một mẫu tôi thường sử dụng là

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

Nhưng: bạn không thể bao gồm getId()trong hashCode(). Nếu bạn làm, khi một đối tượng được duy trì, nó hashCodesẽ thay đổi. Nếu đối tượng ở trong một HashSet, bạn sẽ "không bao giờ" tìm thấy nó nữa.

Trong Personví dụ của tôi , tôi có thể sẽ sử dụng getName()cho hashCodegetId()cộng getName()(chỉ cho hoang tưởng) cho equals(). Không sao nếu có một số rủi ro "va chạm" hashCode(), nhưng không bao giờ ổn equals().

hashCode() nên sử dụng tập hợp con không thay đổi của các thuộc tính từ equals()


2
@Johannes Brodwall: tôi không hiểu Saving an object will change it's state! hashCodephải trả lại int, vậy bạn sẽ sử dụng getName()như thế nào? Bạn có thể đưa ra một ví dụ chohashCode
jimmybondy

@jimmybondy: getName sẽ trả về một đối tượng String cũng có
mã băm

85

Một sự làm rõ về obj.getClass() != getClass().

Tuyên bố này là kết quả của equals()việc thừa kế không thân thiện. JLS (đặc tả ngôn ngữ Java) chỉ định rằng nếu A.equals(B) == truesau đó B.equals(A)cũng phải trả về true. Nếu bạn bỏ qua câu lệnh đó kế thừa các lớp ghi đè equals()(và thay đổi hành vi của nó) sẽ phá vỡ đặc tả này.

Xem xét ví dụ sau về những gì xảy ra khi câu lệnh bị bỏ qua:

    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));
        }
    }    

Làm thêm new A(1).equals(new A(1)), new B(1,1).equals(new B(1,1))kết quả đưa ra đúng, như nó nên.

Điều này có vẻ rất tốt, nhưng hãy xem điều gì sẽ xảy ra nếu chúng ta cố gắng sử dụng cả hai lớp:

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

Rõ ràng, điều này là sai.

Nếu bạn muốn đảm bảo điều kiện đối xứng. a = b nếu b = a và nguyên tắc thay thế Liskov gọi super.equals(other)không chỉ trong trường hợp Bví dụ, mà còn kiểm tra sau 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;

Sẽ xuất ra:

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

Trường hợp, nếu akhông phải là một tham chiếu của B, thì nó có thể là một tham chiếu của lớp A(vì bạn mở rộng nó), trong trường hợp này bạn super.equals() cũng gọi .


2
Bạn có thể tạo các giá trị bằng nhau theo cách này (nếu so sánh một đối tượng siêu lớp với đối tượng lớp con, luôn luôn sử dụng các giá trị bằng của lớp con) if (obj.getClass ()! = This.getClass () && obj.getClass (). IsInstance (this) ) trả lại obj.equals (này);
pieaveragy

5
@piropagy - sau đó tôi sẽ nhận được một stackoverflow khi lớp thực hiện không ghi đè phương thức bằng. không vui.
Ran Biron

2
Bạn sẽ không nhận được một stackoverflow. Nếu phương thức bằng không bị ghi đè, bạn sẽ gọi lại mã tương tự, nhưng điều kiện để đệ quy sẽ luôn là sai!
Jacob Raihle

@pieaveragy: Làm thế nào mà hành xử nếu có hai lớp dẫn xuất khác nhau? Nếu a ThingWithOptionSetAcó thể bằng với một Thingđiều kiện là tất cả các tùy chọn bổ sung đều có giá trị mặc định và tương tự như vậy đối với a ThingWithOptionSetB, thì có ThingWithOptionSetAthể so sánh bằng một ThingWithOptionSetBchỉ khi tất cả các thuộc tính không cơ sở của cả hai đối tượng khớp với mặc định của chúng, nhưng Tôi không thấy cách bạn kiểm tra cho điều đó.
supercat

7
Vấn đề với nó là nó phá vỡ tính siêu việt. Nếu bạn thêm B b2 = new B(1,99), sau đó b.equals(a) == truea.equals(b2) == truenhưng b.equals(b2) == false.
nickgrim

46

Để triển khai thân thiện với thừa kế, hãy xem giải pháp của Tal Cohen, Làm thế nào để tôi thực hiện đúng phương thức bằng ()?

Tóm lược:

Trong cuốn sách Hướng dẫn ngôn ngữ lập trình Java hiệu quả (Addison-Wesley, 2001), Joshua Bloch tuyên bố rằng "Đơn giản là không có cách nào để mở rộng một lớp khả thi và thêm một khía cạnh trong khi duy trì hợp đồng bằng nhau." Tal không đồng ý.

Giải pháp của anh ấy là thực hiện bằng () bằng cách gọi một cách khác là BlindlyEquals () cả hai cách. blindlyEquals () bị ghi đè bởi các lớp con, bằng () được kế thừa và không bao giờ bị ghi đè.

Thí dụ:

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);
    }
}

Lưu ý rằng bằng () phải hoạt động trên các hệ thống phân cấp thừa kế nếu Nguyên tắc thay thế Liskov được thỏa mãn.


10
Hãy xem phương pháp canEqual được giải thích ở đây - nguyên tắc tương tự làm cho cả hai giải pháp hoạt động, nhưng với canEqual, bạn không so sánh các trường giống nhau hai lần (ở trên, px == this.x sẽ được thử nghiệm theo cả hai hướng): artima.com /lejava/articles/equality.html
Blaisorblade

2
Trong mọi trường hợp, tôi không nghĩ rằng đây là một ý tưởng tốt. Nó làm cho hợp đồng Equals trở nên khó hiểu một cách không cần thiết - ai đó có hai tham số Điểm, a và b, phải có ý thức về khả năng a.getX () == b.getX () và a.getY () == b.getY () có thể đúng, nhưng a.equals (b) và b.equals (a) đều sai (nếu chỉ có một là ColorPoint).
Kevin

Về cơ bản, điều này giống như if (this.getClass() != o.getClass()) return false, nhưng linh hoạt ở chỗ nó chỉ trả về false nếu (các) lớp dẫn xuất bận tâm sửa đổi bằng. Có đúng không?
Alexanderr Dubinsky

31

Vẫn ngạc nhiên rằng không ai đề nghị thư viện ổi cho việc này.

 //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());
    }

23
java.util.Objects.hash () và java.util.Objects.equals () là một phần của Java 7 (phát hành năm 2011) vì vậy bạn không cần Guava cho việc này.
herman

1
tất nhiên, nhưng bạn nên tránh điều đó vì Oracle không cung cấp các bản cập nhật công khai nữa cho Java 6 (điều này đã xảy ra kể từ tháng 2 năm 2013).
herman

6
Của bạn thistrong this.getDate()phương tiện gì (trừ lộn xộn)
Steve Kuo

1
Biểu thức "không thể hiện" của bạn cần thêm dấu ngoặc : if (!(otherObject instanceof DateAndPattern)) {. Đồng ý với hernan và Steve Kuo (mặc dù đó là vấn đề sở thích cá nhân), nhưng dù sao +1.
Amos M. Carpenter

26

Có hai phương thức trong siêu lớp là java.lang.Object. Chúng ta cần ghi đè chúng vào đối tượng tùy chỉnh.

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

Các đối tượng bằng nhau phải tạo ra cùng một mã băm miễn là chúng bằng nhau, tuy nhiên các đối tượng không bằng nhau không cần tạo ra các mã băm riêng biệt.

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
}

Nếu bạn muốn nhận thêm, vui lòng kiểm tra liên kết này dưới dạng http://www.javaranch.com/journal/2002/10/equalhash.html

Đây là một ví dụ khác, http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html

Chúc vui vẻ! @. @


Xin lỗi nhưng tôi không hiểu tuyên bố này về phương thức hashCode: nó không hợp pháp nếu nó sử dụng nhiều biến hơn bằng (). Nhưng nếu tôi mã với nhiều biến hơn, mã của tôi sẽ biên dịch. Tại sao nó không hợp pháp?
Adryr83

19

Có một số cách để kiểm tra sự bình đẳng của lớp trước khi kiểm tra sự bình đẳng của thành viên và tôi nghĩ cả hai đều hữu ích trong trường hợp phù hợp.

  1. Sử dụng instanceoftoán tử.
  2. Sử dụng this.getClass().equals(that.getClass()).

Tôi sử dụng số 1 trong finaltriển khai bằng, hoặc khi triển khai giao diện quy định thuật toán cho bằng (như java.utilgiao diện bộ sưu tập Cách thức đúng để kiểm tra(obj instanceof Set) hoặc bất kỳ giao diện nào bạn đang triển khai). Nói chung, đó là một lựa chọn tồi khi các giá trị bằng có thể bị ghi đè vì phá vỡ thuộc tính đối xứng.

Tùy chọn # 2 cho phép lớp được mở rộng một cách an toàn mà không bị ghi đè bằng hoặc phá vỡ tính đối xứng.

Nếu lớp của bạn cũng vậy Comparable, equalscompareTocác phương thức cũng phải nhất quán. Đây là một mẫu cho phương thức bằng trong một Comparablelớp:

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;
  }

}

1
+1 cho điều này. Cả getClass () và instanceof đều không phải là thuốc chữa bách bệnh và đây là một lời giải thích tốt về cách tiếp cận cả hai. Đừng nghĩ rằng có bất kỳ lý do nào để không làm điều này.getClass () == that.getClass () thay vì sử dụng bằng ().
Paul Cantrell

Có một vấn đề với điều này. Các lớp ẩn danh không thêm bất kỳ khía cạnh nào cũng như ghi đè phương thức bằng sẽ không kiểm tra getClass mặc dù chúng phải bằng nhau.
steinybot

@Steiny Tôi không rõ ràng rằng các đối tượng thuộc các loại khác nhau phải bằng nhau; Tôi đang nghĩ về việc triển khai các giao diện khác nhau như một lớp ẩn danh chung. Bạn có thể đưa ra một ví dụ để hỗ trợ tiền đề của bạn?
erickson

MyClass a = new MyClass (123); MyClass b = new MyClass (123) {// Ghi đè một số phương thức}; // a.equals (b) là sai khi sử dụng this.getClass (). bằng (that.getClass ())
steinybot 7/07/2015

1
@Steiny Phải. Vì nó nên trong hầu hết các trường hợp, đặc biệt là nếu một phương thức bị ghi đè thay vì được thêm vào. Hãy xem xét ví dụ của tôi ở trên. Nếu không final, và compareTo()phương thức được ghi đè để đảo ngược thứ tự sắp xếp, các thể hiện của lớp con và siêu lớp không nên được coi là bằng nhau. Khi các đối tượng này được sử dụng cùng nhau trong một cây, các khóa "bằng" theo cách instanceoftriển khai có thể không tìm thấy được.
erickson


11

Phương thức equals () được sử dụng để xác định đẳng thức của hai đối tượng.

as int value của 10 luôn bằng 10. Nhưng phương thức equals () này là về đẳng thức của hai đối tượng. Khi chúng ta nói đối tượng, nó sẽ có các thuộc tính. Để quyết định về bình đẳng những tài sản được xem xét. Không nhất thiết là tất cả các thuộc tính phải được tính đến để xác định sự bằng nhau và liên quan đến định nghĩa lớp và bối cảnh có thể được quyết định. Sau đó, phương thức equals () có thể được ghi đè.

chúng ta nên luôn ghi đè phương thức hashCode () bất cứ khi nào chúng ta ghi đè phương thức equals (). Nếu không, chuyện gì sẽ xảy ra? Nếu chúng tôi sử dụng hashtables trong ứng dụng của mình, nó sẽ không hoạt động như mong đợi. Vì hashCode được sử dụng để xác định sự bằng nhau của các giá trị được lưu trữ, nó sẽ không trả về giá trị tương ứng cho một khóa.

Việc thực hiện mặc định được đưa ra là phương thức hashCode () trong lớp Object sử dụng địa chỉ bên trong của đối tượng và chuyển đổi nó thành số nguyên và trả về nó.

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;

  }
}

Mã đầu ra ví dụ:

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

7

Theo logic chúng ta có:

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

Nhưng không phải ngược lại!


6

Một gotcha tôi đã tìm thấy là nơi hai đối tượng chứa các tham chiếu cho nhau (một ví dụ là mối quan hệ cha mẹ / con cái với một phương thức tiện lợi trên cha mẹ để có được tất cả con cái).
Những thứ này khá phổ biến khi thực hiện ánh xạ Hibernate chẳng hạn.

Nếu bạn bao gồm cả hai đầu của mối quan hệ trong hàm hashCode của mình hoặc bằng với các kiểm tra thì có thể vào vòng lặp đệ quy kết thúc trong StackOverflowException.
Giải pháp đơn giản nhất là không bao gồm bộ sưu tập getChildren trong các phương thức.


5
Tôi nghĩ rằng lý thuyết cơ bản ở đây là để phân biệt giữa các thuộc tính , tập hợpassociatinos của một đối tượng. Các hiệp hội không nên tham gia equals(). Nếu một nhà khoa học điên tạo ra một bản sao của tôi, chúng ta sẽ tương đương. Nhưng chúng tôi sẽ không có cùng một người cha.
Raedwald
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.