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:
| Aspek | object biasa | data object |
|---|---|---|
| Jumlah instance | 1 (singleton) | 1 (singleton) |
toString default | Nama@hashcode | Nama (nama objek saja) |
equals default | identitas (===) | identitas, tetapi jelas terbaca |
hashCode | alamat memori | stabil berdasarkan instance |
| Serialisasi | manual | lebih 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:
// 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:
=== 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 : #FFFFFFPada 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:
Deklarasi konstanta terstruktur — kumpulan konstanta terkait yang ingin diberi namespace:
kotlin Konstanta terstrukturdata object WarnaBrenda { const val PRIMER = "#FF0000" const val SEKUNDER = "#00FF00" const val LATAR = "#FFFFFF" }Sealed child tanpa state — anggota sealed class yang tidak punya data tambahan:
kotlin Sealed child singletonsealed interface Aksi { data object Mulai : Aksi data object Hentikan : Aksi data class Custom(val nama: String) : Aksi }State object untuk companion — objek pendamping class yang membutuhkan
toStringrapi 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:
| Konstruk | Instance jamak? | Data anggota? | toString otomatis? |
|---|---|---|---|
data class Foo(val x: Int) | ya | ya | ya |
object Foo | tidak (singleton) | ya (properti) | tidak |
data object Foo | tidak (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:
- Mengira
data objectmenghasilkancopy()— tidak. Karena singleton,copy()tidak ada artinya. Kompilator tidak men-generatecopy()untukdata object. - Menganggap
hashCodestabil lintas JVM —hashCodedaridata objectberdasarkan instance, jadi dua instancedata objectdi JVM yang berbeda akan punyahashCodeyang berbeda. Untuk cache yang persisten, simpan nilai konkret (mis. ID string) di properti. - Memakai
data objectsebagai pengganticlass— bila kita butuh banyak instance,data classlebih tepat.
Referensi Lanjutan #
- What’s new in Kotlin 2.0
— catatan rilis Kotlin 2.0 yang memperkenalkan deklarasi
data objectuntuk singleton dengantoStringdanequalsotomatis - Kotlin documentation home
— halaman utama dokumentasi Kotlin sebagai rujukan struktur
data objectmodern
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 -> ... }.