CodeGym /Blog Java /rawak /equals dan kaedah hashCode: amalan terbaik
John Squirrels
Tahap
San Francisco

equals dan kaedah hashCode: amalan terbaik

Diterbitkan dalam kumpulan
Hai! Hari ini kita akan bercakap tentang dua kaedah penting dalam Java: equals()dan hashCode(). Ini bukan kali pertama kami bertemu dengan mereka: kursus CodeGym bermula dengan pelajaran ringkas tentang equals()— baca jika anda terlupa atau tidak melihatnya sebelum ini... kaedah sama dan kod cincang: amalan terbaik - 1Dalam pelajaran hari ini, kita akan bercakap tentang konsep-konsep ini secara terperinci. Dan percayalah, kita ada sesuatu untuk dibincangkan! Tetapi sebelum kita beralih kepada yang baharu, mari kita muat semula perkara yang telah kita bincangkan :) Seperti yang anda ingat, selalunya adalah idea yang tidak baik untuk membandingkan dua objek menggunakan operator, ==kerana ==membandingkan rujukan. Berikut ialah contoh kami dengan kereta dari pelajaran 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 Carobjek yang sama: nilai medan sepadan bagi dua objek kereta adalah sama, tetapi hasil perbandingan masih palsu. Kita sudah tahu sebabnya: rujukan car1dan car2merujuk kepada alamat memori yang berbeza, jadi ia tidak sama. Tetapi kami masih mahu membandingkan dua objek, bukan dua rujukan. Penyelesaian terbaik untuk membandingkan objek ialah equals()kaedah.

kaedah sama dengan().

Anda mungkin ingat bahawa kami tidak mencipta kaedah ini dari awal, sebaliknya kami mengatasinya: kaedah equals()ditakrifkan dalam Objectkelas. Yang berkata, dalam bentuk biasa, ia tidak berguna:

public boolean equals(Object obj) {
   return (this == obj);
}
Ini adalah bagaimana equals()kaedah ditakrifkan dalam Objectkelas. Ini adalah perbandingan rujukan sekali lagi. Kenapa mereka buat macam tu? Nah, bagaimanakah pencipta bahasa itu mengetahui objek dalam program anda yang dianggap sama dan yang mana tidak? :) Ini ialah titik utama kaedah equals()— pencipta kelas ialah orang yang menentukan ciri yang digunakan semasa menyemak kesamaan objek kelas. Kemudian anda mengatasi equals()kaedah dalam kelas anda. Jika anda tidak begitu memahami maksud "menentukan ciri-ciri yang mana", mari kita pertimbangkan satu contoh. Berikut ialah kelas ringkas yang mewakili seorang lelaki: 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 adalah kembar seiras atau hanya kelihatan seperti. Kami mempunyai lima ciri: saiz hidung, warna mata, gaya rambut, kehadiran parut, dan keputusan ujian DNA (untuk kesederhanaan, kami mewakili ini sebagai kod integer). Antara ciri ini, yang manakah anda fikir akan membolehkan program kami mengenal pasti kembar seiras? kaedah sama dan kod cincang: amalan terbaik - 2Sudah tentu, hanya ujian DNA boleh memberikan jaminan. Dua orang boleh mempunyai warna mata, potongan rambut, hidung, dan juga parut yang sama — terdapat ramai orang di dunia, dan adalah mustahil untuk menjamin bahawa tidak ada doppelgänger di luar sana. Tetapi kita memerlukan mekanisme yang boleh dipercayai: hanya hasil ujian DNA akan membolehkan kita membuat kesimpulan yang tepat. Apakah maksud ini untuk equals()kaedah kami? Kita perlu mengatasinya dalamMankelas, dengan mengambil kira keperluan program kami. Kaedah harus membandingkan int dnaCodemedan kedua-dua objek. 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 pandang sesuatu. Untuk objek kami, kami mengenal pasti hanya satu medan yang berkaitan dengan mewujudkan kesamaan objek: dnaCode. Sekarang bayangkan bahawa kita tidak mempunyai 1, tetapi 50 bidang yang berkaitan. Dan jika semua 50 medan dua objek adalah sama, maka objek adalah sama. Senario sedemikian juga mungkin. Masalah utama ialah mewujudkan kesaksamaan dengan membandingkan 50 bidang adalah proses yang memakan masa dan intensif sumber. Sekarang bayangkan bahawa sebagai tambahan kepada Mankelas kami, kami mempunyai Womankelas dengan medan yang sama yang wujud dalam Man. Jika pengaturcara lain menggunakan kelas kami, dia boleh menulis kod seperti ini dengan mudah:

public static void main(String[] args) {
  
   Man man = new Man(........); // A bunch of parameters in the constructor

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

   System.out.println(man.equals(woman));
}
Dalam kes ini, menyemak nilai medan adalah sia-sia: kita dapat melihat dengan mudah bahawa kita mempunyai objek dua kelas yang berbeza, jadi tidak ada cara ia boleh sama! Ini bermakna kita harus menambah semakan pada equals()kaedah, membandingkan kelas objek yang dibandingkan. Ada baiknya kita memikirkannya!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Tetapi mungkin kita telah melupakan sesuatu yang lain? Hmm... Sekurang-kurangnya, kita harus menyemak bahawa kita tidak membandingkan objek dengan dirinya sendiri! Jika rujukan A dan B menghala ke alamat memori yang sama, maka ia adalah objek yang sama, dan kita tidak perlu membuang masa dan 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;
}
Ia juga tidak salah untuk menambah semakan untuk null: tiada objek boleh sama dengan null. Jadi, jika parameter kaedah adalah batal, maka tiada gunanya pemeriksaan tambahan. Dengan semua ini dalam fikiran, maka equals()kaedah kami untuk Mankelas 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 melakukan semua pemeriksaan awal yang dinyatakan di atas. Pada penghujung hari, jika:
  • kita sedang membandingkan dua objek daripada kelas yang sama
  • dan objek yang dibandingkan bukanlah objek yang sama
  • dan objek yang dilalui bukannull
... kemudian kita meneruskan ke perbandingan ciri-ciri yang berkaitan. Bagi kami, ini bermakna dnaCodemedan dua objek. Apabila mengatasi equals()kaedah, pastikan anda mematuhi keperluan ini:
  1. Reflekstiviti.

    Apabila equals()kaedah digunakan untuk membandingkan mana-mana objek dengan dirinya sendiri, ia mesti kembali benar.
    Kami telah pun mematuhi keperluan ini. Kaedah kami termasuk:

    
    if (this == o) return true;
    

  2. simetri.

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

  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 b.equals(c)mesti kembali benar.

  4. Kegigihan.

    Keputusan equals()mesti berubah hanya apabila medan yang terlibat diubah. Jika data kedua-dua objek tidak berubah, maka hasil daripada equals()mesti sentiasa sama.

  5. Ketaksamaan dengan null.

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

kaedah hashCode().

Sekarang mari kita bercakap tentang hashCode()kaedah. Kenapa perlu? Untuk tujuan yang sama — untuk membandingkan objek. Tetapi kita sudah ada equals()! Kenapa kaedah lain? Jawapannya mudah: untuk meningkatkan prestasi. Fungsi cincang, yang diwakili dalam Java menggunakan hashCode()kaedah, mengembalikan nilai berangka panjang tetap untuk sebarang objek. Di Jawa, hashCode()kaedah mengembalikan nombor 32-bit ( int) untuk sebarang objek. Membanding dua nombor adalah lebih pantas daripada membandingkan dua objek menggunakan equals()kaedah, terutamanya jika kaedah itu mempertimbangkan banyak medan. Jika program kami membandingkan objek, ini lebih mudah dilakukan menggunakan kod cincang. Hanya jika objek adalah sama berdasarkan hashCode()kaedah, perbandingan diteruskan keequals()kaedah. Ngomong-ngomong, ini adalah cara struktur data berasaskan hash berfungsi, sebagai contoh, familiar HashMap! Kaedah hashCode(), seperti equals()kaedah, ditindih oleh pembangun. Dan sama seperti equals(), hashCode()kaedah tersebut mempunyai keperluan rasmi yang dinyatakan dalam dokumentasi Oracle:
  1. Jika dua objek adalah sama (iaitu equals()kaedah mengembalikan benar), maka ia mesti mempunyai kod cincang yang sama.

    Jika tidak, kaedah kami tidak akan bermakna. Seperti yang kami nyatakan di atas, hashCode()semakan perlu dilakukan terlebih dahulu untuk meningkatkan prestasi. Jika kod cincang adalah berbeza, maka semakan akan mengembalikan palsu, walaupun objek sebenarnya sama mengikut cara kami mentakrifkan equals()kaedah tersebut.

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

  3. Peraturan 1 tidak berfungsi dalam arah yang bertentangan. Dua objek berbeza boleh mempunyai kod cincang yang sama.

Peraturan ketiga agak mengelirukan. Bagaimana ini boleh terjadi? Penerangannya agak mudah. Kaedah hashCode()mengembalikan int. An intialah nombor 32-bit. Ia mempunyai julat nilai terhad: daripada -2,147,483,648 hingga +2,147,483,647. Dalam erti kata lain, terdapat hanya lebih 4 bilion nilai yang mungkin untuk int. Sekarang bayangkan anda sedang mencipta program untuk menyimpan data tentang semua orang yang tinggal di Bumi. Setiap orang akan sepadan dengan objeknya sendiri Person(serupa dengan Mankelas). Terdapat ~7.5 bilion orang yang tinggal di planet ini. Dalam erti kata lain, tidak kira betapa pintar algoritma yang kita tulis untuk menukarPersonobjek kepada int, kita hanya tidak mempunyai nombor yang mungkin mencukupi. Kami hanya mempunyai 4.5 bilion kemungkinan nilai int, tetapi terdapat lebih ramai orang daripada itu. Ini bermakna tidak kira betapa sukarnya kita mencuba, sesetengah orang yang berbeza akan mempunyai kod cincang yang sama. Apabila ini berlaku (kod cincang bertepatan untuk dua objek berbeza) kami memanggilnya perlanggaran. Apabila mengatasi hashCode()kaedah, salah satu objektif pengaturcara adalah untuk meminimumkan bilangan perlanggaran yang berpotensi. Mengambil kira semua peraturan ini, apakah hashCode()cara yang akan kelihatan seperti dalam Personkelas? seperti ini:

@Override
public int hashCode() {
   return dnaCode;
}
Terkejut? :) Jika anda melihat keperluan, anda akan melihat bahawa kami mematuhi semuanya. Objek yang equals()kaedah kami mengembalikan benar juga akan sama mengikut hashCode(). Jika dua Personobjek kami adalah sama dalam equals(iaitu, mereka mempunyai sama dnaCode), maka kaedah kami mengembalikan nombor yang sama. Mari kita pertimbangkan contoh yang lebih sukar. Katakan program kita harus memilih kereta mewah untuk pengumpul kereta. Mengumpul boleh menjadi hobi yang kompleks dengan banyak keanehan. Kereta 1963 tertentu boleh berharga 100 kali ganda lebih daripada kereta 1964. Kereta merah tahun 1970 boleh berharga 100 kali ganda lebih mahal daripada kereta biru jenama yang sama pada tahun yang sama. kaedah sama dan kod cincang: amalan terbaik - 4Dalam contoh kami sebelum ini, dengan Personkelas, kami membuang kebanyakan medan (iaitu ciri manusia) sebagai tidak penting dan hanya menggunakandnaCodebidang dalam perbandingan. Kami kini bekerja dalam alam yang sangat unik, di mana tiada butiran yang tidak penting! Inilah LuxuryAutokelas kami:

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.
}
Sekarang kita mesti mempertimbangkan semua bidang dalam perbandingan kita. Sebarang kesilapan boleh menyebabkan pelanggan mencecah ratusan ribu dolar, jadi adalah lebih baik untuk menjadi terlalu 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 equals()kaedah kami, kami tidak melupakan semua cek yang kami bincangkan sebelum ini. Tetapi sekarang kita membandingkan setiap satu daripada tiga medan objek kita. Untuk program ini, kita memerlukan kesaksamaan mutlak, iaitu kesamarataan 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 ialah String. Ini mudah, kerana Stringkelas sudah mengatasi hashCode()kaedah tersebut. Kami mengira modelkod cincang medan dan kemudian menambah jumlah dua medan berangka yang lain padanya. Pembangun Java mempunyai helah mudah yang mereka gunakan untuk mengurangkan bilangan perlanggaran: apabila mengira kod cincang, darabkan hasil perantaraan dengan perdana ganjil. Nombor yang paling biasa digunakan ialah 29 atau 31. Kami tidak akan menyelidiki kehalusan matematik sekarang, tetapi pada masa hadapan ingat bahawa mendarab hasil perantaraan dengan nombor ganjil yang cukup besar membantu "menyebarkan" hasil fungsi cincang dan, akibatnya, kurangkan bilangan 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 mengenai semua selok-belok mekanisme ini dalam siaran ini di StackOverflow , serta dalam buku Java Efektif oleh Joshua Bloch. Akhir sekali, satu lagi perkara penting yang patut disebutkan. Setiap kali kami mengatasi kaedah equals()dan hashCode(), kami memilih medan contoh tertentu yang diambil kira dalam kaedah ini. Kaedah ini mempertimbangkan bidang yang sama. Tetapi bolehkah kita mempertimbangkan 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;
}
Inilah kami equals()dan hashCode()kaedah untuk LuxuryAutokelas. Kaedah hashCode()kekal tidak berubah, tetapi kami mengalih keluar modelmedan daripada equals()kaedah. Model tidak lagi menjadi ciri yang digunakan apabila equals()kaedah membandingkan dua objek. Tetapi apabila mengira kod cincang, medan itu masih diambil kira. Apa yang kita dapat akibatnya? Mari cipta dua kereta dan ketahui!

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

Are these two objects equal to each other? 
true 
What are their hash codes? 
-1372326051 
1668702472
Ralat! Dengan menggunakan medan yang berbeza untuk equals()dan hashCode()kaedah, kami melanggar kontrak yang telah ditetapkan untuk mereka! Dua objek yang sama mengikut equals()kaedah mesti mempunyai kod cincang yang sama. Kami menerima nilai yang berbeza untuk mereka. Ralat sedemikian boleh membawa kepada akibat yang sangat sukar dipercayai, terutamanya apabila bekerja dengan koleksi yang menggunakan cincangan. Akibatnya, apabila anda mengatasi equals()dan hashCode(), anda harus mempertimbangkan medan yang sama. Pelajaran ini agak panjang, tetapi anda belajar banyak hari ini! :) Kini tiba masanya untuk kembali menyelesaikan tugasan!
Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION