JavaRush /Java Blog /Random-ID /metode sama dengan & kode hash: praktik penggunaan

metode sama dengan & kode hash: praktik penggunaan

Dipublikasikan di grup Random-ID
Halo! Hari ini kita akan berbicara tentang dua metode penting di Java - equals()dan hashCode(). Ini bukan pertama kalinya kami bertemu mereka: di awal kursus JavaRush ada ceramah singkat tentang - equals()bacalah jika Anda lupa atau belum pernah melihatnya. Metode sama dengan &  kode hash: praktik penggunaan - 1Dalam pelajaran hari ini kita akan membicarakan konsep-konsep ini secara mendetail - percayalah, ada banyak hal yang perlu dibicarakan! Dan sebelum kita beralih ke sesuatu yang baru, mari segarkan ingatan kita tentang apa yang telah kita bahas :) Seperti yang Anda ingat, perbandingan dua objek yang biasa menggunakan operator “ ==” adalah ide yang buruk, karena “ ==” membandingkan referensi. Berikut adalah contoh mobil 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);
   }
}
Keluaran konsol:

false
Tampaknya kita telah membuat dua objek kelas yang identik Car: semua bidang pada kedua mesin tersebut sama, tetapi hasil perbandingannya masih salah. Kita sudah tahu alasannya: tautan car1dan car2titik ke alamat berbeda di memori, jadi tidak sama. Kami masih ingin membandingkan dua objek, bukan dua referensi. Solusi terbaik untuk membandingkan objek adalah equals().

sama dengan() metode

Anda mungkin ingat bahwa kami tidak membuat metode ini dari awal, tetapi menimpanya - lagipula, metode ini equals()ditentukan di kelas Object. Namun, dalam bentuknya yang biasa, ini tidak banyak berguna:
public boolean equals(Object obj) {
   return (this == obj);
}
Beginilah cara metode equals()didefinisikan di kelas Object. Perbandingan tautan yang sama. Kenapa dia dibuat seperti ini? Nah, bagaimana pencipta bahasa tersebut mengetahui objek mana dalam program Anda yang dianggap setara dan mana yang tidak? :) Ini adalah ide utama dari metode ini equals()- pembuat kelas sendiri yang menentukan karakteristik yang digunakan untuk memeriksa kesetaraan objek kelas ini. Dengan melakukan ini, Anda mengganti metode equals()di kelas Anda. Jika Anda belum begitu memahami arti dari “Anda sendiri yang menentukan ciri-cirinya”, mari kita lihat contohnya. Ini adalah kelas orang yang sederhana - 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.
}
Katakanlah kita sedang menulis sebuah program yang perlu menentukan apakah dua orang mempunyai hubungan saudara kembar, atau hanya doppelgänger. Kami memiliki lima ciri: ukuran hidung, warna mata, gaya rambut, adanya bekas luka dan hasil tes biologis DNA (untuk mempermudah - dalam bentuk nomor kode). Menurut Anda, manakah dari karakteristik berikut yang memungkinkan program kami mengidentifikasi saudara kembar? Metode sama dengan &  kode hash: praktik penggunaan - 2Tentu saja, hanya uji biologis yang dapat memberikan jaminan. Dua orang dapat memiliki warna mata, gaya rambut, hidung, dan bahkan bekas luka yang sama - ada banyak orang di dunia ini, dan tidak mungkin menghindari kebetulan. Kita membutuhkan mekanisme yang andal: hanya hasil tes DNA yang memungkinkan kita menarik kesimpulan yang akurat. Apa artinya ini bagi metode kami equals()? Kita perlu mendefinisikannya kembali di kelas Mandengan mempertimbangkan persyaratan program kita. Metode tersebut harus membandingkan bidang int dnaCodedua benda, dan jika keduanya sama, maka kedua benda tersebut sama.
@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Apakah sesederhana itu? Tidak terlalu. Kami melewatkan sesuatu. Dalam hal ini, untuk objek kita, kita hanya mendefinisikan satu bidang "penting" yang dengannya persamaannya ditetapkan - dnaCode. Sekarang bayangkan kita tidak mempunyai 1, tapi 50 bidang “penting” tersebut. Dan jika ke-50 bidang dari dua benda sama, maka kedua benda tersebut juga sama. Hal ini juga bisa terjadi. Masalah utamanya adalah menghitung kesetaraan 50 bidang adalah proses yang memakan waktu dan sumber daya. Sekarang bayangkan selain kelas tersebut, Mankita memiliki kelas Womandengan bidang yang sama persis seperti di Man. Dan jika programmer lain menggunakan kelas Anda, dia dapat dengan mudah menulis sesuatu seperti ini ke dalam programnya:
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 hal ini, tidak ada gunanya memeriksa nilai bidang: kita melihat bahwa kita sedang melihat objek dari dua kelas yang berbeda, dan pada prinsipnya keduanya tidak bisa setara! Ini berarti kita perlu memberi tanda centang pada metode equals()—perbandingan objek dari dua kelas yang identik. Ada baiknya kita memikirkan hal ini!
@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Tapi mungkin kita melupakan hal lain? Hmm... Minimal, kita harus memastikan bahwa kita tidak membandingkan objek dengan objek itu sendiri! Jika referensi A dan B menunjuk ke alamat yang sama di memori, maka keduanya adalah objek yang sama, dan kita juga 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;
}
Selain itu, tidak ada salahnya menambahkan tanda centang untuk null: tidak ada objek yang bisa sama dengan null, dalam hal ini tidak ada gunanya melakukan pemeriksaan tambahan. Dengan mempertimbangkan semua ini, metode equals()kelas kita Manakan 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. Jika ternyata:
  • kita membandingkan dua objek dari kelas yang sama
  • ini bukan objek yang sama
  • kita tidak membandingkan objek kita dengannull
