Membandingkan dua object adalah operasi yang sering muncul di kode: apakah dua pelanggan sama? apakah poin ini berada di lokasi yang sama dengan poin itu? Kotlin membedakan dua jenis perbandingan — structural equality (==) yang memanggil method equals, dan referential identity (===) yang membandingkan referensi memori. Kebingungan sering muncul ketika class tidak mengoverride equals dengan benar, atau ketika hashCode tidak konsisten dengan equals. Pada chapter ini kita akan membandingkan == dan ===, mengoverride equals dan hashCode secara manual, dan melihat bagaimana data class menyelesaikan kedua hal tersebut secara otomatis.
Perbedaan == dan === #
Operator == di Kotlin memanggil method equals(other: Any?) — ini disebut structural equality. Dua object dianggap sama bila method equals mengembalikan true. Operator != adalah kebalikannya. Kotlin juga memastikan == aman terhadap null — bila salah satu operand null, kompilator tidak melempar NullPointerException; bila keduanya null, hasilnya true.
Operator === membandingkan referensi memori — apakah kedua operand adalah instance yang sama persis di heap. Ini disebut *referential identity. Operator !==adalah kebalikannya.===tidak memanggilequals`; ia hanya membandingkan alamat memori.
val a = Poin(1, 2)
val b = Poin(1, 2)
println(a == b) // true bila equals ditulis untuk perbandingan struktural
println(a === b) // false — instance berbeda di heapClass Tanpa Override equals #
Contoh berikut mendefinisikan tiga class — PoinSederhana tanpa override, Poin dengan override manual, dan PoinData sebagai data class. Contoh juga menunjukkan class Pelanggan yang mengoverride equals hanya berdasarkan id, mengabaikan nama.
// equality_dan_identity.kt
// Mendemonstrasikan `==` (structural equals) vs `===` (referential)
// dan override `equals()` manual.
class PoinSederhana(val x: Int, val y: Int) {
// Tanpa override equals, `==` memakai referential identity.
}
class Poin(val x: Int, val y: Int) {
// Override equals untuk perbandingan struktural manual.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Poin) return false
return x == other.x && y == other.y
}
override fun hashCode(): Int {
// Konsisten dengan equals: dua instance yang equal harus hashCode sama.
var hasil = x
hasil = 31 * hasil + y
return hasil
}
override fun toString(): String = "Poin(x=$x, y=$y)"
}
data class PoinData(val x: Int, val y: Int)
class Pelanggan(val id: Int, val nama: String) {
// Override equals hanya berdasarkan id — dua pelanggan dianggap sama
// bila id-nya sama, terlepas dari nama.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Pelanggan) return false
return id == other.id
}
override fun hashCode(): Int = id
override fun toString(): String = "Pelanggan(id=$id, nama='$nama')"
}
fun main() {
val a = PoinSederhana(1, 2)
val b = PoinSederhana(1, 2)
println("PoinSederhana:")
println(" a == b ? ${a == b}") // false — referensial berbeda
println(" a === b ? ${a === b}") // false — instance berbeda
println()
val p1 = Poin(3, 4)
val p2 = Poin(3, 4)
val p3 = Poin(5, 6)
println("Poin (override equals):")
println(" p1 = $p1")
println(" p2 = $p2")
println(" p1 == p2 ? ${p1 == p2}") // true — struktural sama
println(" p1 === p2 ? ${p1 === p2}") // false — instance berbeda
println(" p1 == p3 ? ${p1 == p3}")
println(" hashCode p1 = ${p1.hashCode()}")
println(" hashCode p2 = ${p2.hashCode()}")
println(" hashCode konsisten? ${p1.hashCode() == p2.hashCode()}")
println()
val d1 = PoinData(3, 4)
val d2 = PoinData(3, 4)
println("PoinData (data class auto equals):")
println(" d1 == d2 ? ${d1 == d2}")
println(" d1 === d2 ? ${d1 === d2}")
println()
val c1 = Pelanggan(1, "Budi")
val c2 = Pelanggan(1, "Siti")
val c3 = Pelanggan(2, "Budi")
println("Pelanggan (equals berdasarkan id):")
println(" c1 = $c1")
println(" c2 = $c2")
println(" c3 = $c3")
println(" c1 == c2 ? ${c1 == c2}") // true — id sama walau nama beda
println(" c1 == c3 ? ${c1 == c3}") // false — id beda walau nama sama
println()
// Nullable equality: == aman terhadap null.
val p: Poin? = null
val q: Poin? = Poin(0, 0)
println("Nullable equality:")
println(" null == q ? ${p == q}") // false, tidak lempar NPE
println(" q == null ? ${q == null}")
println(" null == null ? ${Poin(0, 0).let { null == p }}")
}Output yang Dihasilkan:
PoinSederhana:
a == b ? false
a === b ? false
Poin (override equals):
p1 = Poin(x=3, y=4)
p2 = Poin(x=3, y=4)
p1 == p2 ? true
p1 === p2 ? false
p1 == p3 ? false
hashCode p1 = 97
hashCode p2 = 97
hashCode konsisten? true
PoinData (data class auto equals):
d1 == d2 ? true
d1 === d2 ? false
Pelanggan (equals berdasarkan id):
c1 = Pelanggan(id=1, nama='Budi')
c2 = Pelanggan(id=1, nama='Siti')
c3 = Pelanggan(id=2, nama='Budi')
c1 == c2 ? true
c1 == c3 ? false
Nullable equality:
null == q ? false
q == null ? false
null == null ? trueMari kita bedah kode di atas:
class PoinSederhana(val x: Int, val y: Int): class tanpa overrideequals. Outputa == b ? falsemembuktikan bahwa class seperti ini memakai implementasiequalsbawaan dariAny— yang hanya membandingkan referensi memori. Dua instance dengan nilaixdanyyang sama tetap dianggap tidak equal karena bukan instance yang sama.override fun equals(other: Any?): Boolean { if (this === other) return true; if (other !is Poin) return false; return x == other.x && y == other.y }: implementasi manualequals. Baris pertamathis === otheradalah optimisasi cepat — bila kedua operand adalah instance yang sama, langsung kembalikantrue. Baris kedua memeriksa tipeother— bila bukanPoin, kembalikanfalse. Baris ketiga membandingkanxdany— dua poin dianggap equal bila kedua property sama.override fun hashCode(): Int { var hasil = x; hasil = 31 * hasil + y; return hasil }: implementasihashCodekonsisten denganequals. Bila dua instance equal, hashCode mereka juga harus sama. Pola31 * hasil + yadalah rumus hash yang umum —31dipilih karena ganjil dan menghasilkan distribusi hash yang baik. Pemanggilanp1.hashCode() == p2.hashCode()menghasilkantruekarenap1danp2equal.p1 == p2 ? true: setelah override, operator==sekarang memanggil methodequalsyang baru. Karenap1.x == p2.xdanp1.y == p2.y, hasilnyatrue. Berbeda dariPoinSederhanayang mengembalikanfalse.p1 === p2 ? false: operator===tidak terpengaruh oleh overrideequals. Karenap1danp2adalah instance berbeda di heap (dibuat dari pemanggilanPoin(3, 4)dua kali),===tetap mengembalikanfalse.data class PoinData(val x: Int, val y: Int):data classotomatis menghasilkanequalsdanhashCodeberdasarkan property di primary constructor. Outputd1 == d2 ? truemembuktikan auto-generatedequalsmembandingkanxdanysecara struktural.class Pelanggan(val id: Int, val nama: String) { override fun equals(other) = ... id == other.id; override fun hashCode() = id }: overrideequalshanya membandingkanid, mengabaikannama. Outputc1 == c2 ? truemembuktikan dua pelanggan denganid=1dianggap equal meskipun namanya berbeda ("Budi"vs"Siti"). Sebaliknyac1 == c3 ? falsekarenaidberbeda (1 vs 2) walau nama sama.hashCodejuga hanya mengembalikanidagar konsisten.null == q ? false: Kotlin memastikan==aman terhadapnull. Bila salah satu operandnull, kompilator tidak memanggilequalspada null — langsung mengembalikanfalse(atautruebila keduanyanull). Pola ini menghilangkan kebutuhan menulisif (a != null) a.equals(b) else b == nullmanual seperti di Java.
Kontrak equals dan hashCode #
Java dan Kotlin mewarisi kontrak equals dan hashCode dari java.lang.Object:
- Refleksif:
a == aharustrue. - Simetris: bila
a == b, makab == a. - Transitif: bila
a == bdanb == c, makaa == c. - Konsisten: pemanggilan
equalsberulang tidak berubah selama property tidak diubah. - Konsistensi hashCode: bila
a == b, makaa.hashCode() == b.hashCode(). Sebaliknya tidak wajib — dua object dengan hashCode yang sama boleh tidak equal.
Bila kita override equals tanpa mengoverride hashCode, class akan rusak saat dipakai di HashMap, HashSet, atau struktur lain yang mengandalkan hashCode. Selalu override keduanya bersama-sama.
data class Menyelesaikan Persoalan #
Cara paling mudah menghindari kesalahan equals/hashCode adalah memakai data class. Kompilator otomatis menghasilkan kedua method berdasarkan property di primary constructor, dengan implementasi yang konsisten dengan kontrak. data class adalah pilihan terbaik untuk class yang hanya menyimpan data dan dianggap equal bila seluruh property sama.
Referensi Lanjutan #
- Equality
— penjelasan resmi
==(equality) dan===(identity)
Ringkasan #
Operator == memanggil equals untuk perbandingan struktural, sedangkan === membandingkan referensi memori. Override equals memungkinkan class membandingkan diri secara struktural, namun hashCode juga wajib di-override agar konsisten. data class menyelesaikan persoalan ini secara otomatis. Pada chapter copy dan component Functions berikutnya, kita akan melihat method copy dan componentN yang juga otomatis dihasilkan oleh data class.