JavaRush /Java Blog /Random-ID /Kontrak yang setara dan kode hash atau apa pun itu
Aleksandr Zimin
Level 1
Санкт-Петербург

Kontrak yang setara dan kode hash atau apa pun itu

Dipublikasikan di grup Random-ID
Tentu saja, sebagian besar pemrogram Java mengetahui bahwa metode equalsberkaitan hashCodeerat satu sama lain, dan disarankan untuk mengganti kedua metode ini di kelas mereka secara konsisten. Hanya sedikit orang yang mengetahui mengapa hal ini terjadi dan dampak buruk apa yang dapat terjadi jika aturan ini dilanggar. Saya mengusulkan untuk mempertimbangkan konsep metode-metode ini, mengulangi tujuannya dan memahami mengapa metode-metode tersebut begitu terhubung. Saya menulis artikel ini, seperti artikel sebelumnya tentang memuat kelas, untuk diri saya sendiri agar akhirnya mengungkapkan semua detail masalahnya dan tidak lagi kembali ke sumber pihak ketiga. Oleh karena itu, saya dengan senang hati menerima kritik yang membangun, karena jika ada kesenjangan, sebaiknya dihilangkan. Sayangnya, artikelnya ternyata cukup panjang.

sama dengan mengesampingkan aturan

Sebuah metode equals()diperlukan di Java untuk mengkonfirmasi atau menyangkal fakta bahwa dua objek dari asal yang sama secara logis sama . Artinya, ketika membandingkan dua objek, pemrogram perlu memahami apakah bidang signifikannya setara . Tidak semua bidang harus identik, karena metode ini equals()menyiratkan persamaan logis . Namun terkadang tidak ada kebutuhan khusus untuk menggunakan metode ini. Seperti yang mereka katakan, cara termudah untuk menghindari masalah dengan menggunakan mekanisme tertentu adalah dengan tidak menggunakannya. Perlu juga dicatat bahwa setelah Anda melanggar kontrak, equalsAnda kehilangan kendali atas pemahaman bagaimana objek dan struktur lain akan berinteraksi dengan objek Anda. Dan selanjutnya akan sangat sulit untuk menemukan penyebab kesalahan tersebut.

Kapan tidak mengesampingkan metode ini

  • Ketika setiap instance kelas adalah unik.
  • Pada tingkat yang lebih luas, hal ini berlaku untuk kelas-kelas yang menyediakan perilaku spesifik, dan bukan dirancang untuk bekerja dengan data. Seperti misalnya kelas Thread. Bagi mereka equals, penerapan metode yang disediakan kelas Objectsudah lebih dari cukup. Contoh lainnya adalah kelas enum ( Enum).
  • Padahal sebenarnya kelas tersebut tidak diharuskan untuk menentukan kesetaraan instance-nya.
  • Misalnya, untuk suatu kelas, java.util.Randomtidak perlu sama sekali membandingkan instance kelas satu sama lain, menentukan apakah instance tersebut dapat mengembalikan urutan angka acak yang sama. Hanya karena sifat kelas ini tidak menyiratkan perilaku seperti itu.
  • Ketika kelas yang Anda perluas sudah memiliki implementasi metodenya sendiri equalsdan perilaku implementasi ini cocok untuk Anda.
  • Misalnya untuk kelas Set, List, Mapimplementasinya equalsada di AbstractSet, AbstractListdan AbstractMapmasing-masing.
  • Dan terakhir, tidak perlu melakukan override equalsketika cakupan kelas Anda berada privateatau package-privatedan Anda yakin metode ini tidak akan pernah dipanggil.

sama dengan kontrak

