CodeGym /Java Blog /Acak /metode equals dan hashCode: praktik terbaik
John Squirrels
Level 41
San Francisco

metode equals dan hashCode: praktik terbaik

Dipublikasikan di grup Acak
Hai! Hari ini kita akan berbicara tentang dua metode penting dalam Java: equals()dan hashCode(). Ini bukan pertama kalinya kita bertemu dengan mereka: kursus CodeGym dimulai dengan pelajaran singkat tentang equals()— bacalah jika Anda lupa atau belum pernah melihatnya sebelumnya... metode equals dan hashCode: praktik terbaik - 1Dalam pelajaran hari ini, kita akan membahas tentang konsep-konsep ini secara rinci. Dan percayalah, kita punya sesuatu untuk dibicarakan! Namun sebelum kita beralih ke yang baru, mari segarkan kembali apa yang telah kita bahas :) Seperti yang Anda ingat, biasanya merupakan ide yang buruk untuk membandingkan dua objek menggunakan operator ==, karena ==membandingkan referensi. Inilah contoh kami dengan mobil 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);
   }
}
Keluaran konsol:

false
Tampaknya kita telah membuat dua Carobjek yang identik: nilai bidang yang bersesuaian dari kedua objek mobil adalah sama, tetapi hasil perbandingannya masih salah. Kita sudah tahu alasannya: car1dan car2referensi menunjuk ke alamat memori yang berbeda, jadi keduanya tidak sama. Tapi kami tetap ingin membandingkan kedua objek tersebut, bukan dua referensi. Solusi terbaik untuk membandingkan objek adalah metodenya equals().

sama dengan() metode

Anda mungkin ingat bahwa kita tidak membuat metode ini dari awal, melainkan menimpanya: metode equals()didefinisikan di dalam Objectkelas. Yang mengatakan, dalam bentuknya yang biasa, itu tidak banyak berguna:

public boolean equals(Object obj) {
   return (this == obj);
}
Ini adalah bagaimana equals()metode didefinisikan dalam Objectkelas. Ini adalah perbandingan referensi sekali lagi. Mengapa mereka membuatnya seperti itu? Nah, bagaimana pembuat bahasa mengetahui objek mana dalam program Anda yang dianggap sama dan mana yang tidak? :) Ini adalah poin utama dari equals()metode ini — pembuat kelas adalah orang yang menentukan karakteristik mana yang digunakan saat memeriksa kesetaraan objek kelas. Kemudian Anda mengganti equals()metode di kelas Anda. Jika Anda tidak begitu mengerti arti dari "menentukan karakteristik yang mana", mari kita pertimbangkan sebuah contoh. Inilah kelas sederhana yang mewakili seorang pria: 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.
}
Misalkan kita sedang menulis sebuah program yang perlu menentukan apakah dua orang adalah kembar identik atau hanya mirip. Kami memiliki lima karakteristik: ukuran hidung, warna mata, gaya rambut, adanya bekas luka, dan hasil tes DNA (untuk kesederhanaan, kami menyatakan ini sebagai kode bilangan bulat). Manakah dari karakteristik berikut yang menurut Anda akan memungkinkan program kami untuk mengidentifikasi kembar identik? metode equals dan hashCode: praktik terbaik - 2Tentu saja, hanya tes DNA yang bisa memberikan jaminan. Dua orang dapat memiliki warna mata, potongan rambut, hidung, dan bahkan bekas luka yang sama — ada banyak orang di dunia ini, dan tidak mungkin untuk menjamin bahwa tidak ada doppelgänger di luar sana. Tetapi kami membutuhkan mekanisme yang andal: hanya hasil tes DNA yang memungkinkan kami membuat kesimpulan yang akurat. Apa artinya ini bagi equals()metode kita? Kita perlu menimpanya diMankelas, dengan mempertimbangkan persyaratan program kami. Metode harus membandingkan int dnaCodebidang dua objek. Jika mereka sama, maka objeknya sama.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Apakah itu benar-benar sederhana? Tidak terlalu. Kami mengabaikan sesuatu. Untuk objek kami, kami mengidentifikasi hanya satu bidang yang relevan untuk menetapkan persamaan objek: dnaCode. Sekarang bayangkan kita tidak memiliki 1, tetapi 50 bidang yang relevan. Dan jika semua 50 bidang dari dua objek sama, maka objek tersebut sama. Skenario seperti itu juga dimungkinkan. Masalah utamanya adalah membangun kesetaraan dengan membandingkan 50 bidang adalah proses yang memakan waktu dan sumber daya. Sekarang bayangkan bahwa selain Mankelas kita, kita memiliki Womankelas dengan bidang yang persis sama dengan yang ada di Man. Jika pemrogram lain menggunakan kelas kami, dia dapat dengan mudah menulis kode seperti ini:

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 hal ini, memeriksa nilai bidang tidak ada gunanya: kita dapat dengan mudah melihat bahwa kita memiliki objek dari dua kelas yang berbeda, jadi tidak mungkin keduanya sama! Ini berarti kita harus menambahkan centang pada equals()metode, membandingkan kelas dari objek yang dibandingkan. Ada baiknya kita memikirkan itu!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Tapi mungkin kita lupa sesuatu yang lain? Hmm... Paling tidak, kita harus memastikan bahwa kita tidak membandingkan suatu objek dengan objek itu sendiri! Jika referensi A dan B mengarah ke alamat memori yang sama, maka keduanya adalah objek yang sama, dan kita tidak perlu membuang waktu untuk membandingkan 50 bidang.

@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;
}
Juga tidak ada salahnya menambahkan tanda centang untuk null: tidak ada objek yang bisa sama dengan null. Jadi, jika parameter metode adalah nol, maka tidak ada gunanya pemeriksaan tambahan. Dengan mengingat semua ini, equals()metode kita untuk Mankelas akan terlihat 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 disebutkan di atas. Pada akhir hari, jika:
  • kita membandingkan dua objek dari kelas yang sama
  • dan objek yang dibandingkan bukanlah objek yang sama
  • dan objek yang diteruskan tidaknull
...kemudian kita lanjutkan ke perbandingan karakteristik yang relevan. Bagi kami, ini berarti dnaCodebidang dari dua objek. Saat mengganti equals()metode, pastikan untuk mematuhi persyaratan ini:
  1. Refleksivitas.

    Ketika equals()metode digunakan untuk membandingkan objek apapun dengan dirinya sendiri, itu harus mengembalikan nilai true.
    Kami telah memenuhi persyaratan ini. Metode kami meliputi:

    
    if (this == o) return true;
    

  2. Simetri.

    Jika a.equals(b) == true, maka b.equals(a)harus kembali true.
    Metode kami juga memenuhi persyaratan ini.

  3. Transitivitas.

    Jika dua objek sama dengan objek ketiga, maka keduanya harus sama satu sama lain.
    Jika a.equals(b) == truedan a.equals(c) == true, maka b.equals(c)juga harus mengembalikan true.

  4. Kegigihan.

    Hasil dari equals()harus berubah hanya ketika bidang yang terlibat diubah. Jika data kedua objek tidak berubah, maka hasil dari equals()harus selalu sama.

  5. Ketidaksetaraan dengan null.

    Untuk objek apa pun, a.equals(null)harus mengembalikan false
    Ini bukan hanya sekumpulan "rekomendasi berguna", melainkan kontrak ketat , yang ditetapkan dalam dokumentasi Oracle

kode hash() metode

