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.

kotlin Contoh: perbedaan == dan ===
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 heap

Class 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.

kotlin
// 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:

text
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 ? true

Mari kita bedah kode di atas:

  1. class PoinSederhana(val x: Int, val y: Int): class tanpa override equals. Output a == b ? false membuktikan bahwa class seperti ini memakai implementasi equals bawaan dari Any — yang hanya membandingkan referensi memori. Dua instance dengan nilai x dan y yang sama tetap dianggap tidak equal karena bukan instance yang sama.
  2. 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 manual equals. Baris pertama this === other adalah optimisasi cepat — bila kedua operand adalah instance yang sama, langsung kembalikan true. Baris kedua memeriksa tipe other — bila bukan Poin, kembalikan false. Baris ketiga membandingkan x dan y — dua poin dianggap equal bila kedua property sama.
  3. override fun hashCode(): Int { var hasil = x; hasil = 31 * hasil + y; return hasil }: implementasi hashCode konsisten dengan equals. Bila dua instance equal, hashCode mereka juga harus sama. Pola 31 * hasil + y adalah rumus hash yang umum — 31 dipilih karena ganjil dan menghasilkan distribusi hash yang baik. Pemanggilan p1.hashCode() == p2.hashCode() menghasilkan true karena p1 dan p2 equal.
  4. p1 == p2 ? true: setelah override, operator == sekarang memanggil method equals yang baru. Karena p1.x == p2.x dan p1.y == p2.y, hasilnya true. Berbeda dari PoinSederhana yang mengembalikan false.
  5. p1 === p2 ? false: operator === tidak terpengaruh oleh override equals. Karena p1 dan p2 adalah instance berbeda di heap (dibuat dari pemanggilan Poin(3, 4) dua kali), === tetap mengembalikan false.
  6. data class PoinData(val x: Int, val y: Int): data class otomatis menghasilkan equals dan hashCode berdasarkan property di primary constructor. Output d1 == d2 ? true membuktikan auto-generated equals membandingkan x dan y secara struktural.
  7. class Pelanggan(val id: Int, val nama: String) { override fun equals(other) = ... id == other.id; override fun hashCode() = id }: override equals hanya membandingkan id, mengabaikan nama. Output c1 == c2 ? true membuktikan dua pelanggan dengan id=1 dianggap equal meskipun namanya berbeda ("Budi" vs "Siti"). Sebaliknya c1 == c3 ? false karena id berbeda (1 vs 2) walau nama sama. hashCode juga hanya mengembalikan id agar konsisten.
  8. null == q ? false: Kotlin memastikan == aman terhadap null. Bila salah satu operand null, kompilator tidak memanggil equals pada null — langsung mengembalikan false (atau true bila keduanya null). Pola ini menghilangkan kebutuhan menulis if (a != null) a.equals(b) else b == null manual seperti di Java.

Kontrak equals dan hashCode #

Java dan Kotlin mewarisi kontrak equals dan hashCode dari java.lang.Object:

  • Refleksif: a == a harus true.
  • Simetris: bila a == b, maka b == a.
  • Transitif: bila a == b dan b == c, maka a == c.
  • Konsisten: pemanggilan equals berulang tidak berubah selama property tidak diubah.
  • Konsistensi hashCode: bila a == b, maka a.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.