Saat mengganti suatu metode, equalspengembang harus mematuhi aturan dasar yang ditentukan dalam spesifikasi bahasa Java.
  • Refleksivitas
  • untuk nilai tertentu x, ekspresi x.equals(x)harus kembali true.
    Diberikan - artinya seperti itux != null
  • Simetri
  • untuk nilai apa pun xdan y, x.equals(y)harus dikembalikan truehanya jika ia y.equals(x)kembali true.
  • Transitivitas
  • untuk setiap nilai tertentu x, ydan z, jika x.equals(y)kembali truedan y.equals(z)kembali true, x.equals(z)harus mengembalikan nilai tersebut true.
  • Konsistensi
  • untuk nilai apa pun, xdan ypanggilan berulang x.equals(y)akan mengembalikan nilai panggilan sebelumnya ke metode ini, asalkan bidang yang digunakan untuk membandingkan dua objek tidak berubah di antara panggilan.
  • Perbandingan nol
  • untuk nilai tertentu, xpanggilan x.equals(null)harus kembali false.

sama dengan pelanggaran kontrak

Banyak kelas, misalnya dari Java Collections Framework, bergantung pada implementasi metode ini equals(), jadi Anda tidak boleh mengabaikannya, karena Pelanggaran terhadap kontrak metode ini dapat menyebabkan pengoperasian aplikasi yang tidak rasional, dan dalam hal ini akan cukup sulit untuk menemukan alasannya. Menurut prinsip refleksivitas , setiap benda harus setara dengan dirinya sendiri. Jika prinsip ini dilanggar, maka ketika kita menambahkan suatu objek ke dalam koleksi dan kemudian mencarinya menggunakan metode tersebut, contains()kita tidak akan dapat menemukan objek yang baru saja kita tambahkan ke dalam koleksi tersebut. Syarat simetri menyatakan bahwa dua benda harus sama, tidak peduli urutan perbandingannya. Misalnya, jika Anda memiliki kelas yang hanya berisi satu bidang bertipe string, maka salah jika membandingkan equalsbidang ini dengan string dalam suatu metode. Karena dalam kasus perbandingan terbalik, metode akan selalu mengembalikan nilai false.
// Нарушение симметричности
public class SomeStringify {
    private String s;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof SomeStringify) {
            return s.equals(((SomeStringify) o).s);
        }
        // нарушение симметричности, классы разного происхождения
        if (o instanceof String) {
            return s.equals(o);
        }
        return false;
    }
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    return o instanceof SomeStringify &&
            ((SomeStringify) o).s.equals(s);
}
Dari syarat transitivitas dapat disimpulkan bahwa jika ada dua dari tiga benda yang sama, maka dalam hal ini ketiga benda tersebut harus sama. Prinsip ini dapat dengan mudah dilanggar ketika diperlukan untuk memperluas kelas dasar tertentu dengan menambahkan komponen yang berarti ke dalamnya . Misalnya, ke kelas Pointdengan koordinat xdan yAnda perlu menambahkan warna titik dengan memperluasnya. Untuk melakukan ini, Anda perlu mendeklarasikan kelas ColorPointdengan bidang yang sesuai color. Jadi, jika di kelas yang diperluas kita memanggil equalsmetode induk, dan di induk kita berasumsi bahwa hanya koordinat xdan yang dibandingkan y, maka dua titik dengan warna berbeda tetapi dengan koordinat yang sama akan dianggap sama, dan ini tidak benar. Dalam hal ini perlu diajarkan kelas turunan untuk membedakan warna. Untuk melakukan ini, Anda dapat menggunakan dua metode. Tapi yang satu akan melanggar aturan simetri , dan yang kedua - transitivitas .
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) return false;
    return super.equals(o) && ((ColorPoint) o).color == color;
}
Dalam hal ini, panggilan point.equals(colorPoint)akan mengembalikan nilai true, dan perbandingan colorPoint.equals(point)akan mengembalikan false, karena mengharapkan objek dari kelas "nya". Dengan demikian, aturan simetri dilanggar. Metode kedua melibatkan melakukan pemeriksaan “buta” jika tidak ada data tentang warna titik, yaitu kita memiliki kelas Point. Atau periksa warnanya jika informasi tentangnya tersedia, yaitu membandingkan objek kelas ColorPoint.
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) return false;

    // Слепая проверка
    if (!(o instanceof ColorPoint))
        return super.equals(o);

    // Полная проверка, включая цвет точки
    return super.equals(o) && ((ColorPoint) o).color == color;
}
Prinsip transitivitas dilanggar di sini sebagai berikut. Katakanlah ada definisi dari objek berikut:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
Jadi, meskipun persamaan p1.equals(p2)dan bernilai true p2.equals(p3), p1.equals(p3)namun akan mengembalikan nilai false. Sedangkan cara kedua menurut saya kurang menarik karena Dalam beberapa kasus, algoritme mungkin buta dan tidak melakukan perbandingan sepenuhnya, dan Anda mungkin tidak mengetahuinya. Sedikit puisi Secara umum, sepengetahuan saya, tidak ada solusi konkrit untuk masalah ini. Ada pendapat dari salah satu penulis resmi bernama Kay Horstmann bahwa Anda dapat mengganti penggunaan operator instanceofdengan pemanggilan metode getClass()yang mengembalikan kelas objek dan, sebelum Anda mulai membandingkan objek itu sendiri, pastikan tipenya sama. , dan tidak memperhatikan fakta asal usulnya yang sama. Dengan demikian, aturan simetri dan transitivitas akan terpenuhi. Namun pada saat yang sama, di sisi lain barikade berdiri penulis lain, yang tidak kalah dihormati di kalangan luas, Joshua Bloch, yang percaya bahwa pendekatan ini melanggar prinsip substitusi Barbara Liskov. Prinsip ini menyatakan bahwa “kode pemanggil harus memperlakukan kelas dasar dengan cara yang sama seperti subkelasnya tanpa menyadarinya . ” Dan dalam solusi yang diajukan Horstmann, prinsip ini jelas dilanggar, karena bergantung pada implementasinya. Singkatnya, jelas bahwa masalahnya gelap. Perlu juga dicatat bahwa Horstmann mengklarifikasi aturan untuk menerapkan pendekatannya dan menulis dalam bahasa Inggris sederhana bahwa Anda perlu memutuskan strategi saat merancang kelas, dan jika pengujian kesetaraan hanya akan dilakukan oleh superclass, Anda dapat melakukannya dengan melakukan operasi instanceof. Jika tidak, ketika semantik pemeriksaan berubah tergantung pada kelas turunan dan penerapan metode perlu dipindahkan ke bawah hierarki, Anda harus menggunakan metode tersebut getClass(). Joshua Bloch, pada gilirannya, mengusulkan untuk meninggalkan warisan dan menggunakan komposisi objek dengan memasukkan ColorPointkelas ke dalam kelas Pointdan menyediakan metode akses asPoint()untuk memperoleh informasi khusus tentang titik tersebut. Ini akan menghindari pelanggaran semua aturan, tetapi menurut saya, ini akan membuat kode lebih sulit untuk dipahami. Opsi ketiga adalah menggunakan metode pembuatan persamaan secara otomatis menggunakan IDE. Idenya, omong-omong, mereproduksi generasi Horstmann, memungkinkan Anda memilih strategi untuk mengimplementasikan metode di superclass atau di turunannya. Terakhir, aturan konsistensi berikutnya menyatakan bahwa meskipun objek xtidak yberubah, pemanggilan objek lagi x.equals(y)harus mengembalikan nilai yang sama seperti sebelumnya. Aturan terakhirnya adalah tidak ada objek yang sama dengan null. Semuanya jelas di sini null- ini ketidakpastian, apakah objeknya sama dengan ketidakpastian? Tidak jelas, yaitu false.

Algoritma umum untuk menentukan persamaan

  1. Periksa kesetaraan referensi objek thisdan parameter metode o.
    if (this == o) return true;
  2. Periksa apakah tautan tersebut ditentukan o, yaitu apakah sudah ditentukan null.
    Jika di masa depan, saat membandingkan tipe objek, operator akan digunakan instanceof, item ini dapat dilewati, karena parameter ini kembali falsedalam kasus ini null instanceof Object.
  3. Bandingkan jenis objek thismenggunakan ooperator instanceofatau metode getClass(), dipandu oleh uraian di atas dan intuisi Anda sendiri.
  4. Jika suatu metode equalsditimpa dalam subkelas, pastikan untuk melakukan panggilansuper.equals(o)
  5. Ubah tipe parameter omenjadi kelas yang diperlukan.
  6. Lakukan perbandingan semua bidang objek penting:
    • untuk tipe primitif (kecuali floatdan double), menggunakan operator==
    • untuk bidang referensi Anda perlu memanggil metodenyaequals
    • untuk array, Anda dapat menggunakan iterasi siklik atau metodenyaArrays.equals()
    • untuk tipe floatdan doubleperlu menggunakan metode perbandingan dari kelas pembungkus yang sesuai Float.compare()danDouble.compare()
  7. Dan terakhir, jawab tiga pertanyaan: apakah metode yang diterapkan simetris ? Transitif ? Sepakat ? Dua prinsip lainnya ( refleksivitas dan kepastian ) biasanya dijalankan secara otomatis.

