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... 
==
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 Car
objek yang sama: nilai medan sepadan bagi dua objek kereta adalah sama, tetapi hasil perbandingan masih palsu. Kita sudah tahu sebabnya: rujukan car1
dan car2
merujuk 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: kaedahequals()
ditakrifkan dalam Object
kelas. Yang berkata, dalam bentuk biasa, ia tidak berguna:
public boolean equals(Object obj) {
return (this == obj);
}
Ini adalah bagaimana equals()
kaedah ditakrifkan dalam Object
kelas. 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? 
equals()
kaedah kami? Kita perlu mengatasinya dalamMan
kelas, dengan mengambil kira keperluan program kami. Kaedah harus membandingkan int dnaCode
medan 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 Man
kelas kami, kami mempunyai Woman
kelas 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 Man
kelas 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 bukan
null
dnaCode
medan dua objek. Apabila mengatasi equals()
kaedah, pastikan anda mematuhi keperluan ini:
-
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;
-
simetri.
Jika
a.equals(b) == true
, makab.equals(a)
mesti kembalitrue
.
Kaedah kami memenuhi keperluan ini juga. -
Transitiviti.
Jika dua objek adalah sama dengan beberapa objek ketiga, maka mereka mesti sama antara satu sama lain.
Jikaa.equals(b) == true
dana.equals(c) == true
, makab.equals(c)
mesti kembali benar. -
Kegigihan.
Keputusan
equals()
mesti berubah hanya apabila medan yang terlibat diubah. Jika data kedua-dua objek tidak berubah, maka hasil daripadaequals()
mesti sentiasa sama. -
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 tentanghashCode()
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:
-
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 mentakrifkanequals()
kaedah tersebut. -
Jika
hashCode()
kaedah dipanggil beberapa kali pada objek yang sama, ia mesti mengembalikan nombor yang sama setiap kali. -
Peraturan 1 tidak berfungsi dalam arah yang bertentangan. Dua objek berbeza boleh mempunyai kod cincang yang sama.
hashCode()
mengembalikan int
. An int
ialah 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 Man
kelas). Terdapat ~7.5 bilion orang yang tinggal di planet ini. Dalam erti kata lain, tidak kira betapa pintar algoritma yang kita tulis untuk menukarPerson
objek 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 Person
kelas? 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 Person
objek 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. 
Person
kelas, kami membuang kebanyakan medan (iaitu ciri manusia) sebagai tidak penting dan hanya menggunakandnaCode
bidang dalam perbandingan. Kami kini bekerja dalam alam yang sangat unik, di mana tiada butiran yang tidak penting! Inilah LuxuryAuto
kelas 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 model
dalam kelas kami ialah String. Ini mudah, kerana String
kelas sudah mengatasi hashCode()
kaedah tersebut. Kami mengira model
kod 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 LuxuryAuto
kelas. Kaedah hashCode()
kekal tidak berubah, tetapi kami mengalih keluar model
medan 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!
GO TO FULL VERSION