Kontrak Java sama dengan () dan hashCode ()

1. Ikhtisar

Dalam tutorial ini, kami akan memperkenalkan dua metode yang saling terkait: equals () dan hashCode () . Kami akan fokus pada hubungan mereka satu sama lain, cara menimpanya dengan benar, dan mengapa kami harus menimpa keduanya atau tidak keduanya.

2. sama dengan ()

Kelas Object mendefinisikan metode equals () dan hashCode () - yang berarti bahwa kedua metode ini secara implisit didefinisikan di 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 berharap income.equals (biaya) kembali true . Tetapi dengan kelas Uang dalam bentuknya saat ini, itu tidak akan terjadi.

Implementasi default dari equals () di kelas Object mengatakan bahwa kesamaan adalah sama dengan identitas objek. Dan pendapatan dan pengeluaran adalah dua contoh yang berbeda.

2.1. Mengganti sama dengan ()

Mari kita mengganti metode equals () sehingga tidak hanya mempertimbangkan identitas objek, tetapi juga nilai dari dua properti 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 mendefinisikan kontrak yang harus dipenuhi oleh implementasi metode equals () . Sebagian besar kriteria tersebut masuk akal. Metode equals () harus:

  • refleksif : suatu objek harus sama dengan dirinya sendiri
  • simetris : x.equals (y) harus mengembalikan hasil yang sama seperti y.equals (x)
  • transitif : jika x.equals (y) dan y.equals (z) maka x.equals (z)
  • konsisten : nilai equals () harus berubah hanya jika properti yang terkandung dalam equals () berubah (tidak boleh ada keacakan)

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

2.3. Melanggar sama dengan () Simetri Dengan Warisan

Jika kriteria untuk sama () seperti itu, bagaimana kita bisa melanggarnya? Nah, pelanggaran paling sering terjadi, jika kita memperluas kelas yang telah diganti sama dengan () . Mari pertimbangkan kelas Voucher yang memperluas kelas Uang kita :

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

Sekilas, kelas Voucher dan penggantinya untuk sama dengan () tampaknya benar. Dan kedua metode equals () berfungsi dengan benar selama kita membandingkan Uang dengan Uang atau Voucher dengan Voucher . Tetapi apa yang terjadi, jika kita membandingkan kedua benda 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 dari kontrak equals () .

2.4. Memperbaiki sama dengan () Simetri Dengan Komposisi

Untuk menghindari jebakan ini, kita harus mengutamakan komposisi daripada warisan.

Alih-alih membuat subclass Money , mari buat kelas Voucher dengan properti Money :

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, persamaan akan bekerja secara simetris seperti yang disyaratkan kontrak.

3. hashCode ()

hashCode () mengembalikan integer yang mewakili instance kelas saat ini. Kita harus menghitung nilai ini sesuai dengan definisi persamaan untuk kelas. Jadi jika kita menimpa metode equals () , kita juga harus mengganti hashCode () .

Untuk beberapa detail lebih lanjut, lihat panduan kami untuk hashCode () .

3.1. kontrak hashCode ()

Java SE juga mendefinisikan kontrak untuk metode hashCode () . Jika dilihat secara menyeluruh, ini menunjukkan seberapa erat kaitan hashCode () dan sama dengan () .

Ketiga kriteria dalam kontrak hashCode () menyebutkan dalam beberapa cara metode equals () :

  • konsistensi internal : nilai hashCode () hanya dapat berubah jika properti yang ada di sama dengan () berubah
  • sama dengan konsistensi : objek yang sama satu sama lain harus mengembalikan kode hash yang sama
  • tabrakan : objek yang tidak sama mungkin memiliki kode hash yang sama

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

Kriteria kedua dari kontrak metode hashCode memiliki konsekuensi penting: Jika kita menimpa sama dengan (), kita juga harus mengganti hashCode (). Dan ini adalah pelanggaran paling luas terkait metode contract of the equals () dan hashCode () .

Mari kita lihat contoh seperti itu:

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

Kelas Team menimpa hanya sama dengan () , tetapi masih secara implisit menggunakan implementasi default hashCode () seperti yang didefinisikan di kelas Object . Dan ini mengembalikan hashCode () yang berbeda untuk setiap instance kelas. Ini melanggar aturan kedua.

Sekarang jika kita membuat dua objek Tim , keduanya dengan kota "New York" dan departemen "pemasaran", keduanya akan sama, tetapi mereka akan mengembalikan kode hash yang berbeda.

3.3. Kunci HashMap Dengan hashCode () 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()
  • Ganti sama dengan () dan hashCode () untuk objek nilai
  • Waspadai perangkap kelas perluasan yang telah diganti sama dengan () dan hashCode ()
  • Pertimbangkan untuk menggunakan IDE atau perpustakaan pihak ketiga untuk menghasilkan equals () dan hashCode () metode
  • Pertimbangkan untuk menggunakan EqualsVerifier untuk menguji implementasi kami

Akhirnya, semua contoh kode dapat ditemukan di GitHub.