Aturan penggantian HashCode

Hash adalah angka yang dihasilkan dari suatu objek yang menggambarkan keadaannya pada suatu titik waktu. Nomor ini digunakan di Java terutama dalam tabel hash seperti HashMap. Dalam hal ini, fungsi hash untuk memperoleh angka berdasarkan suatu objek harus diimplementasikan sedemikian rupa untuk memastikan distribusi elemen yang relatif merata di seluruh tabel hash. Dan juga untuk meminimalkan kemungkinan tabrakan ketika fungsi mengembalikan nilai yang sama untuk kunci yang berbeda.

Kode hash kontrak

Untuk mengimplementasikan fungsi hash, spesifikasi bahasa mendefinisikan aturan berikut:
  • memanggil metode hashCodesatu kali atau lebih pada objek yang sama harus mengembalikan nilai hash yang sama, asalkan bidang objek yang terlibat dalam penghitungan nilai tidak berubah.
  • Memanggil metode hashCodepada dua objek harus selalu menghasilkan angka yang sama jika objeknya sama (memanggil metode equalspada objek ini akan mengembalikan true).
  • memanggil metode hashCodepada dua objek yang tidak sama harus menghasilkan nilai hash yang berbeda. Meskipun persyaratan ini tidak wajib, namun harus dipertimbangkan bahwa penerapannya akan berdampak positif pada kinerja tabel hash.

Metode sama dengan dan kode hash harus diganti secara bersamaan

Berdasarkan kontrak yang dijelaskan di atas, maka saat mengganti metode dalam kode Anda equals, Anda harus selalu mengganti metode tersebut hashCode. Karena sebenarnya dua instance dari suatu kelas berbeda karena berada di area memori yang berbeda, maka keduanya harus dibandingkan berdasarkan beberapa kriteria logis. Oleh karena itu, dua objek yang setara secara logis harus mengembalikan nilai hash yang sama. Apa yang terjadi jika hanya salah satu dari metode ini yang diganti?
  1. equalsya hashCodeTidak

    Katakanlah kita mendefinisikan metode equalsdi kelas kita dengan benar, dan hashCodememutuskan untuk membiarkan metode tersebut apa adanya di kelas Object. Maka dari sudut pandang metode equalskedua objek tersebut secara logis akan sama, sedangkan dari sudut pandang metode hashCodekeduanya tidak mempunyai kesamaan. Jadi, dengan menempatkan suatu objek dalam tabel hash, kita berisiko tidak mendapatkannya kembali dengan kunci.
    Misalnya seperti ini:

    Map<Point, String> m = new HashMap<>();
    m.put(new Point(1, 1),Point A);
    // pointName == null
    String pointName = m.get(new Point(1, 1));

    Jelasnya, benda yang ditempatkan dan benda yang dicari adalah dua benda yang berbeda, walaupun secara logika keduanya sama. Tapi karena mereka memiliki nilai hash yang berbeda karena kita melanggar kontrak, kita dapat mengatakan bahwa kita kehilangan objek kita di suatu tempat di dalam tabel hash.

  2. hashCodeya equalsTidak.

    Apa yang terjadi jika kita mengganti metode hashCodedan equalsmewarisi implementasi metode dari kelas Object. Seperti yang Anda ketahui, equalsmetode default hanya membandingkan pointer ke objek, menentukan apakah pointer tersebut merujuk ke objek yang sama. Mari kita asumsikan bahwa hashCodekita telah menulis metode sesuai dengan semua kanon, yaitu, menghasilkannya menggunakan IDE, dan metode tersebut akan mengembalikan nilai hash yang sama untuk objek yang identik secara logis. Jelasnya, dengan melakukan ini kita telah mendefinisikan beberapa mekanisme untuk membandingkan dua objek.

    Oleh karena itu, contoh dari paragraf sebelumnya secara teori harus dilaksanakan. Namun kami tetap tidak dapat menemukan objek kami di tabel hash. Meskipun kita akan mendekati ini, karena minimal kita akan menemukan keranjang tabel hash di mana objek tersebut akan berada.

    Agar berhasil mencari suatu objek dalam tabel hash, selain membandingkan nilai hash kunci, juga digunakan penentuan persamaan logika kunci dengan objek yang dicari. Artinya, equalstidak ada cara untuk melakukannya tanpa mengesampingkan metode tersebut.

Algoritma umum untuk menentukan kode hash

Di sini, menurut saya, Anda tidak perlu terlalu khawatir dan membuat metode di IDE favorit Anda. Karena semua perpindahan bit ke kanan dan kiri untuk mencari rasio emas, yaitu distribusi normal, adalah untuk pria yang benar-benar keras kepala. Secara pribadi, saya ragu apakah saya bisa melakukan lebih baik dan lebih cepat daripada Ide yang sama.

Alih-alih sebuah kesimpulan

Jadi, kita melihat bahwa metode equalsmemainkan hashCodeperan yang jelas dalam bahasa Java dan dirancang untuk memperoleh karakteristik persamaan logis dari dua objek. Dalam kasus metode, equalsini memiliki hubungan langsung dengan membandingkan objek, dalam kasus hashCodetidak langsung, ketika diperlukan, katakanlah, untuk menentukan perkiraan lokasi suatu objek dalam tabel hash atau struktur data serupa untuk meningkatkan kecepatan mencari suatu objek. Selain akad , equalsada hashCodesyarat lain yang berkaitan dengan perbandingan benda. Ini adalah konsistensi metode compareToantarmuka Comparabledengan file equals. Persyaratan ini mewajibkan pengembang untuk selalu kembali x.equals(y) == truekapan x.compareTo(y) == 0. Artinya, kita melihat bahwa perbandingan logis dari dua objek tidak boleh bertentangan dimanapun dalam penerapannya dan harus selalu konsisten.

Sumber

Java Efektif, Edisi Kedua. Joshua Bloch. Terjemahan gratis dari buku yang sangat bagus. Java, perpustakaan profesional. Jilid 1. Dasar-dasar. Kay Horstmann. Sedikit teori dan lebih banyak praktik. Namun semuanya tidak dianalisis sedetail yang dilakukan Bloch. Meskipun ada tampilan yang sama dengan(). Struktur data dalam gambar. HashMap Artikel yang sangat berguna tentang perangkat HashMap di Java. Daripada melihat sumbernya.
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION