Banyak class yang kita tulis hanyalah wadah data — sekumpulan property dengan method equals, hashCode, dan toString yang membosankan untuk ditulis ulang di setiap class. Kotlin menyediakan keyword data pada deklarasi class yang membuat kompilator otomatis menghasilkan method tersebut berdasarkan property yang dideklarasikan di primary constructor. Cukup tulis data class Pengguna(val nama: String, val umur: Int), dan kita mendapatkan class lengkap dengan perbandingan struktur, hash konsisten, cetakan string yang informatif, method copy untuk modifikasi sebagian, dan method componentN untuk destrukturisasi. Pada chapter ini kita akan mendeklarasikan beberapa data class dan mengamati kelima method yang dihasilkan kompilator.

Apa Itu data class #

data class adalah class yang dideklarasikan dengan keyword data sebelum class. Kompilator Kotlin menghasilkan method berikut secara otomatis berdasarkan property di primary constructor:

  • equals(other: Any?): membandingkan dua instance secara struktural — true bila seluruh property sama.
  • hashCode(): nilai hash yang konsisten dengan equals — dua instance yang equal menghasilkan hashCode yang sama.
  • toString(): format "NamaClass(prop1=val1, prop2=val2)" yang mudah dibaca.
  • copy(...): membuat salinan instance dengan opsi mengubah sebagian property.
  • component1(), component2(), dst.: mendukung destrukturisasi val (a, b) = instance.

Syarat utama data class: harus memiliki primary constructor dengan minimal satu property (dengan val atau var). Constructor dapat memiliki default value, dan class tetap dapat menambah method biasa di body.

Deklarasi Dasar #

Contoh berikut mendefinisikan empat data class — Pengguna, Buku, Titik, dan ItemKeranjang — yang mendemonstrasikan method auto-generated: equals, hashCode, toString, copy, dan componentN.

kotlin
// data_classes.kt
// Mendemonstrasikan `data class` Kotlin — auto-generates equals/hashCode/
// toString/copy/componentN.

data class Pengguna(val nama: String, val umur: Int)

data class Buku(
    val judul: String,
    val penulis: String,
    val tahun: Int,
    val kategori: String = "Umum",
)

// Data class dengan custom equals — auto-generated tetap dipakai bila tidak
// di-override. Di sini kita menambahkan method biasa untuk demonstrasi.
data class Titik(val x: Int, val y: Int) {
    // Method tambahan tetap dapat ditambahkan ke data class.
    fun jarakKe(lain: Titik): Double {
        val dx = (lain.x - x).toDouble()
        val dy = (lain.y - y).toDouble()
        return Math.sqrt(dx * dx + dy * dy)
    }
}

// Data class untuk struktur ringkas, mirip record di Java.
data class ItemKeranjang(val produk: String, val harga: Int, val jumlah: Int) {
    fun total(): Int = harga * jumlah
}

fun main() {
    val p1 = Pengguna("Budi", 25)
    val p2 = Pengguna("Budi", 25)
    val p3 = Pengguna("Siti", 30)

    println("p1 = $p1")
    println("p2 = $p2")
    println("p1 == p2 ? ${p1 == p2}")   // structural equals
    println("p1 === p2 ? ${p1 === p2}") // referential
    println("p1 == p3 ? ${p1 == p3}")
    println()

    // hashCode konsisten dengan equals.
    println("hashCode p1 = ${p1.hashCode()}")
    println("hashCode p2 = ${p2.hashCode()}")
    println("hashCode p1 == hashCode p2 ? ${p1.hashCode() == p2.hashCode()}")
    println()

    // copy dengan modifikasi sebagian field.
    val p4 = p1.copy(umur = 26)
    println("p1.copy(umur = 26) = $p4")
    println("p1 setelah copy tetap = $p1")
    println()

    // Destructuring via componentN.
    val (nama, umur) = p1
    println("Destructure: nama=$nama, umur=$umur")
    println("p1.component1()=${p1.component1()}, p1.component2()=${p1.component2()}")
    println()

    val buku1 = Buku("Belajar Kotlin", "Budi", 2026)
    val buku2 = Buku("Belajar Kotlin", "Budi", 2026, "Pemrograman")
    println("buku1 = $buku1")
    println("buku2 = $buku2")
    println("buku1 == buku2 ? ${buku1 == buku2}")
    println()

    val a = Titik(0, 0)
    val b = Titik(3, 4)
    println("a = $a, b = $b")
    println("jarak a -> b = ${a.jarakKe(b)}")
    println()

    val keranjang = listOf(
        ItemKeranjang("Buku", 50_000, 2),
        ItemKeranjang("Pulpen", 5_000, 5),
        ItemKeranjang("Map", 3_000, 4),
    )
    val totalBelanja = keranjang.sumOf { it.total() }
    println("Daftar belanja:")
    for (item in keranjang) {
        println("  - ${item.produk}: ${item.harga} x ${item.jumlah} = ${item.total()}")
    }
    println("Total belanja = $totalBelanja")
}

Output yang Dihasilkan:

text
p1 = Pengguna(nama=Budi, umur=25)
p2 = Pengguna(nama=Budi, umur=25)
p1 == p2 ? true
p1 === p2 ? false
p1 == p3 ? false

hashCode p1 = 64537313
hashCode p2 = 64537313
hashCode p1 == hashCode p2 ? true

p1.copy(umur = 26) = Pengguna(nama=Budi, umur=26)
p1 setelah copy tetap = Pengguna(nama=Budi, umur=25)

Destructure: nama=Budi, umur=25
p1.component1()=Budi, p1.component2()=25

buku1 = Buku(judul=Belajar Kotlin, penulis=Budi, tahun=2026, kategori=Umum)
buku2 = Buku(judul=Belajar Kotlin, penulis=Budi, tahun=2026, kategori=Pemrograman)
buku1 == buku2 ? false

a = Titik(x=0, y=0), b = Titik(x=3, y=4)
jarak a -> b = 5.0

Daftar belanja:
  - Buku: 50000 x 2 = 100000
  - Pulpen: 5000 x 5 = 25000
  - Map: 3000 x 4 = 12000
Total belanja = 137000

Mari kita bedah kode di atas:

  1. data class Pengguna(val nama: String, val umur: Int): deklarasi satu baris. Kompilator otomatis menghasilkan kelima method (equals, hashCode, toString, copy, componentN) berdasarkan dua property ini. Padanannya di Java membutuhkan sekitar 40 baris boilerplate.
  2. p1 == p2 ? true: operator == di Kotlin memanggil equals(). Karena p1 dan p2 memiliki nama dan umur yang sama, hasilnya true. Bandingkan dengan p1 === p2 yang menghasilkan false karena === membandingkan referensi memori, dan p1/p2 adalah instance yang berbeda.
  3. p1.hashCode() == p2.hashCode() ? true: kontrak hashCode mensyaratkan dua instance yang equals juga memiliki hashCode yang sama. Data class memenuhi kontrak ini secara otomatis — penting saat instance dipakai sebagai kunci di HashMap atau elemen HashSet.
  4. p1.copy(umur = 26): membuat salinan p1 dengan umur diganti 26. Property yang tidak disebut (nama) tetap membawa nilai dari p1. Pola ini memudahkan pembuatan instance baru yang hampir sama dengan instance lama, tanpa mengubah instance asli.
  5. val (nama, umur) = p1: destrukturisasi. Sintaks ini memanggil p1.component1() untuk mengisi nama dan p1.component2() untuk mengisi umur. Kedua method componentN ini dihasilkan otomatis untuk setiap property di primary constructor.
  6. data class Buku(..., val kategori: String = "Umum"): property dengan default value. Saat membuat instance, pemanggil dapat mengosongkan kategori untuk memakai default, atau menyediakan nilai eksplisit. Perhatikan buku1 == buku2 menghasilkan false karena kategori berbeda ("Umum" vs "Pemrograman") — semua property diperhitungkan dalam equals.
  7. data class Titik(...) { fun jarakKe(lain: Titik): Double { ... } }: data class tetap dapat menambah method biasa di body. Method jarakKe menghitung jarak Euclidean antara dua titik — method ini bukan bagian dari property sehingga tidak memengaruhi equals atau hashCode.
  8. ItemKeranjang("Buku", 50_000, 2) dengan fun total(): Int = harga * jumlah: data class dengan method bantu. Pola umum untuk model domain — property menyimpan state, method menghitung turunan. _ di literal 50_000 adalah pemisah visual yang memudahkan pembacaan angka besar.

Method yang Dihasilkan #

Tabel berikut merangkum method yang dihasilkan otomatis untuk data class Pengguna(val nama: String, val umur: Int):

MethodYang dihasilkan kompilatorPemakaian
equalsBandingkan nama dan umurp1 == p2
hashCodeHash dari nama dan umurset.add(p1)
toString"Pengguna(nama=Budi, umur=25)"println(p1)
copySalinan dengan override sebagianp1.copy(umur = 26)
component1Mengembalikan namaval (n, _) = p1
component2Mengembalikan umurval (_, u) = p1

Batasan data class #

Data class memiliki beberapa batasan: tidak boleh di-open (tidak dapat diwariskan secara default, kecuali sejak Kotlin 1.1 dapat diwariskan dengan syarat ketat), tidak boleh abstract atau sealed, dan tidak boleh mewarisi data class lain. Untuk class yang memerlukan inheritance, gunakan class biasa dan tulis method equals/hashCode secara manual bila diperlukan. Untuk model murni yang hanya menyimpan data, data class adalah pilihan terbaik.

Referensi Lanjutan #

  • Data classes — panduan resmi data class dan method yang dihasilkan otomatis

Ringkasan #

data class menghasilkan method equals, hashCode, toString, copy, dan componentN secara otomatis berdasarkan property di primary constructor. Satu baris deklarasi menggantikan puluhan baris boilerplate Java. Data class tetap dapat memiliki method biasa di body dan property dengan default value. Pada chapter enum class berikutnya, kita akan melihat class khusus untuk merepresentasikan sekumpulan nilai konstan.