...kemudian kita melanjutkan dengan membandingkan karakteristik yang signifikan. Dalam kasus kami, bidang dnaCodedua objek. Saat mengganti suatu metode equals(), pastikan untuk mematuhi persyaratan berikut:
  1. Refleksivitas.

    Objek apa pun harus menjadi equals()miliknya sendiri.
    Kami telah mempertimbangkan persyaratan ini. Metode kami menyatakan:

    if (this == o) return true;

  2. Simetri.

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

  3. Transitivitas.

    Jika dua benda sama dengan benda ketiga, maka keduanya harus sama satu sama lain.
    Jika a.equals(b) == truedan a.equals(c) == true, maka pemeriksaan b.equals(c)juga akan menghasilkan nilai true.

  4. Keabadian.

    Hasil pekerjaan equals()seharusnya berubah hanya jika bidang yang termasuk di dalamnya berubah. Jika data kedua objek tidak berubah, maka hasil pengecekan equals()harus selalu sama.

  5. Ketimpangan dengan null.

    Untuk objek apa pun, pemeriksaan a.equals(null)harus menghasilkan false.
    Ini bukan hanya serangkaian "rekomendasi berguna", tetapi kontrak metode yang ketat , yang ditentukan dalam dokumentasi Oracle

metode kode hash()

Sekarang mari kita bicara tentang metodenya hashCode(). Mengapa itu diperlukan? Tepatnya untuk tujuan yang sama - membandingkan objek. Tapi kami sudah memilikinya equals()! Mengapa metode lain? Jawabannya sederhana: meningkatkan produktivitas. Fungsi hash, yang diwakili oleh metode , di Java hashCode(), mengembalikan nilai numerik dengan panjang tetap untuk objek apa pun. Dalam kasus Java, metode ini hashCode()mengembalikan nomor tipe 32-bit int. Membandingkan dua angka satu sama lain jauh lebih cepat dibandingkan membandingkan dua objek menggunakan metode ini equals(), apalagi jika menggunakan banyak kolom. Jika program kita akan membandingkan objek, akan lebih mudah melakukannya dengan kode hash, dan hanya jika objek tersebut sama hashCode()- lanjutkan ke perbandingan dengan equals(). Omong-omong, beginilah cara kerja struktur data berbasis hash—misalnya, yang Anda tahu HashMap! Metodenya hashCode(), seperti halnya equals(), ditimpa oleh pengembangnya sendiri. Dan sama seperti for equals(), metode ini hashCode()memiliki persyaratan resmi yang ditentukan dalam dokumentasi Oracle:
  1. Jika dua objek sama (yaitu, metode equals()mengembalikan nilai true), keduanya harus memiliki kode hash yang sama.

    Kalau tidak, metode kami tidak akan ada artinya. Pengecekan oleh hashCode(), seperti yang kami katakan, harus dilakukan terlebih dahulu untuk meningkatkan kinerja. Jika kode hashnya berbeda, pemeriksaan akan menghasilkan false, meskipun objeknya sebenarnya sama (seperti yang kita definisikan dalam metode equals()).

  2. Jika suatu metode hashCode()dipanggil beberapa kali pada objek yang sama, maka 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 ini hashCode()kembali int. intadalah angka 32-bit. Ini memiliki jumlah nilai yang terbatas - dari -2.147.483.648 hingga +2.147.483.647. Dengan kata lain, terdapat lebih dari 4 miliar variasi angka int. Sekarang bayangkan Anda membuat program untuk menyimpan data tentang semua makhluk hidup di Bumi. Setiap orang akan memiliki objek kelasnya sendiri Man. ~7,5 miliar orang hidup di bumi. Dengan kata lain, tidak peduli seberapa bagus algoritma Manyang kita tulis untuk mengubah objek menjadi angka, kita tidak akan memiliki cukup angka. Kita hanya mempunyai 4,5 miliar pilihan, dan masih banyak lagi orang. Artinya, sekeras apa pun kita mencoba, kode hash akan tetap sama untuk beberapa orang yang berbeda. Situasi ini (kode hash dari dua objek berbeda yang cocok) disebut tabrakan. Salah satu tujuan programmer ketika mengganti suatu metode hashCode()adalah untuk mengurangi potensi jumlah tabrakan sebanyak mungkin. hashCode()Seperti apa metode kelas kita Man, dengan mempertimbangkan semua aturan ini? Seperti ini:
@Override
public int hashCode() {
   return dnaCode;
}
Terkejut? :) Tanpa diduga, tetapi jika Anda melihat persyaratannya, Anda akan melihat bahwa kami mematuhi semuanya. Objek yang nilai pengembaliannya equals()true akan sama dengan hashCode(). Jika kedua objek kita Manmemiliki nilai yang sama equals(yaitu keduanya memiliki nilai yang sama dnaCode), metode kita akan mengembalikan angka yang sama. Mari kita lihat contoh yang lebih rumit. Katakanlah program kita harus memilih mobil mewah untuk klien kolektor. Mengumpulkan adalah hal yang kompleks, dan ada banyak fitur di dalamnya. Sebuah mobil dari tahun 1963 dapat berharga 100 kali lipat dibandingkan mobil yang sama dari tahun 1964. Harga mobil merah dari tahun 1970 bisa 100 kali lebih mahal daripada mobil biru dengan merek yang sama dari tahun yang sama. Metode sama dengan &  kode hash: praktik penggunaan - 4Dalam kasus pertama, dengan kelas Man, kami membuang sebagian besar bidang (yaitu, karakteristik orang) sebagai tidak penting dan hanya menggunakan bidang tersebut untuk perbandingan dnaCode. Di sini kami bekerja dengan area yang sangat unik, dan tidak boleh ada detail 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, ketika membandingkan, kita harus memperhitungkan semua bidang. Kesalahan apa pun dapat merugikan klien hingga ratusan ribu dolar, jadi lebih baik 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 metode kami, equals()kami tidak melupakan semua pemeriksaan yang kami bicarakan sebelumnya. Namun sekarang kita membandingkan masing-masing dari ketiga bidang objek kita. Dalam program ini, kesetaraan harus menjadi hal yang mutlak, dalam segala bidang. Bagaimana dengan hashCode?
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Bidang modeldi kelas kita adalah string. Ini nyaman: Stringmetode ini hashCode()sudah ditimpa di kelas. Kami menghitung kode hash bidang tersebut model, dan ke dalamnya kami menambahkan jumlah dari dua bidang numerik lainnya. Ada sedikit trik di Java yang digunakan untuk mengurangi jumlah tabrakan: saat menghitung kode hash, kalikan hasil antara dengan bilangan prima ganjil. Angka yang paling umum digunakan adalah 29 atau 31. Kita tidak akan membahas detail matematikanya sekarang, namun untuk referensi di masa mendatang, ingatlah bahwa mengalikan hasil antara dengan angka ganjil yang cukup besar membantu “menyebarkan” hasil hash berfungsi dan berakhir dengan lebih sedikit 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 di StackOverflow ini , serta di buku Joshua Bloch “ Java Efektif ”. Terakhir, ada satu hal penting lagi yang patut disebutkan. Setiap kali mengganti equals(), hashCode()kami memilih bidang objek tertentu, yang diperhitungkan dalam metode ini. Namun bisakah kita memperhitungkan berbagai bidang di equals()dan hashCode()? Secara teknis, kami 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;
}
Berikut adalah metode kami equals()untuk hashCode()kelas LuxuryAuto. Metodenya hashCode()tetap tidak berubah, dan equals()kami menghapus bidang tersebut dari metode model. Sekarang model bukan merupakan karakteristik untuk membandingkan dua objek dengan equals(). Namun tetap diperhitungkan saat menghitung kode hash. Apa yang akan kita dapatkan sebagai hasilnya? Ayo buat dua mobil dan periksa!
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
Kesalahan! Dengan menggunakan bidang yang berbeda equals()dan hashCode()kami melanggar kontrak yang dibuat untuk mereka! Dua equals()objek yang sama harus memiliki kode hash yang sama. Kami mempunyai arti yang berbeda bagi mereka. Kesalahan seperti itu dapat menimbulkan konsekuensi yang paling luar biasa, terutama ketika bekerja dengan koleksi yang menggunakan hash. Oleh karena itu, ketika mendefinisikan ulang equals()dan hashCode()akan lebih tepat jika menggunakan bidang yang sama. Kuliahnya ternyata lumayan panjang, tapi hari ini kamu belajar banyak hal baru! :) Saatnya kembali memecahkan masalah!
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION