JavaRush /Blog Java /Random-MS /kaedah equals & hashCode: amalan penggunaan

kaedah equals & hashCode: amalan penggunaan

Diterbitkan dalam kumpulan
hello! Hari ini kita akan bercakap tentang dua kaedah penting dalam Java - equals()dan hashCode(). Ini bukan kali pertama kami bertemu dengan mereka: pada permulaan kursus JavaRush terdapat kuliah ringkas tentang - equals()baca jika anda terlupa atau belum melihatnya sebelum ini. Kaedah bersamaan &  Kod hash: amalan penggunaan - 1Dalam pelajaran hari ini kita akan bercakap tentang konsep ini secara terperinci - percayalah, banyak perkara yang perlu dibincangkan! Dan sebelum kita beralih kepada sesuatu yang baharu, mari kita segarkan semula ingatan kita tentang perkara yang telah kita bincangkan :) Seperti yang anda ingat, perbandingan biasa dua objek menggunakan operator “ ==” adalah idea yang tidak baik, kerana “ ==” membandingkan rujukan. Berikut ialah contoh kami dengan kereta dari kuliah baru-baru ini:
public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car car1 = new Car();
       car1.model = "Ferrari";
       car1.maxSpeed = 300;

       Car car2 = new Car();
       car2.model = "Ferrari";
       car2.maxSpeed = 300;

       System.out.println(car1 == car2);
   }
}
Output konsol:

false
Nampaknya kami telah mencipta dua objek kelas yang sama Car: semua medan pada kedua-dua mesin adalah sama, tetapi hasil perbandingan masih palsu. Kita sudah tahu sebabnya: pautan car1dan car2menunjuk ke alamat yang berbeza dalam ingatan, jadi mereka tidak sama. Kami masih mahu membandingkan dua objek, bukan dua rujukan. Penyelesaian terbaik untuk membandingkan objek ialah equals().

kaedah sama dengan().

Anda mungkin ingat bahawa kami tidak mencipta kaedah ini dari awal, tetapi mengatasinya - selepas semua, kaedah itu equals()ditakrifkan dalam kelas Object. Walau bagaimanapun, dalam bentuk biasa ia tidak banyak digunakan:
public boolean equals(Object obj) {
   return (this == obj);
}
Ini adalah bagaimana kaedah equals()ditakrifkan dalam kelas Object. Perbandingan pautan yang sama. Kenapa dia dijadikan begini? Nah, bagaimanakah pencipta bahasa itu mengetahui objek dalam program anda yang dianggap sama dan yang mana tidak? :) Ini adalah idea utama kaedah equals()- pencipta kelas itu sendiri menentukan ciri-ciri yang mana kesamaan objek kelas ini diperiksa. Dengan melakukan ini, anda mengatasi kaedah equals()dalam kelas anda. Jika anda tidak begitu memahami maksud "anda menentukan ciri-ciri itu sendiri," mari lihat contoh. Berikut ialah kelas orang yang mudah - Man.
public class Man {

   private String noseSize;
   private String eyesColor;
   private String haircut;
   private boolean scars;
   private int dnaCode;

public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
   this.noseSize = noseSize;
   this.eyesColor = eyesColor;
   this.haircut = haircut;
   this.scars = scars;
   this.dnaCode = dnaCode;
}

   //getters, setters, etc.
}
Katakan kita sedang menulis program yang perlu menentukan sama ada dua orang mempunyai pertalian kembar, atau hanya doppelgänger. Kami mempunyai lima ciri: saiz hidung, warna mata, gaya rambut, kehadiran parut dan keputusan ujian biologi DNA (untuk kesederhanaan - dalam bentuk nombor kod). Antara ciri ini, yang manakah anda fikir akan membolehkan program kami mengenal pasti saudara kembar? Kaedah bersamaan &  Kod hash: amalan penggunaan - 2Sudah tentu, hanya ujian biologi boleh memberikan jaminan. Dua orang boleh mempunyai warna mata, gaya rambut, hidung, dan juga parut yang sama - terdapat ramai orang di dunia, dan adalah mustahil untuk mengelakkan kebetulan. Kami memerlukan mekanisme yang boleh dipercayai: hanya hasil ujian DNA membolehkan kami membuat kesimpulan yang tepat. Apakah maksud ini untuk kaedah kami equals()? Kami perlu mentakrifkannya semula dalam kelas Mandengan mengambil kira keperluan program kami. Kaedah mesti membandingkan medan int dnaCodedua objek, dan jika mereka sama, maka objek adalah sama.
@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Adakah ia benar-benar semudah itu? Tidak juga. Kami terlepas sesuatu. Dalam kes ini, untuk objek kami, kami telah mentakrifkan hanya satu medan "penting" di mana kesamaan mereka ditubuhkan - dnaCode. Sekarang bayangkan bahawa kita tidak akan mempunyai 1, tetapi 50 medan "penting" sedemikian. Dan jika semua 50 medan dua objek adalah sama, maka objek adalah sama. Ini juga boleh berlaku. Masalah utama ialah pengiraan kesamaan 50 medan adalah proses yang memakan masa dan memakan sumber. Sekarang bayangkan bahawa sebagai tambahan kepada kelas, Mankita mempunyai kelas Womandengan medan yang sama seperti dalam Man. Dan jika pengaturcara lain menggunakan kelas anda, dia boleh dengan mudah menulis dalam programnya sesuatu seperti:
public static void main(String[] args) {

   Man man = new Man(........); //a bunch of parameters in the constructor

   Woman woman = new Woman(.........);//same bunch of parameters.

   System.out.println(man.equals(woman));
}
Dalam kes ini, tidak ada gunanya menyemak nilai medan: kita melihat bahawa kita melihat objek dua kelas yang berbeza, dan mereka tidak boleh sama pada dasarnya! Ini bermakna kita perlu meletakkan semakan dalam kaedah equals()—perbandingan objek dua kelas yang sama. Ada baiknya kita memikirkan perkara ini!
@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Tetapi mungkin kita terlupa sesuatu yang lain? Hmm... Sekurang-kurangnya, kita harus menyemak bahawa kita tidak membandingkan objek itu dengan dirinya sendiri! Jika rujukan A dan B menghala ke alamat yang sama dalam ingatan, maka ia adalah objek yang sama, dan kami juga tidak perlu membuang masa membandingkan 50 medan.
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Di samping itu, tidak ada salahnya untuk menambah cek untuk null: tiada objek boleh sama dengan null, dalam hal ini tiada gunanya pemeriksaan tambahan. Dengan mengambil kira semua ini, kaedah equals()kelas kami Manakan kelihatan seperti ini:
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Kami menjalankan semua pemeriksaan awal yang dinyatakan di atas. Jika ternyata:
  • kita membandingkan dua objek daripada kelas yang sama
  • ini bukan objek yang sama
  • kita tidak membandingkan objek kita dengannull
... kemudian kita beralih kepada membandingkan ciri-ciri penting. Dalam kes kami, medan dnaCodedua objek. Apabila mengatasi kaedah equals(), pastikan anda mematuhi keperluan ini:
  1. Reflekstiviti.

    Apa-apa objek mestilah equals()untuk dirinya sendiri.
    Kami telah pun mengambil kira keperluan ini. Kaedah kami menyatakan:

    if (this == o) return true;

  2. simetri.

    Jika a.equals(b) == true, maka b.equals(a)ia harus kembali true.
    Kaedah kami juga memenuhi keperluan ini.

  3. Transitiviti.

    Jika dua objek adalah sama dengan beberapa objek ketiga, maka mereka mesti sama antara satu sama lain.
    Jika a.equals(b) == truedan a.equals(c) == true, maka semakan itu b.equals(c)juga harus kembali benar.

  4. Kekal.

    Hasil kerja equals()harus berubah hanya apabila medan yang disertakan di dalamnya berubah. Jika data dua objek tidak berubah, keputusan semakan equals()hendaklah sentiasa sama.

  5. Ketaksamaan dengan null.

    Untuk sebarang objek, semakan a.equals(null)mesti mengembalikan palsu.
    Ini bukan hanya satu set beberapa "cadangan berguna", tetapi kontrak kaedah yang ketat , yang ditetapkan dalam dokumentasi Oracle

kaedah hashCode().

Sekarang mari kita bercakap tentang kaedah hashCode(). Mengapa ia diperlukan? Tepat untuk tujuan yang sama - membandingkan objek. Tetapi kami sudah memilikinya equals()! Kenapa kaedah lain? Jawapannya mudah: untuk meningkatkan produktiviti. Fungsi cincang, yang diwakili oleh kaedah , dalam Java hashCode(), mengembalikan nilai berangka tetap untuk sebarang objek. Dalam kes Java, kaedah hashCode()mengembalikan nombor 32-bit jenis int. Membandingkan dua nombor antara satu sama lain adalah lebih pantas daripada membandingkan dua objek menggunakan kaedah equals(), terutamanya jika ia menggunakan banyak medan. Jika program kami akan membandingkan objek, lebih mudah untuk melakukan ini dengan kod cincang, dan hanya jika ia sama dengan hashCode()- teruskan ke perbandingan dengan equals(). Secara kebetulan, ini ialah cara struktur data berasaskan cincang berfungsi—contohnya, yang anda tahu HashMap! Kaedahnya hashCode(), sama seperti equals(), ditindih oleh pembangun sendiri. Dan sama seperti untuk equals(), kaedah ini hashCode()mempunyai keperluan rasmi yang dinyatakan dalam dokumentasi Oracle:
  1. Jika dua objek adalah sama (iaitu, kaedah equals()mengembalikan benar), ia mesti mempunyai kod cincang yang sama.

    Jika tidak kaedah kita akan menjadi tidak bermakna. Menyemak oleh hashCode(), seperti yang kami katakan, harus didahulukan untuk meningkatkan prestasi. Jika kod cincang berbeza, semakan akan mengembalikan palsu, walaupun objek sebenarnya sama (seperti yang kami takrifkan dalam kaedah equals()).

  2. Jika kaedah hashCode()dipanggil berbilang kali pada objek yang sama, ia harus mengembalikan nombor yang sama setiap kali.

  3. Peraturan 1 tidak berfungsi secara terbalik. Dua objek berbeza boleh mempunyai kod cincang yang sama.

Peraturan ketiga agak mengelirukan. Bagaimana ini boleh terjadi? Penerangannya agak mudah. Kaedah hashCode()kembali int. intialah nombor 32-bit. Ia mempunyai bilangan nilai yang terhad - daripada -2,147,483,648 hingga +2,147,483,647. Dalam erti kata lain, terdapat hanya lebih 4 bilion variasi nombor int. Sekarang bayangkan anda sedang mencipta program untuk menyimpan data tentang semua orang yang hidup di Bumi. Setiap orang akan mempunyai objek kelasnya sendiri Man. ~7.5 bilion orang tinggal di bumi. Dalam erti kata lain, tidak kira betapa baiknya algoritma Manyang kita tulis untuk menukar objek kepada nombor, kita tidak akan mempunyai nombor yang mencukupi. Kami hanya mempunyai 4.5 bilion pilihan, dan ramai lagi orang. Ini bermakna tidak kira betapa sukarnya kita mencuba, kod cincang akan tetap sama untuk sesetengah orang yang berbeza. Situasi ini (kod cincangan dua objek berbeza yang sepadan) dipanggil perlanggaran. Salah satu matlamat pengaturcara apabila mengatasi kaedah hashCode()adalah untuk mengurangkan jumlah potensi perlanggaran sebanyak mungkin. Apakah rupa kaedah kami hashCode()untuk kelas Man, dengan mengambil kira semua peraturan ini? seperti ini:
@Override
public int hashCode() {
   return dnaCode;
}
Terkejut? :) Tak sangka, tapi bila tengok syarat-syarat tu, memang kita akan akur semua. Objek yang kami equals()kembalikan benar akan sama dalam hashCode(). Jika dua objek kami Manadalah sama nilai equals(iaitu, ia mempunyai nilai yang sama dnaCode), kaedah kami akan mengembalikan nombor yang sama. Mari kita lihat contoh yang lebih rumit. Katakan program kami harus memilih kereta mewah untuk pelanggan pengumpul. Mengumpul adalah perkara yang kompleks, dan terdapat banyak ciri untuknya. Sebuah kereta dari tahun 1963 boleh berharga 100 kali lebih mahal daripada kereta yang sama dari tahun 1964. Sebuah kereta merah dari tahun 1970 boleh berharga 100 kali ganda lebih mahal daripada kereta biru buatan yang sama dari tahun yang sama. Kaedah bersamaan &  Kod hash: amalan penggunaan - 4Dalam kes pertama, dengan kelas Man, kami membuang kebanyakan medan (iaitu, ciri orang) sebagai tidak penting dan hanya menggunakan medan untuk perbandingan dnaCode. Di sini kami bekerja dengan kawasan yang sangat unik, dan tidak boleh ada butiran kecil! Inilah kelas kami LuxuryAuto:
public class LuxuryAuto {

