Masalah apa yang harus dipertimbangkan ketika mengganti kode hash dan yang sama di Java?

617

Masalah / perangkap apa yang harus dipertimbangkan ketika mengesampingkan equalsdan hashCode?

Matt Sheppard
sumber

Jawaban:

1439

Teori (untuk pengacara bahasa dan yang cenderung matematis):

equals()( javadoc ) harus mendefinisikan hubungan ekivalensi (harus refleksif , simetris , dan transitif ). Selain itu, harus konsisten (jika objek tidak dimodifikasi, maka ia harus tetap mengembalikan nilai yang sama). Selanjutnya, o.equals(null)harus selalu mengembalikan false.

hashCode()( javadoc ) juga harus konsisten (jika objek tidak dimodifikasi dalam hal equals(), harus tetap mengembalikan nilai yang sama).

The hubungan antara dua metode adalah:

Kapanpun a.equals(b), maka a.hashCode()harus sama dengan b.hashCode().

Dalam praktek:

Jika Anda menimpa satu, maka Anda harus menimpa yang lain.

Gunakan kumpulan bidang yang sama yang Anda gunakan untuk menghitung equals()untuk menghitung hashCode().

Gunakan kelas pembantu yang sangat baik, EqualsBuilder dan HashCodeBuilder dari perpustakaan Apache Commons Lang . Sebuah contoh:

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

Ingat juga:

Saat menggunakan Koleksi atau Peta berbasis hash seperti HashSet , LinkedHashSet , HashMap , Hashtable , atau WeakHashMap , pastikan kode hash () dari objek kunci yang Anda masukkan ke dalam koleksi tidak pernah berubah saat objek berada di koleksi. Cara anti peluru untuk memastikan ini adalah membuat kunci Anda tidak berubah, yang juga memiliki manfaat lain .

Antti Sykäri
sumber
12
Poin tambahan tentang appendSuper (): Anda harus menggunakannya dalam kode hash () dan equals () jika dan hanya jika Anda ingin mewarisi perilaku kesetaraan dari superclass. Misalnya, jika Anda berasal langsung dari Objek, tidak ada gunanya karena semua Objek berbeda secara default.
Antti Kissaniemi
312
Anda bisa mendapatkan Eclipse untuk menghasilkan dua metode untuk Anda: Sumber> Hasilkan kode hash () dan equals ().
Rok Strniša
27
Hal yang sama berlaku dengan Netbeans: developmentality.wordpress.com/2010/08/24/…
seinecle
6
@Darthenius Eclipse yang dihasilkan sama dengan menggunakan getClass () yang dapat menyebabkan masalah dalam beberapa kasus (lihat item Java 8 yang Efektif)
AndroidGecko
7
Pemeriksaan nol pertama tidak diperlukan mengingat fakta bahwa instanceofmengembalikan false jika operan pertamanya nol (Java lagi efektif)
izaban
295

Ada beberapa masalah yang perlu diperhatikan jika Anda berurusan dengan kelas yang bertahan menggunakan Object-Relationship Mapper (ORM) seperti Hibernate, jika Anda tidak berpikir ini sudah terlalu rumit rumit!

Objek yang dimuat malas adalah subclass

Jika objek Anda tetap menggunakan ORM, dalam banyak kasus Anda akan berhadapan dengan proxy dinamis untuk menghindari memuat objek terlalu dini dari penyimpanan data. Proxy ini diimplementasikan sebagai subclass dari kelas Anda sendiri. Ini berarti this.getClass() == o.getClass()akan kembali false. Sebagai contoh:

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

Jika Anda berurusan dengan ORM, gunakan o instanceof Person adalah satu-satunya hal yang akan berperilaku benar.

Objek malas yang dimuat memiliki bidang-nol

ORM biasanya menggunakan getter untuk memaksa pemuatan objek bermuatan malas. Ini berarti bahwa person.nameakan nulljika persondimuat malas, bahkan jika person.getName()memaksa memuat dan mengembalikan "John Doe". Dalam pengalaman saya, ini muncul lebih sering di hashCode()danequals() .

Jika Anda berurusan dengan ORM, pastikan untuk selalu menggunakan getter, dan jangan pernah referensi bidang di hashCode()dan equals().

Menyimpan objek akan mengubah kondisinya

Objek persisten sering menggunakan idbidang untuk menahan kunci objek. Bidang ini akan diperbarui secara otomatis saat sebuah objek disimpan pertama kali. Jangan gunakan bidang id di hashCode(). Tapi Anda bisa menggunakannya equals().

Pola yang sering saya gunakan adalah

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

Tetapi: Anda tidak dapat menyertakan getId()di hashCode(). Jika Anda melakukannya, ketika suatu objek bertahan, ituhashCode perubahannya. Jika objek berada dalam a HashSet, Anda "tidak akan" menemukannya lagi.

Dalam Personcontoh saya , saya mungkin akan menggunakan getName()untuk hashCodedan getId()plus getName()(hanya untuk paranoia) untuk equals(). Tidak apa-apa jika ada beberapa risiko "tabrakan" untuk hashCode(), tetapi tidak pernah baik untuk itu equals().

hashCode() harus menggunakan subset properti yang tidak berubah dari equals()

Johannes Brodwall
sumber
2
@ Johannes Brodwall: saya tidak mengerti Saving an object will change it's state! hashCodeharus kembali int, jadi bagaimana Anda akan menggunakan getName()? Bisakah Anda memberi contoh untuk AndahashCode
jimmybondy
@jimmybondy: getName akan mengembalikan objek String yang juga memiliki
kode
85

Klarifikasi tentang obj.getClass() != getClass().

Pernyataan ini adalah hasil dari equals()pewarisan yang tidak ramah. JLS (spesifikasi bahasa Java) menetapkan bahwa jika A.equals(B) == truekemudian B.equals(A)harus juga kembali true. Jika Anda mengabaikan pernyataan yang mewarisi kelas yang menimpa equals()(dan mengubah perilakunya) akan melanggar spesifikasi ini.

Pertimbangkan contoh berikut tentang apa yang terjadi ketika pernyataan dihilangkan:

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

Melakukan new A(1).equals(new A(1))Juga, new B(1,1).equals(new B(1,1))hasil memberi yang benar, sebagaimana mestinya.

Ini terlihat sangat bagus, tetapi lihat apa yang terjadi jika kita mencoba menggunakan kedua kelas:

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

Jelas, ini salah.

Jika Anda ingin memastikan kondisi simetris. a = b jika b = a dan panggilan prinsip substitusi Liskov super.equals(other)tidak hanya dalam hal Binstance, tetapi periksa juga Amisalnya:

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;

Yang akan menghasilkan:

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

Di mana, jika abukan referensi B, maka itu mungkin menjadi referensi kelas A(karena Anda memperluasnya), dalam hal ini Anda menelepon super.equals() juga .

Ran Biron
sumber
2
Anda dapat membuat persamaan simetris dengan cara ini (jika membandingkan objek superclass dengan objek subclass, selalu gunakan equals dari subclass) if (obj.getClass ()! = This.getClass () && obj.getClass (). IsInstance (this) ) mengembalikan obj.equals (ini);
pihentagy
5
@pihentagy - maka saya akan mendapatkan stackoverflow ketika kelas implementasi tidak menimpa metode equals. tidak menyenangkan.
Ran Biron
2
Anda tidak akan mendapatkan aliran stackover. Jika metode equals tidak diganti, Anda akan memanggil kode yang sama lagi, tetapi kondisi untuk rekursi akan selalu salah!
Jacob Raihle
@pihentagy: Bagaimana perilakunya jika ada dua kelas turunan yang berbeda? Jika a ThingWithOptionSetAdapat sama dengan Thingasalkan semua opsi tambahan memiliki nilai default, dan juga untuk a ThingWithOptionSetB, maka harus mungkin untuk ThingWithOptionSetAdibandingkan dengan ThingWithOptionSetBhanya jika semua properti non-basis dari kedua objek cocok dengan default mereka, tetapi Saya tidak melihat bagaimana Anda menguji itu.
supercat
7
Masalahnya dengan ini yang merusak transitivitas. Jika Anda menambahkan B b2 = new B(1,99), lalu b.equals(a) == truedan a.equals(b2) == truetetapi b.equals(b2) == false.
jamaah
46

Untuk implementasi yang ramah-warisan, lihat solusi Tal Cohen, Bagaimana Saya Menerapkan Metode equals () dengan benar?

Ringkasan:

Dalam bukunya Panduan Bahasa Pemrograman Java yang Efektif (Addison-Wesley, 2001), Joshua Bloch mengklaim bahwa "Tidak ada cara untuk memperluas kelas yang mungkin ada dan menambahkan aspek sambil mempertahankan kontrak yang sama." Tal tidak setuju.

