Setiap kelas Kotlin mewarisi tiga metode dari tipe induk Any, yaitu equals untuk perbandingan kesetaraan, hashCode untuk angka penanda hash, dan toString untuk perwakilan teks. Perilaku bawaan ketiganya berbasis identitas, sehingga dua instans dengan isi sama tetap dianggap berbeda. Topik ini membahas perilaku bawaan tersebut, cara menimpa ketiga metode secara manual, kontrak yang mengikat equals dan hashCode, serta bukti perubahannya pada HashSet. Seluruh contoh dijalankan nyata dan keluarannya ditampilkan sebagaimana adanya.
Perilaku bawaan dari Any#
Tanpa penimpaan apa pun, equals membandingkan identitas, yaitu sama tidaknya dua variabel menunjuk instans yang sama di memori. hashCode bawaan turun dari identitas itu juga, sedangkan toString bawaan menghasilkan nama kelas, tanda @, dan angka heksadesimal dari identitas instans. Bentuk toString semacam itu tidak membawa informasi isi, sehingga kurang berguna pada catatan log maupun pesan error.
Buat berkas main.kt berikut, lalu jalankan dengan dua perintah di bawahnya.
class KartuBiasa(val nomor: Int, val nama: String)
class KartuUjian(val nomor: Int, val nama: String) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is KartuUjian) return false
return nomor == other.nomor && nama == other.nama
}
override fun hashCode(): Int {
var hasil = nomor
hasil = 31 * hasil + nama.hashCode()
return hasil
}
override fun toString(): String {
return "KartuUjian(nomor=$nomor, nama=$nama)"
}
}
fun main() {
val a = KartuBiasa(1, "Ali")
val b = KartuBiasa(1, "Ali")
val setA = HashSet<KartuBiasa>()
setA.add(a)
setA.add(b)
println("Set tanpa override menampung ${setA.size} elemen")
println("Perbandingan a == b tanpa override: ${a == b}")
val c = KartuUjian(1, "Ali")
val d = KartuUjian(1, "Ali")
println("Perbandingan c == d dengan override: ${c == d}")
println("hashCode c sama dengan hashCode d: ${c.hashCode() == d.hashCode()}")
val setB = HashSet<KartuUjian>()
setB.add(c)
setB.add(d)
println("Set dengan override menampung ${setB.size} elemen")
println(d)
}# compiler Kotlin dan pustaka standar yang dipakai jalur ini
export PATH=/home/kal/my-project/tools/kotlinc/bin:/home/kal/my-project/tools/jdk-merged/bin:$PATH
export STDLIB=/home/kal/my-project/tools/kotlinc/lib/kotlin-stdlib.jar
# kompilasi berkas main.kt menjadi out.jar, lalu jalankan programnya
kotlinc main.kt -d out.jar && java -cp "out.jar:$STDLIB" MainKtSet tanpa override menampung 2 elemen
Perbandingan a == b tanpa override: false
Perbandingan c == d dengan override: true
hashCode c sama dengan hashCode d: true
Set dengan override menampung 1 elemen
KartuUjian(nomor=1, nama=Ali)Dua kartu KartuBiasa berisi nomor dan nama yang sama persis, tetapi HashSet tetap menampung keduanya dan perbandingan a == b bernilai false, karena kesetaraannya diukur dari identitas memori. Kartu KartuUjian berisi data yang sama dan menghasilkan nilai berlawanan pada ketiga pengukuran, yaitu c == d bernilai true, kode hash keduanya sama, dan HashSet hanya menampung satu elemen. Baris terakhir memperlihatkan toString hasil penimpaan, yang menuliskan isi kartu secara terbaca.
Menimpa equals#
Penimpaan equals pada KartuUjian mengikuti pola tiga langkah yang aman. Pemeriksaan this === other mengembalikan true ketika pembanding adalah instans yang sama, sehingga sisa pekerjaannya ditiadakan. Pemeriksaan other !is KartuUjian menolak tipe lain sekaligus memberi smart cast, sehingga akses other.nomor pada baris berikutnya sah tanpa konversi manual. Langkah ketiga membandingkan seluruh field yang menentukan kesetaraan, yaitu nomor dan nama.
Tanda == pada Kotlin diteruskan ke equals, sehingga c == d dan c.equals(d) menghasilkan keputusan yang sama. Perbedaannya terletak pada tanda ===, yang selalu membandingkan identitas dan tidak dapat ditimpa. Parameter equals bertipe Any? agar pembandingan terhadap null maupun tipe lain tetap sah dan selalu berujung pada false.
Menimpa hashCode dan kontraknya#
hashCode menghasilkan satu bilangan Int dari isi objek, dan bilangan itu dipakai oleh HashSet maupun HashMap untuk menentukan letak penyimpanan elemen. Implementasi pada KartuUjian mengikuti idiom yang lazim, yaitu memulai dari field pertama, lalu menggabungkan field berikutnya dengan pengganda 31 dan penjumlahan. Angka 31 dipilih karena perkaliannya murah dijalankan processor dan hasilnya menyebar cukup merata.
Kontrak yang mengikat keduanya berbunyi sederhana, yaitu objek yang sama menurut equals wajib menghasilkan hashCode yang sama. Kebalikannya tidak berlaku, karena dua objek yang berbeda boleh kebetulan berkode hash sama, dan kejadian itu disebut tabrakan hash. Pelanggaran kontrak tidak selalu terdeteksi compiler, tetapi akibatnya nyata pada HashSet, yang elemennya dapat terduplikasi atau justru tidak ditemukan saat pencarian.
Menimpa toString#
toString menentukan teks yang muncul ketika objek dicetak, disisipkan ke dalam string template, atau dicatat ke log. Penimpaan pada KartuUjian menghasilkan KartuUjian(nomor=1, nama=Ali), sebuah format yang menampung nama kelas beserta field pentingnya. Format serupa dipakai luas di ekosistem Kotlin, sehingga keluaran seperti itu mudah dibaca dan mudah dicari.
Kebalikannya terlihat pada KartuBiasa yang tidak menimpa apa pun. Mencetak instansnya akan menghasilkan nama kelas diikuti @ dan angka heksadesimal yang berbeda pada setiap kali program dijalankan, sehingga teks itu tidak layak dijadikan penanda. Kelas yang muncul pada keluaran program sebaiknya selalu menimpa toString, atau dibuat sebagai data class yang menghasilkan ketiga metode ini secara otomatis.
Kesalahan umum#
Kesalahan pertama adalah membandingkan field satu per satu tanpa memeriksa tipe pembanding terlebih dahulu. Konversi tipe yang langsung dilakukan tanpa pemeriksaan is akan menghentikan program dengan pengecualian ketika pembandingnya bertipe lain, sedangkan pola tiga langkah pada program utama menolaknya dengan aman. Kesalahan kedua adalah menyertakan properti yang berubah-ubah ke dalam equals dan hashCode, karena objek yang sedang menjadi kunci HashMap akan hilang jejaknya begitu nilai propertinya diganti.
Kesalahan ketiga adalah menganggap dua objek berkode hash sama sebagai objek yang setara. Kode hash yang sama hanyalah petunjuk awal, dan keputusan kesetaraan tetap milik equals. Menyamakan keduanya menghasilkan perbandingan yang salah pada objek berbeda yang kebetulan bertabrakan kodenya.
Ringkasan#
equals,hashCode, dantoStringdiwarisi dariAnydan berbasis identitas sebelum ditimpa.- Penimpaan
equalsyang aman memeriksa identitas dengan===, menolak tipe lain dengan!is, lalu membandingkan seluruh field penentu kesetaraan. - Kontraknya menyatakan objek yang setara menurut
equalswajib berkode hash sama, sedangkan kebalikannya tidak dijamin. HashSetdanHashMapmengandalkan kontrak tersebut, dandata classmenghasilkan ketiga metode ini secara otomatis untuk kelas yang murni memuat data.
Referensi#
- Equality — Kotlin Documentation — Dokumentasi resmi kesetaraan struktural dan referensial beserta aturan equals dan hashCode.
- Classes — Kotlin Documentation — Dokumentasi resmi kelas Kotlin, termasuk relasi tipe terhadap Any.