object di Kotlin sudah lama menjadi cara standar mendeklarasikan singleton — instance tunggal yang dijamin oleh kompilator. Sayangnya, object biasa tidak menghasilkan toString yang informatif (defaultnya ClassName@hashcode) dan tidak menimbulkan equals/hashCode berbasis identitas. data object hadir sejak Kotlin 1.9 (stabil di 2.0) untuk mengatasi masalah tersebut: menggabungkan singleton dengan implementasi toString/equals/hashCode otomatis seperti data class. Pada chapter ini kita akan membandingkan object biasa dengan data object, mempraktikkan pola umum seperti sealed child, dan mengetahui kapan harus memilih data object dibanding alternatif lain.

object Biasa vs data object #

Perbedaan utama antara object biasa dan data object adalah implementasi toString, equals, dan hashCode:

Aspekobject biasadata object
Jumlah instance1 (singleton)1 (singleton)
toString defaultNama@hashcodeNama (nama objek saja)
equals defaultidentitas (===)identitas, tetapi jelas terbaca
hashCodealamat memoristabil berdasarkan instance
Serialisasimanuallebih ramah data

Karena data object tetap singleton, equals selalu mengembalikan true untuk dua referensi yang sama (sama dengan ===). Namun, toString yang rapi membuat log dan debug jauh lebih mudah dibaca.

Demonstrasi data object #

Cuplikan berikut membandingkan kedua pendekatan, memeriksa kontrak equals/hashCode, dan menunjukkan data object sebagai sealed child:

kotlin
// data_objects.kt
// Mendemonstrasikan `data object` (Kotlin 1.9+, stabil di 2.0).
// `data object` menghasilkan toString/equals/hashCode otomatis seperti `data class`,
// tetapi instance bersifat singleton (deklarasi object).

// 1. Singleton biasa - toString default tidak informatif (ClassName@hashcode)
object KonfigurasiLama {
    val versi = "1.0"
    val mode = "production"

    // Override manual agar toString terbaca
    override fun toString(): String = "KonfigurasiLama(versi=$versi, mode=$mode)"
}

// 2. data object - toString/equals/hashCode otomatis
data object KonfigurasiBaru {
    val versi = "2.0"
    val mode = "production"
    val fiturEksperimen = false
}

// 3. data object untuk deklarasi konstanta terstruktur
data object WarnaBrenda {
    const val PRIMER = "#FF0000"
    const val SEKUNDER = "#00FF00"
    const val LATAR = "#FFFFFF"
}

// 4. data object sebagai sealed child
sealed interface Bentuk {
    data object Lingkaran : Bentuk
    data object Persegi : Bentuk
    data class Segitiga(val sisi: Int) : Bentuk
}

fun deskripsiBentuk(b: Bentuk): String = when (b) {
    is Bentuk.Lingkaran -> "Lingkaran (singleton)"
    is Bentuk.Persegi -> "Persegi (singleton)"
    is Bentuk.Segitiga -> "Segitiga sisi=${b.sisi}"
}

fun main() {
    println("=== 1. Perbandingan toString ===")
    println("object biasa : $KonfigurasiLama")
    println("data object  : $KonfigurasiBaru")

    println()
    println("=== 2. equals antar instance ===")
    val refA = KonfigurasiBaru
    val refB = KonfigurasiBaru
    println("refA === refB : ${refA === refB}  // identitas singleton")
    println("refA == refB  : ${refA == refB}   // equals otomatis true")

    println()
    println("=== 3. hashCode konsisten ===")
    val hashA = refA.hashCode()
    val hashB = refB.hashCode()
    println("hashCode A == hashCode B : ${hashA == hashB}")
    println("HashCode untuk data object bersifat stabil untuk instance yang sama")

    println()
    println("=== 4. data object sebagai sealed child ===")
    val daftar: List<Bentuk> = listOf(
        Bentuk.Lingkaran,
        Bentuk.Persegi,
        Bentuk.Segitiga(3)
    )
    daftar.forEach { println("  ${deskripsiBentuk(it)}") }

    println()
    println("=== 5. Penggunaan sebagai konstanta terstruktur ===")
    println("Primer   : ${WarnaBrenda.PRIMER}")
    println("Sekunder : ${WarnaBrenda.SEKUNDER}")
    println("Latar    : ${WarnaBrenda.LATAR}")
}

Output yang Dihasilkan:

text
=== 1. Perbandingan toString ===
object biasa : KonfigurasiLama(versi=1.0, mode=production)
data object  : KonfigurasiBaru

=== 2. equals antar instance ===
refA === refB : true  // identitas singleton
refA == refB  : true   // equals otomatis true

=== 3. hashCode konsisten ===
hashCode A == hashCode B : true
HashCode untuk data object bersifat stabil untuk instance yang sama

=== 4. data object sebagai sealed child ===
  Lingkaran (singleton)
  Persegi (singleton)
  Segitiga sisi=3

=== 5. Penggunaan sebagai konstanta terstruktur ===
Primer   : #FF0000
Sekunder : #00FF00
Latar    : #FFFFFF

Pada baris deklarasi, data object KonfigurasiBaru memicu kompilator men-generate toString yang mengembalikan "KonfigurasiBaru" (bukan KonfigurasiBaru@hexAddress). Saat dipakai sebagai sealed child, data object Lingkaran : Bentuk membuat branch when tetap exhaustive — kompilator memaksa kita menangani semua kasus tanpa else.

Pola Umum data object #

Tiga pola paling umum untuk data object:

  1. Deklarasi konstanta terstruktur — kumpulan konstanta terkait yang ingin diberi namespace:

    kotlin Konstanta terstruktur
    data object WarnaBrenda {
        const val PRIMER = "#FF0000"
        const val SEKUNDER = "#00FF00"
        const val LATAR = "#FFFFFF"
    }
  2. Sealed child tanpa state — anggota sealed class yang tidak punya data tambahan:

    kotlin Sealed child singleton
    sealed interface Aksi {
        data object Mulai : Aksi
        data object Hentikan : Aksi
        data class Custom(val nama: String) : Aksi
    }
  3. State object untuk companion — objek pendamping class yang membutuhkan toString rapi untuk logging.

data object vs data class vs object #

Pilihan antara data class, object, dan data object bergantung pada apakah kita butuh instance jamak dan apakah ada data anggota:

KonstrukInstance jamak?Data anggota?toString otomatis?
data class Foo(val x: Int)yayaya
object Footidak (singleton)ya (properti)tidak
data object Footidak (singleton)ya (properti)ya

Untuk singleton dengan banyak properti yang perlu toString rapi (mis. konfigurasi global, registry), data object adalah pilihan terbaik. Untuk singleton sederhana tanpa properti, object biasa cukup.

Jebakan Umum #

Beberapa kesalahan yang sering muncul:

  1. Mengira data object menghasilkan copy() — tidak. Karena singleton, copy() tidak ada artinya. Kompilator tidak men-generate copy() untuk data object.
  2. Menganggap hashCode stabil lintas JVM — hashCode dari data object berdasarkan instance, jadi dua instance data object di JVM yang berbeda akan punya hashCode yang berbeda. Untuk cache yang persisten, simpan nilai konkret (mis. ID string) di properti.
  3. Memakai data object sebagai pengganti class — bila kita butuh banyak instance, data class lebih tepat.

Referensi Lanjutan #

  • What’s new in Kotlin 2.0 — catatan rilis Kotlin 2.0 yang memperkenalkan deklarasi data object untuk singleton dengan toString dan equals otomatis
  • Kotlin documentation home — halaman utama dokumentasi Kotlin sebagai rujukan struktur data object modern

Selanjutnya #

Setelah menguasai data object, kita beranjak ke fitur yang membantu menulis when lebih ekspresif — guard conditions — yang memungkinkan when (x) { is String && x.length > 5 -> ... }.