Solusinya adalah mengimplementasikan equals () dengan memanggil blindlyEquals nonsimetrik lain () keduanya. blindlyEquals () ditimpa oleh subclass, equals () diwarisi, dan tidak pernah diganti.

Contoh:

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

Perhatikan bahwa equals () harus bekerja lintas hierarki warisan jika Prinsip Substitusi Liskov harus dipenuhi.

Kevin Wong
sumber
10
Lihat metode canEqual yang dijelaskan di sini - prinsip yang sama membuat kedua solusi berfungsi, tetapi dengan canEqual Anda tidak membandingkan bidang yang sama dua kali (di atas, px == this.x akan diuji dalam dua arah): artima.com /lejava/articles/equality.html
Blaisorblade
2
Bagaimanapun, saya tidak berpikir ini adalah ide yang bagus. Itu membuat kontrak Equals membingungkan secara tidak perlu - seseorang yang mengambil dua parameter Poin, a dan b, harus menyadari kemungkinan bahwa a.getX () == b.getX () dan a.getY () dan a.getY () == b.getY () bisa benar, tetapi a.equals (b) dan b.equals (a) keduanya salah (jika hanya satu yang merupakan ColorPoint).
Kevin
Pada dasarnya, ini seperti if (this.getClass() != o.getClass()) return false, tetapi fleksibel karena hanya mengembalikan false jika kelas turunannya repot untuk memodifikasi sama. Apakah itu benar?
Aleksandr Dubinsky
31

Masih heran bahwa tidak ada yang merekomendasikan perpustakaan jambu biji untuk ini.

 //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());
    }
Eugene
sumber
23
java.util.Objects.hash () dan java.util.Objects.equals () adalah bagian dari Java 7 (dirilis pada 2011) sehingga Anda tidak memerlukan Guava untuk ini.
herman
1
tentu saja, tetapi Anda harus menghindari itu karena Oracle tidak lagi menyediakan pembaruan publik untuk Java 6 (ini telah terjadi sejak Februari 2013).
herman
6
Anda thisdi this.getDate()berarti apa-apa (selain kekacauan)
Steve Kuo
1
Anda "tidak instanceof" ekspresi membutuhkan braket tambahan: if (!(otherObject instanceof DateAndPattern)) {. Setuju dengan hernan dan Steve Kuo (meskipun itu masalah pilihan pribadi), tapi tetap saja +1.
Amos M. Carpenter
26

Ada dua metode dalam kelas super sebagai java.lang.Object. Kita perlu menimpanya ke objek kustom.

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

Objek yang sama harus menghasilkan kode hash yang sama selama mereka sama, namun objek yang tidak sama tidak perlu menghasilkan kode hash yang berbeda.

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
}

Jika Anda ingin mendapatkan lebih banyak, silakan periksa tautan ini sebagai http://www.javaranch.com/journal/2002/10/equalhash.html

Ini adalah contoh lain, http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html

Selamat bersenang-senang! @. @

Luna Kong
sumber
Maaf tapi saya tidak mengerti pernyataan ini tentang metode hashCode: tidak sah jika menggunakan lebih banyak variabel daripada equals (). Tetapi jika saya kode dengan lebih banyak variabel, kode saya dikompilasi. Kenapa itu tidak legal?
Adryr83
19

Ada beberapa cara untuk melakukan pemeriksaan kesetaraan kelas sebelum memeriksa kesetaraan anggota, dan saya pikir keduanya berguna dalam keadaan yang tepat.

  1. Gunakan instanceofoperator.
  2. Gunakan this.getClass().equals(that.getClass()).

Saya menggunakan # 1 dalam finalimplementasi equals, atau ketika mengimplementasikan antarmuka yang menentukan algoritma untuk equals (seperti java.utilantarmuka koleksi — cara yang tepat untuk memeriksa dengan(obj instanceof Set) atau antarmuka apa pun yang Anda laksanakan). Ini umumnya merupakan pilihan yang buruk ketika persamaan dapat ditimpa karena itu merusak properti simetri.

Opsi # 2 memungkinkan kelas untuk diperpanjang dengan aman tanpa mengesampingkan sama atau melanggar simetri.

Jika kelas Anda juga Comparable, metode equalsdan compareToharus konsisten juga. Inilah templat untuk metode sama dengan di Comparablekelas:

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

}
erickson
sumber
1
+1 untuk ini. Baik getClass () maupun instanceof adalah obat mujarab, dan ini adalah penjelasan yang bagus tentang cara mendekati keduanya. Jangan berpikir ada alasan untuk tidak melakukan this.getClass () == that.getClass () alih-alih menggunakan equals ().
Paul Cantrell
Ada satu masalah dengan ini. Kelas anonim yang tidak menambahkan aspek apa pun atau menimpa metode equals akan gagal dalam pemeriksaan getClass meskipun harus sama.
steinybot
@ Siiny Tidak jelas bagi saya bahwa objek dari berbagai jenis harus sama; Saya sedang memikirkan implementasi yang berbeda dari antarmuka sebagai kelas anonim yang umum. Bisakah Anda memberi contoh untuk mendukung premis Anda?
erickson
MyClass a = MyClass baru (123); MyClass b = MyClass baru (123) {// Override some method}; // a.equals (b) salah ketika menggunakan this.getClass (). equals (that.getClass ())
steinybot
1
@ Benar, Benar. Seperti yang seharusnya dalam banyak kasus, terutama jika metode diganti alih-alih ditambahkan. Perhatikan contoh saya di atas. Jika tidak final, dan compareTo()metodenya diganti untuk membalik urutan, contoh dari subclass dan superclass tidak boleh dianggap sama. Ketika objek-objek ini digunakan bersama-sama di pohon, kunci yang "sama" sesuai dengan instanceofimplementasi mungkin tidak dapat ditemukan.
erickson
16

Untuk yang sederajat, lihatlah Rahasia dari Persamaan oleh Angelika Langer . Saya sangat menyukainya. Dia juga FAQ yang bagus tentang Generik di Jawa . Lihat artikel lainnya di sini (gulir ke bawah ke "Core Java"), di mana ia juga melanjutkan dengan Bagian-2 dan "perbandingan tipe campuran". Selamat membaca!

Johannes Schaub - litb
sumber
11

metode equals () digunakan untuk menentukan kesetaraan dua objek.

karena nilai int 10 selalu sama dengan 10. Tapi metode equals () ini adalah tentang persamaan dua objek. Ketika kita mengatakan objek, ia akan memiliki properti. Untuk memutuskan kesetaraan sifat-sifat tersebut dipertimbangkan. Tidak perlu bahwa semua properti harus diperhitungkan untuk menentukan kesetaraan dan sehubungan dengan definisi dan konteks kelas itu dapat diputuskan. Kemudian metode equals () dapat diganti.

kita harus selalu mengganti metode hashCode () setiap kali kita mengganti metode equals (). Jika tidak, apa yang akan terjadi? Jika kita menggunakan hashtable dalam aplikasi kita, itu tidak akan berlaku seperti yang diharapkan. Karena kode hash digunakan dalam menentukan kesetaraan nilai yang disimpan, kode hash tidak akan mengembalikan nilai terkait yang tepat untuk kunci.

Implementasi default yang diberikan adalah metode hashCode () di kelas Object menggunakan alamat internal objek dan mengubahnya menjadi integer dan mengembalikannya.

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;

  }
}

Output Kode Contoh:

bengalTiger1 and bengalTiger2: true 
bengalTiger1 and siberianTiger: false 
bengalTiger1 hashCode: 1398212510 
bengalTiger2 hashCode: 1398212510 
siberianTiger hashCode: 1227465966
rohan kamat
sumber
7

Secara logis kami memiliki:

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

Tapi tidak sebaliknya!

Khaled.K
sumber
6

Satu gotcha yang saya temukan adalah di mana dua objek berisi referensi satu sama lain (satu contoh menjadi hubungan orang tua / anak dengan metode kenyamanan pada orang tua untuk mendapatkan semua anak).
Hal-hal semacam ini cukup umum ketika melakukan pemetaan Hibernate misalnya.

Jika Anda memasukkan kedua ujung hubungan dalam kode hash Anda atau sama dengan tes itu mungkin untuk masuk ke loop rekursif yang berakhir dengan StackOverflowException.
Solusi paling sederhana adalah tidak memasukkan koleksi getChildren dalam metode.

Darren Greaves
sumber
5
Saya pikir teori yang mendasari di sini adalah untuk membedakan antara atribut , agregat dan asosiasi dari suatu objek. The asssociations seharusnya tidak berpartisipasi dalam equals(). Jika seorang ilmuwan gila membuat duplikat saya, kami akan setara. Tetapi kita tidak akan memiliki ayah yang sama.
Raedwald