Sekarang mari kita bicara tentang metodenya hashCode(). Mengapa itu perlu? Untuk tujuan yang persis sama — untuk membandingkan objek. Tapi kita sudah punya equals()! Mengapa metode lain? Jawabannya sederhana: untuk meningkatkan kinerja. Fungsi hash, yang direpresentasikan dalam Java menggunakan hashCode()metode ini, mengembalikan nilai numerik dengan panjang tetap untuk objek apa pun. Di Java, hashCode()metode mengembalikan angka 32-bit ( int) untuk objek apa pun. Membandingkan dua angka jauh lebih cepat daripada membandingkan dua objek menggunakan equals()metode, terutama jika metode tersebut mempertimbangkan banyak bidang. Jika program kita membandingkan objek, ini jauh lebih mudah dilakukan dengan menggunakan kode hash. Hanya jika objeknya sama berdasarkan metode, hashCode()perbandingan dilanjutkan keequals()metode. Omong-omong, beginilah cara kerja struktur data berbasis hash, misalnya, familiar HashMap! Metode hashCode(), seperti halnya equals()metode, ditimpa oleh pengembang. Dan seperti equals(), hashCode()metode ini memiliki persyaratan resmi yang dijabarkan dalam dokumentasi Oracle:
  1. Jika dua objek sama (yaitu equals()metode mengembalikan nilai true), maka keduanya harus memiliki kode hash yang sama.

    Kalau tidak, metode kami tidak akan ada artinya. Seperti yang kami sebutkan di atas, hashCode()pemeriksaan harus dilakukan terlebih dahulu untuk meningkatkan kinerja. Jika kode hash berbeda, maka pemeriksaan akan mengembalikan false, meskipun objek sebenarnya sama menurut cara kita mendefinisikan metode equals().

  2. Jika hashCode()metode dipanggil beberapa kali pada objek yang sama, metode tersebut harus mengembalikan nomor yang sama setiap kali.

  3. Aturan 1 tidak berlaku sebaliknya. Dua objek berbeda dapat memiliki kode hash yang sama.

Aturan ketiga agak membingungkan. Bagaimana ini bisa terjadi? Penjelasannya cukup sederhana. Metode hashCode()mengembalikan file int. An intadalah angka 32-bit. Ini memiliki rentang nilai yang terbatas: dari -2.147.483.648 hingga +2.147.483.647. Dengan kata lain, ada lebih dari 4 miliar nilai yang mungkin untuk sebuah int. Sekarang bayangkan Anda membuat program untuk menyimpan data tentang semua orang yang hidup di Bumi. Setiap orang akan sesuai dengan objeknya sendiri Person(mirip dengan Mankelas). Ada ~7,5 miliar orang yang hidup di planet ini. Dengan kata lain, tidak peduli seberapa pintar algoritma yang kita tulis untuk konversiPersonobjek ke int, kami tidak memiliki cukup angka yang mungkin. Kami hanya memiliki 4,5 miliar nilai int yang mungkin, tetapi ada lebih banyak orang dari itu. Ini berarti tidak peduli seberapa keras kita mencoba, beberapa orang yang berbeda akan memiliki kode hash yang sama. Ketika ini terjadi (kode hash bertepatan untuk dua objek berbeda) kami menyebutnya tabrakan. Saat mengganti hashCode()metode, salah satu tujuan pemrogram adalah meminimalkan potensi jumlah tabrakan. Dengan memperhitungkan semua aturan ini, seperti apa hashCode()tampilan metode di Personkelas? Seperti ini:

@Override
public int hashCode() {
   return dnaCode;
}
Terkejut? :) Jika Anda melihat persyaratannya, Anda akan melihat bahwa kami mematuhi semuanya. Objek yang equals()metode kami mengembalikan true juga akan sama menurut hashCode(). Jika kedua Personobjek kita sama equals(yaitu, keduanya memiliki dnaCode), maka metode kita mengembalikan angka yang sama. Mari kita pertimbangkan contoh yang lebih sulit. Misalkan program kita harus memilih mobil mewah untuk kolektor mobil. Mengoleksi bisa menjadi hobi yang kompleks dengan banyak kekhasan. Mobil tahun 1963 tertentu harganya 100 kali lebih mahal daripada mobil tahun 1964. Harga mobil merah tahun 1970 bisa 100 kali lebih mahal daripada mobil biru dengan merek yang sama di tahun yang sama. metode equals dan hashCode: praktik terbaik - 4Dalam contoh kami sebelumnya, dengan Personkelas, kami membuang sebagian besar bidang (yaitu karakteristik manusia) sebagai hal yang tidak penting dan hanya menggunakandnaCodelapangan dalam perbandingan. Kami sekarang bekerja di dunia yang sangat istimewa, di mana tidak ada detail 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 harus mempertimbangkan semua bidang dalam perbandingan kita. Kesalahan apa pun dapat merugikan klien ratusan ribu dolar, jadi akan lebih baik jika terlalu aman:

@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()metode kami, kami tidak melupakan semua pemeriksaan yang kami bicarakan sebelumnya. Tapi sekarang kita membandingkan masing-masing dari tiga bidang objek kita. Untuk program ini, diperlukan persamaan mutlak, yaitu persamaan setiap bidang. Bagaimana hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Bidang modeldi kelas kami adalah sebuah String. Ini nyaman, karena Stringkelas sudah mengganti hashCode()metode. Kami menghitung modelkode hash bidang dan kemudian menambahkan jumlah dari dua bidang numerik lainnya ke dalamnya. Pengembang Java memiliki trik sederhana yang mereka gunakan untuk mengurangi jumlah tabrakan: saat menghitung kode hash, gandakan hasil antara dengan bilangan prima ganjil. Angka yang paling umum digunakan adalah 29 atau 31. Kami tidak akan mempelajari seluk-beluk matematika sekarang, tetapi di masa mendatang ingatlah bahwa mengalikan hasil antara dengan angka ganjil yang cukup besar membantu "menyebarkan" hasil fungsi hash dan, akibatnya, kurangi jumlah objek dengan kode hash yang sama. Untuk metode kami hashCode()di LuxuryAuto, akan terlihat seperti ini:

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Anda dapat membaca lebih lanjut tentang semua seluk-beluk mekanisme ini di postingan StackOverflow ini , serta di buku Effective Java oleh Joshua Bloch. Terakhir, satu hal penting lagi yang perlu disebutkan. Setiap kali kami mengganti metode equals()and hashCode(), kami memilih bidang contoh tertentu yang diperhitungkan dalam metode ini. Metode ini mempertimbangkan bidang yang sama. Tapi bisakah kita mempertimbangkan bidang yang berbeda di equals()dan hashCode()? Secara teknis, kita bisa. Tapi ini ide yang buruk, dan inilah alasannya:

@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()metode untuk LuxuryAutokelas. Metode hashCode()tetap tidak berubah, tetapi kami menghapus modelbidang dari equals()metode. Model tidak lagi menjadi karakteristik yang digunakan ketika equals()metode membandingkan dua objek. Namun saat menghitung kode hash, bidang tersebut tetap diperhitungkan. Apa yang kita dapatkan sebagai hasilnya? Ayo buat dua mobil dan cari tahu!

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
Kesalahan! Dengan menggunakan bidang yang berbeda untuk metode equals()dan hashCode(), kami melanggar kontrak yang telah dibuat untuk mereka! Dua objek yang sama menurut equals()metode harus memiliki kode hash yang sama. Kami menerima nilai yang berbeda untuk mereka. Kesalahan seperti itu dapat menyebabkan konsekuensi yang benar-benar tidak dapat dipercaya, terutama saat bekerja dengan koleksi yang menggunakan hash. Akibatnya, saat Anda mengganti equals()dan hashCode(), Anda harus mempertimbangkan bidang yang sama. Pelajaran ini agak panjang, tetapi Anda belajar banyak hari ini! :) Sekarang saatnya untuk kembali menyelesaikan tugas!
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION