Java sama dengan () dan kontrak hashCode ()

1. Gambaran keseluruhan

Dalam tutorial ini, kami akan memperkenalkan dua kaedah yang saling berkaitan : sama () dan hashCode () . Kami akan memfokuskan pada hubungan mereka antara satu sama lain, cara mengatasi mereka dengan betul, dan mengapa kita harus mengatasi kedua-duanya atau tidak.

2. sama dengan ()

The Objek mentakrifkan kelas kedua-dua sama dengan () dan Kodcincang () kaedah - yang bermaksud bahawa kedua-dua kaedah yang tersirat yang ditakrifkan dalam setiap kelas Java, termasuk yang kita buat:

class Money { int amount; String currencyCode; }
Money income = new Money(55, "USD"); Money expenses = new Money(55, "USD"); boolean balanced = income.equals(expenses)

Kami menjangkakan pendapatan.sama (perbelanjaan) akan menjadi kenyataan . Tetapi dengan kelas Wang dalam bentuknya sekarang, ia tidak akan berlaku.

Pelaksanaan default sama dengan () dalam kelas Objek mengatakan bahawa persamaan adalah sama dengan identiti objek. Dan pendapatan dan perbelanjaan adalah dua keadaan yang berbeza.

2.1. Mengatasi sama dengan ()

Mari ganti kaedah sama () sehingga tidak hanya mempertimbangkan identiti objek, tetapi juga nilai dua sifat yang relevan:

@Override public boolean equals(Object o)  if (o == this) return true; if (!(o instanceof Money)) return false; Money other = (Money)o; boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null) 

2.2. sama dengan () Kontrak

Java SE menentukan kontrak yang mesti dipenuhi oleh pelaksanaan kaedah sama () . Sebilangan besar kriteria adalah akal sehat. Kaedah sama () mestilah:

  • refleksif : objek mesti sama dengan dirinya sendiri
  • simetrik : x.sama (y) mesti menghasilkan hasil yang sama dengan y.sama (x)
  • transitif : jika x.sama (y) dan y.sama (z) maka juga x.sama (z)
  • konsisten : nilai sama () harus berubah hanya jika harta yang terkandung dalam sama () berubah (tiada rawak dibenarkan)

Kita dapat mencari kriteria yang tepat di Java SE Docs untuk kelas Objek .

2.3. Melanggar sama () Simetri Dengan Warisan

Sekiranya kriteria untuk sama () adalah akal sehat, bagaimana kita dapat melanggarnya sama sekali? Nah, pelanggaran berlaku paling kerap, jika kita memanjangkan kelas yang telah diganti sama dengan () . Mari pertimbangkan kelas Baucar yang meluaskan kelas Wang kami :

class WrongVoucher extends Money { private String store; @Override public boolean equals(Object o)  // other methods }

Pada pandangan pertama, kelas Voucher dan penggantiannya untuk sama () nampaknya betul. Dan kedua-dua kaedah sama dengan () berkelakuan betul sepanjang kita membandingkan Wang dengan Wang atau Voucher dengan Voucher . Tetapi apa yang berlaku, jika kita membandingkan dua objek ini?

Money cash = new Money(42, "USD"); WrongVoucher voucher = new WrongVoucher(42, "USD", "Amazon"); voucher.equals(cash) => false // As expected. cash.equals(voucher) => true // That's wrong.

Itu melanggar kriteria simetri kontrak sama () .

2.4. Memperbaiki sama dengan () Simetri Dengan Komposisi

Untuk mengelakkan masalah ini, kita harus memilih komposisi daripada harta pusaka.

Daripada subkelas Wang , mari buat kelas Baucar dengan harta Wang :

class Voucher { private Money value; private String store; Voucher(int amount, String currencyCode, String store) { this.value = new Money(amount, currencyCode); this.store = store; } @Override public boolean equals(Object o)  // other methods }

Dan sekarang, sama akan berfungsi secara simetri seperti yang dikehendaki oleh kontrak.

3. hashCode ()

hashCode () mengembalikan bilangan bulat yang mewakili contoh semasa kelas. Kita harus mengira nilai ini selaras dengan definisi persamaan untuk kelas. Oleh itu, jika kita mengatasi kaedah sama () , kita juga harus mengatasi hashCode () .

Untuk maklumat lanjut, lihat panduan kami untuk hashCode () .

3.1. hashCode () Kontrak

Java SE juga menentukan kontrak untuk kaedah hashCode () . Jika kita mengkaji secara menyeluruh di ia menunjukkan berapa rapat berkaitan Kodcincang () dan sama dengan () berada.

Ketiga-tiga kriteria dalam kontrak hashCode () menyebutkan dalam beberapa kaedah kaedah sama () :

  • ketekalan dalaman : nilai hashCode () hanya boleh berubah jika harta yang sama dengan () berubah
  • sama dengan konsistensi : objek yang sama antara satu sama lain mesti mengembalikan hashCode yang sama
  • perlanggaran : objek yang tidak sama mungkin mempunyai kod hash yang sama

3.2. Melanggar Konsistensi hashCode () dan sama dengan ()

Kriteria ke-2 kontrak kaedah hashCode mempunyai konsekuensi penting: Sekiranya kita menimpa sama dengan (), kita juga mesti mengatasi hashCode (). Dan ini adalah pelanggaran yang paling meluas mengenai kontrak kaedah sama () dan hashCode () .

Mari lihat contoh seperti itu:

class Team { String city; String department; @Override public final boolean equals(Object o) { // implementation } }

The Team mengatasi kelas hanya sama dengan () , tetapi ia masih tersirat menggunakan pelaksanaan lalai Kodcincang () sebagaimana yang ditakrifkan dalam Objek kelas. Dan ini mengembalikan hashCode () yang berbeza untuk setiap contoh kelas. Ini melanggar peraturan kedua.

Sekarang jika kita membuat dua objek Pasukan , baik dengan kota "New York" dan departemen "pemasaran", mereka akan sama, tetapi mereka akan mengembalikan kod hash yang berbeza.

3.3. Kunci HashMap Dengan Kod hash yang Tidak Konsisten ()

But why is the contract violation in our Team class a problem? Well, the trouble starts when some hash-based collections are involved. Let's try to use our Team class as a key of a HashMap:

Map leaders = new HashMap(); leaders.put(new Team("New York", "development"), "Anne"); leaders.put(new Team("Boston", "development"), "Brian"); leaders.put(new Team("Boston", "marketing"), "Charlie"); Team myTeam = new Team("New York", "development"); String myTeamLeader = leaders.get(myTeam);

We would expect myTeamLeader to return “Anne”. But with the current code, it doesn't.

If we want to use instances of the Team class as HashMap keys, we have to override the hashCode() method so that it adheres to the contract: Equal objects return the same hashCode.

Let's see an example implementation:

@Override public final int hashCode() { int result = 17; if (city != null) { result = 31 * result + city.hashCode(); } if (department != null) { result = 31 * result + department.hashCode(); } return result; }

After this change, leaders.get(myTeam) returns “Anne” as expected.

4. When Do We Override equals() and hashCode()?

Generally, we want to override either both of them or neither of them. We've just seen in Section 3 the undesired consequences if we ignore this rule.

Domain-Driven Design can help us decide circumstances when we should leave them be. For entity classes – for objects having an intrinsic identity – the default implementation often makes sense.

However, for value objects, we usually prefer equality based on their properties. Thus want to override equals() and hashCode(). Remember our Money class from Section 2: 55 USD equals 55 USD – even if they're two separate instances.

5. Implementation Helpers

We typically don't write the implementation of these methods by hand. As can be seen, there are quite a few pitfalls.

One common way is to let our IDE generate the equals() and hashCode() methods.

Apache Commons Lang and Google Guava have helper classes in order to simplify writing both methods.

Project Lombok also provides an @EqualsAndHashCode annotation. Note again how equals() and hashCode() “go together” and even have a common annotation.

6. Verifying the Contracts

If we want to check whether our implementations adhere to the Java SE contracts and also to some best practices, we can use the EqualsVerifier library.

Let's add the EqualsVerifier Maven test dependency:

 nl.jqno.equalsverifier equalsverifier 3.0.3 test 

Let's verify that our Team class follows the equals() and hashCode() contracts:

@Test public void equalsHashCodeContracts() { EqualsVerifier.forClass(Team.class).verify(); }

It's worth noting that EqualsVerifier tests both the equals() and hashCode() methods.

EqualsVerifier is much stricter than the Java SE contract. For example, it makes sure that our methods can't throw a NullPointerException. Also, it enforces that both methods, or the class itself, is final.

It's important to realize that the default configuration of EqualsVerifier allows only immutable fields. This is a stricter check than what the Java SE contract allows. This adheres to a recommendation of Domain-Driven Design to make value objects immutable.

If we find some of the built-in constraints unnecessary, we can add a suppress(Warning.SPECIFIC_WARNING) to our EqualsVerifier call.

7. Conclusion

In this article, we've discussed the equals() and hashCode() contracts. We should remember to:

  • Always override hashCode() if we override equals()
  • Override sama dengan () dan hashCode () untuk objek nilai
  • Berhati-hatilah dengan perangkap memperluas kelas yang telah mengatasi sama dengan () dan hashCode ()
  • Pertimbangkan menggunakan IDE atau perpustakaan pihak ketiga untuk menjana setaraf () dan Kodcincang () kaedah
  • Pertimbangkan untuk menggunakan EqualsVerifier untuk menguji pelaksanaan kami

Akhirnya, semua contoh kod boleh didapati di GitHub.