   private String model;
   private int manufactureYear;
   private int dollarPrice;

   public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
       this.model = model;
       this.manufactureYear = manufactureYear;
       this.dollarPrice = dollarPrice;
   }

   //... getters, setters, etc.
}
Di sini, apabila membandingkan, kita mesti mengambil kira semua bidang. Sebarang kesilapan boleh menelan kos ratusan ribu dolar untuk pelanggan, jadi lebih baik untuk selamat:
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   if (dollarPrice != that.dollarPrice) return false;
   return model.equals(that.model);
}
Dalam kaedah kami, equals()kami tidak melupakan semua cek yang kami bincangkan sebelum ini. Tetapi sekarang kita membandingkan setiap satu daripada tiga medan objek kita. Dalam program ini, kesaksamaan mestilah mutlak, dalam setiap bidang. Bagaimana pula hashCode?
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Medan modeldalam kelas kami adalah rentetan. Ini mudah: Stringkaedah hashCode()sudah diganti dalam kelas. Kami mengira kod cincang medan model, dan padanya kami menambah jumlah dua medan angka yang lain. Terdapat sedikit helah dalam Java yang digunakan untuk mengurangkan bilangan perlanggaran: apabila mengira kod cincang, darabkan hasil perantaraan dengan nombor perdana ganjil. Nombor yang paling biasa digunakan ialah 29 atau 31. Kami tidak akan pergi ke butiran matematik sekarang, tetapi untuk rujukan masa hadapan, ingat bahawa mendarab hasil perantaraan dengan nombor ganjil yang cukup besar membantu "menyebarkan" hasil cincangan berfungsi dan berakhir dengan lebih sedikit objek dengan kod cincang yang sama. Untuk kaedah kami hashCode()dalam LuxuryAuto ia akan kelihatan seperti ini:
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Anda boleh membaca lebih lanjut tentang semua selok-belok mekanisme ini dalam siaran ini di StackOverflow , serta dalam buku Joshua Bloch " Effective Java ". Akhir sekali, ada satu lagi perkara penting yang patut disebutkan. Setiap kali apabila mengatasi equals(), hashCode()kami memilih medan tertentu objek, yang diambil kira dalam kaedah ini. Tetapi bolehkah kita mengambil kira bidang yang berbeza dalam equals()dan hashCode()? Secara teknikal, kita boleh. Tetapi ini adalah idea yang tidak baik, dan inilah sebabnya:
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   return dollarPrice == that.dollarPrice;
}

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Berikut ialah kaedah kami equals()untuk hashCode()kelas LuxuryAuto. Kaedah hashCode()kekal tidak berubah dan equals()kami mengalih keluar medan daripada kaedah model. Kini model itu bukan ciri untuk membandingkan dua objek dengan equals(). Tetapi ia masih diambil kira semasa mengira kod cincang. Apa yang kita akan dapat hasilnya? Mari cipta dua kereta dan lihat!
public class Main {

   public static void main(String[] args) {

       LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
       LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);

       System.out.println("Are these two objects equal to each other?");
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println("What are their hash codes?");
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Эти два an object равны друг другу?
true
Какие у них хэш-codeы?
-1372326051
1668702472
Ralat! Dengan menggunakan medan yang berbeza untuk equals()dan hashCode()kami melanggar kontrak yang ditetapkan untuk mereka! Dua equals()objek yang sama mesti mempunyai kod cincang yang sama. Kami mendapat makna yang berbeza untuk mereka. Ralat sedemikian boleh membawa kepada akibat yang paling luar biasa, terutamanya apabila bekerja dengan koleksi yang menggunakan cincangan. Oleh itu, apabila mentakrifkan semula equals()dan hashCode()adalah betul untuk menggunakan medan yang sama. Kuliah itu ternyata agak panjang, tetapi hari ini anda belajar banyak perkara baru! :) Sudah tiba masanya untuk kembali menyelesaikan masalah!
Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION