HashCode Java dasar dan sama dengan Demonstrasi

Saya sering menggunakan blog ini untuk mengunjungi kembali pelajaran yang diperoleh dengan susah payah tentang dasar-dasar Java. Posting blog ini adalah salah satu contohnya dan berfokus pada ilustrasi kekuatan berbahaya di balik metode sama (Object) dan hashCode (). Saya tidak akan membahas setiap nuansa dari dua metode yang sangat signifikan ini yang semua objek Java miliki baik secara eksplisit dideklarasikan atau secara implisit diwarisi dari induknya (mungkin langsung dari Object itu sendiri), tetapi saya akan membahas beberapa masalah umum yang muncul ketika ini adalah tidak diterapkan atau tidak diterapkan dengan benar. Saya juga mencoba untuk menunjukkan dengan demonstrasi ini mengapa penting untuk tinjauan kode yang cermat, pengujian unit menyeluruh, dan / atau analisis berbasis alat untuk memverifikasi kebenaran penerapan metode ini.

Karena semua objek Java pada akhirnya mewarisi implementasi untuk equals(Object)dan hashCode(), compiler Java dan Java runtime launcher tidak akan melaporkan masalah saat menjalankan "implementasi default" metode ini. Sayangnya, jika metode ini diperlukan, implementasi default metode ini (seperti sepupunya metode toString) jarang yang diinginkan. Berbasis Javadoc dokumentasi API untuk kelas Object membahas "kontrak" yang diharapkan dari setiap pelaksanaan equals(Object)dan hashCode()metode dan juga membahas implementasi standar kemungkinan masing-masing jika tidak diganti oleh anak kelas.

Untuk contoh dalam posting ini, saya akan menggunakan kelas HashAndEquals yang daftar kodenya ditampilkan di sebelah proses instansiasi objek dari berbagai kelas Person dengan tingkat dukungan hashCodedan equalsmetode yang berbeda.

HashAndEquals.java

package dustin.examples; import java.util.HashSet; import java.util.Set; import static java.lang.System.out; public class HashAndEquals { private static final String HEADER_SEPARATOR = "======================================================================"; private static final int HEADER_SEPARATOR_LENGTH = HEADER_SEPARATOR.length(); private static final String NEW_LINE = System.getProperty("line.separator"); private final Person person1 = new Person("Flintstone", "Fred"); private final Person person2 = new Person("Rubble", "Barney"); private final Person person3 = new Person("Flintstone", "Fred"); private final Person person4 = new Person("Rubble", "Barney"); public void displayContents() { printHeader("THE CONTENTS OF THE OBJECTS"); out.println("Person 1: " + person1); out.println("Person 2: " + person2); out.println("Person 3: " + person3); out.println("Person 4: " + person4); } public void compareEquality() { printHeader("EQUALITY COMPARISONS"); out.println("Person1.equals(Person2): " + person1.equals(person2)); out.println("Person1.equals(Person3): " + person1.equals(person3)); out.println("Person2.equals(Person4): " + person2.equals(person4)); } public void compareHashCodes() { printHeader("COMPARE HASH CODES"); out.println("Person1.hashCode(): " + person1.hashCode()); out.println("Person2.hashCode(): " + person2.hashCode()); out.println("Person3.hashCode(): " + person3.hashCode()); out.println("Person4.hashCode(): " + person4.hashCode()); } public Set addToHashSet() { printHeader("ADD ELEMENTS TO SET - ARE THEY ADDED OR THE SAME?"); final Set set = new HashSet(); out.println("Set.add(Person1): " + set.add(person1)); out.println("Set.add(Person2): " + set.add(person2)); out.println("Set.add(Person3): " + set.add(person3)); out.println("Set.add(Person4): " + set.add(person4)); return set; } public void removeFromHashSet(final Set sourceSet) { printHeader("REMOVE ELEMENTS FROM SET - CAN THEY BE FOUND TO BE REMOVED?"); out.println("Set.remove(Person1): " + sourceSet.remove(person1)); out.println("Set.remove(Person2): " + sourceSet.remove(person2)); out.println("Set.remove(Person3): " + sourceSet.remove(person3)); out.println("Set.remove(Person4): " + sourceSet.remove(person4)); } public static void printHeader(final String headerText) { out.println(NEW_LINE); out.println(HEADER_SEPARATOR); out.println("= " + headerText); out.println(HEADER_SEPARATOR); } public static void main(final String[] arguments) { final HashAndEquals instance = new HashAndEquals(); instance.displayContents(); instance.compareEquality(); instance.compareHashCodes(); final Set set = instance.addToHashSet(); out.println("Set Before Removals: " + set); //instance.person1.setFirstName("Bam Bam"); instance.removeFromHashSet(set); out.println("Set After Removals: " + set); } } 

Kelas di atas akan digunakan apa adanya berulang kali dengan hanya satu perubahan kecil nanti di pos. Namun, Personkelas akan diubah untuk mencerminkan pentingnya equalsdan hashCodedan untuk menunjukkan betapa mudahnya mengacaukan ini sementara pada saat yang sama sulit untuk melacak masalah ketika ada kesalahan.

Tidak Ada Eksplisit equalsatau hashCodeMetode

Versi pertama Personkelas tidak menyediakan versi yang diganti secara eksplisit dari equalsmetode atau hashCodemetode tersebut. Ini akan mendemonstrasikan "implementasi default" dari setiap metode yang diwarisi dari Object. Berikut adalah kode sumber untuk Persontanpa hashCodeatau equalsdiganti secara eksplisit.

Person.java (tidak ada hashCode eksplisit atau metode yang sama)

package dustin.examples; public class Person { private final String lastName; private final String firstName; public Person(final String newLastName, final String newFirstName) { this.lastName = newLastName; this.firstName = newFirstName; } @Override public String toString() { return this.firstName + " " + this.lastName; } } 

Versi pertama Personini tidak menyediakan metode get / set dan tidak menyediakan equalsatau hashCodeimplementasi. Ketika kelas demonstrasi utama HashAndEqualsdijalankan dengan instance dari kelas -less equalsdan hashCode-less ini Person, hasilnya akan muncul seperti yang ditampilkan di snapshot layar berikutnya.

Beberapa pengamatan dapat dilakukan dari keluaran yang ditunjukkan di atas. Pertama, tanpa implementasi eksplisit dari suatu equals(Object)metode, tidak ada instance Personyang dianggap sama, meskipun semua atribut instance (dua String) identik. Ini karena, seperti yang dijelaskan dalam dokumentasi untuk Object.equals (Object), equalsimplementasi default didasarkan pada referensi yang sama persis:

Metode sama untuk kelas Objek mengimplementasikan kemungkinan relasi ekivalen yang paling diskriminatif pada objek; yaitu, untuk nilai referensi non-null x dan y, metode ini mengembalikan true jika dan hanya jika x dan y merujuk ke objek yang sama (x == y memiliki nilai true).

Pengamatan kedua dari contoh pertama ini adalah bahwa kode hash berbeda untuk setiap contoh Personobjek bahkan ketika dua contoh berbagi nilai yang sama untuk semua atributnya. HashSet kembali trueketika objek "unik" ditambahkan (HashSet.add) ke set atau falsejika objek yang ditambahkan tidak dianggap unik sehingga tidak ditambahkan. Demikian pula, HashSetmetode hapus kembali truejika objek yang disediakan dianggap ditemukan dan dihapus atau falsejika objek yang ditentukan dianggap bukan bagian dari HashSetdan karenanya tidak dapat dihapus. Karena metode default equalsdan yang hashCodediwarisi memperlakukan instance ini sama sekali berbeda, tidak mengherankan jika semua ditambahkan ke set dan semuanya berhasil dihapus dari set.

equalsMetode Eksplisit Saja

Versi kedua dari Personkelas menyertakan equalsmetode yang diganti secara eksplisit seperti yang ditunjukkan dalam daftar kode berikutnya.

Person.java (eksplisit sama dengan metode yang disediakan)

package dustin.examples; public class Person { private final String lastName; private final String firstName; public Person(final String newLastName, final String newFirstName) { this.lastName = newLastName; this.firstName = newFirstName; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (this.getClass() != obj.getClass()) { return false; } final Person other = (Person) obj; if (this.lastName == null ? other.lastName != null : !this.lastName.equals(other.lastName)) { return false; } if (this.firstName == null ? other.firstName != null : !this.firstName.equals(other.firstName)) { return false; } return true; } @Override public String toString() { return this.firstName + " " + this.lastName; } } 

Jika instance ini Persondengan yang equals(Object)ditentukan secara eksplisit digunakan, hasilnya akan seperti yang ditampilkan di cuplikan layar berikutnya.

Pengamatan pertama adalah bahwa sekarang equalspanggilan pada Personinstance memang kembali trueketika objeknya sama dalam hal semua atribut menjadi sama daripada memeriksa persamaan referensi yang ketat. Ini menunjukkan bahwa equalsimplementasi kustom di Persontelah melakukan tugasnya. Pengamatan kedua adalah bahwa penerapan equalsmetode tidak berpengaruh pada kemampuan untuk menambah dan menghapus objek yang tampaknya sama ke HashSet.

Eksplisit equalsdan hashCodeMetode

Sekarang saatnya menambahkan hashCode()metode eksplisit ke Personkelas. Memang, ini benar-benar harus dilakukan ketika equalsmetode itu diterapkan. Alasan untuk ini dinyatakan dalam dokumentasi untuk Object.equals(Object)metode tersebut:

Perhatikan bahwa secara umum metode hashCode harus diganti setiap kali metode ini diganti, untuk mempertahankan kontrak umum metode hashCode, yang menyatakan bahwa objek yang sama harus memiliki kode hash yang sama.

Berikut adalah Persondengan hashCodemetode yang diimplementasikan secara eksplisit berdasarkan atribut yang sama Personsebagai equalsmetode.

Person.java (sama dengan eksplisit dan implementasi kode hash)

package dustin.examples; public class Person { private final String lastName; private final String firstName; public Person(final String newLastName, final String newFirstName) { this.lastName = newLastName; this.firstName = newFirstName; } @Override public int hashCode() { return lastName.hashCode() + firstName.hashCode(); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (this.getClass() != obj.getClass()) { return false; } final Person other = (Person) obj; if (this.lastName == null ? other.lastName != null : !this.lastName.equals(other.lastName)) { return false; } if (this.firstName == null ? other.firstName != null : !this.firstName.equals(other.firstName)) { return false; } return true; } @Override public String toString() { return this.firstName + " " + this.lastName; } } 

Output dari menjalankan dengan Personkelas baru dengan hashCodedan equalsmetode ditampilkan berikutnya.

Tidak mengherankan bahwa kode hash yang dikembalikan untuk objek dengan nilai atribut yang sama sekarang sama, tetapi pengamatan yang lebih menarik adalah kita hanya dapat menambahkan dua dari empat contoh ke HashSetsekarang. Ini karena upaya penambahan ketiga dan keempat dianggap mencoba menambahkan objek yang sudah ditambahkan ke set. Karena hanya ada dua yang ditambahkan, hanya dua yang dapat ditemukan dan dihapus.

Masalah dengan Atribut HashCode yang Dapat Diubah

Untuk contoh keempat dan terakhir dalam posting ini, saya melihat apa yang terjadi ketika hashCodeimplementasi didasarkan pada atribut yang berubah. Untuk contoh ini, setFirstNamemetode ditambahkan ke Persondan finalpengubah dihapus dari firstNameatributnya. Selain itu, kelas utama HashAndEquals harus menghapus komentar dari baris yang memanggil metode set baru ini. Versi baru Personditampilkan berikutnya.

package dustin.examples; public class Person { private final String lastName; private String firstName; public Person(final String newLastName, final String newFirstName) { this.lastName = newLastName; this.firstName = newFirstName; } @Override public int hashCode() { return lastName.hashCode() + firstName.hashCode(); } public void setFirstName(final String newFirstName) { this.firstName = newFirstName; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (this.getClass() != obj.getClass()) { return false; } final Person other = (Person) obj; if (this.lastName == null ? other.lastName != null : !this.lastName.equals(other.lastName)) { return false; } if (this.firstName == null ? other.firstName != null : !this.firstName.equals(other.firstName)) { return false; } return true; } @Override public String toString() { return this.firstName + " " + this.lastName; } } 

Output yang dihasilkan dari menjalankan contoh ini ditampilkan berikutnya.