Setiap aplikasi yang menyimpan atau mengirim data perlu mengubah object Kotlin menjadi format yang dapat dipersistensi—byte array, teks JSON, atau baris CSV. Proses ini disebut serialisasi. Kebalikannya, deserialisasi, mengubah byte atau teks kembali menjadi object. Tanpa serialisasi, kita tidak dapat menyimpan pengguna ke database, mengirim transaksi via HTTP, atau men-cache hasil query di Redis. Pada chapter ini kita akan memahami konsep serialisasi, melihat tiga pendekatan berbeda di Kotlin—binary manual, Java Serializable, dan text JSON—serta strategi skema evolusi agar data lama tetap dapat dibaca saat model berubah.
Konsep Serialisasi #
Serialisasi mengubah object Kotlin dalam memori menjadi urutan byte atau teks yang dapat dipersistensi. Object Pengguna(id=1001, nama="Budi") di memori berisi field yang tersebar di alamat berbeda—tidak dapat langsung ditulis ke file. Serialisasi menggabungkan semua field menjadi satu urutan yang dapat ditulis, dikirim, atau disimpan.
Memory object Serialized bytes Reconstructed object
───────────────── ───────────────── ─────────────────────
Pengguna { 01 00 00 00 00 00 03 Pengguna {
id = 1001 ───► E9 00 0C 42 75 64 69 ──► id = 1001
nama = "Budi" 20 53 61 6E 74 6F 73 nama = "Budi"
} 6F ... }Tiga use case utama serialisasi: persistence (simpan ke database/file), network (kirim via HTTP/MQ), cache (simpan di Redis/Memcached). Setiap use case memiliki trade-off format—binary kompak tetapi tidak terbaca; text lebih besar tetapi dapat diperiksa dan di-debug.
Tiga Pendekatan di Kotlin #
Demo berikut menunjukkan tiga pendekatan serialisasi di Kotlin. Pertama, manual binary—paling kompak, kompatibel lintas platform. Kedua, Java Serializable—built-in JVM tetapi tidak dapat dijalankan di Kotlin/Native. Ketiga, text JSON—paling besar tetapi dapat dibaca dan debug.
// serialization_fundamentals.kt
// Konsep serialisasi: object <-> bytes. Tiga use case utama:
// persistence, network, cache. Plus skema evolusi (versioning).
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable as JavaSerializable
// --- Model data ---
data class Pengguna(
val id: Long,
val nama: String,
val email: String,
val aktif: Boolean = true
)
// --- Pendekatan 1: Manual binary serialization ---
// Cocok untuk format kompak kustom.
object ManualSerializer {
fun serialize(p: Pengguna): ByteArray {
val baos = ByteArrayOutputStream()
DataOutputStream(baos).use { out ->
out.writeByte(1) // format version
out.writeLong(p.id)
out.writeUTF(p.nama)
out.writeUTF(p.email)
out.writeBoolean(p.aktif)
}
return baos.toByteArray()
}
fun deserialize(bytes: ByteArray): Pengguna {
val bais = ByteArrayInputStream(bytes)
DataInputStream(bais).use { input ->
val version = input.readByte().toInt()
require(version == 1) { "versi tidak didukung: $version" }
val id = input.readLong()
val nama = input.readUTF()
val email = input.readUTF()
val aktif = input.readBoolean()
return Pengguna(id, nama, email, aktif)
}
}
fun ukuran(p: Pengguna): Int = serialize(p).size
}
// --- Pendekatan 2: Java Serializable (built-in JVM) ---
// Catatan: tidak dapat dijalankan di Kotlin/Native atau Kotlin/JS.
class JavaSerializablePengguna(
val id: Long,
val nama: String,
val email: String
) : JavaSerializable {
companion object {
private const val serialVersionUID: Long = 1L
}
}
object JavaObjectSerializer {
fun serialize(p: JavaSerializablePengguna): ByteArray {
val baos = ByteArrayOutputStream()
ObjectOutputStream(baos).use { it.writeObject(p) }
return baos.toByteArray()
}
@Suppress("UNCHECKED_CAST")
fun deserialize(bytes: ByteArray): JavaSerializablePengguna {
val bais = ByteArrayInputStream(bytes)
ObjectInputStream(bais).use { return it.readObject() as JavaSerializablePengguna }
}
}
// --- Pendekatan 3: JSON-like text serialization ---
// Kompatibel lintas platform; mudah dibaca.
object JsonTextSerializer {
fun serialize(p: Pengguna): String {
val sb = StringBuilder()
sb.append('{')
sb.append("\"id\":").append(p.id).append(',')
sb.append("\"nama\":\"").append(escape(p.nama)).append("\",")
sb.append("\"email\":\"").append(escape(p.email)).append("\",")
sb.append("\"aktif\":").append(p.aktif)
sb.append('}')
return sb.toString()
}
private fun escape(s: String): String =
s.replace("\\", "\\\\").replace("\"", "\\\"")
fun ukuran(p: Pengguna): Int = serialize(p).toByteArray(Charsets.UTF_8).size
}
// --- Demo ---
fun main() {
val pengguna = Pengguna(
id = 1001L,
nama = "Budi Santoso",
email = "[email protected]",
aktif = true
)
println("=== Serialization Fundamentals ===")
println()
println("Data asli : $pengguna")
println()
println("--- Pendekatan 1: Manual binary ---")
val bytes1 = ManualSerializer.serialize(pengguna)
println("Byte array : ${bytes1.size} bytes")
println("Hex : ${bytes1.joinToString(" ") { "%02x".format(it) }.take(60)}...")
val restored1 = ManualSerializer.deserialize(bytes1)
println("Restored : $restored1")
println("Identik : ${restored1 == pengguna}")
println()
println("--- Pendekatan 2: Java Serializable ---")
val javaObj = JavaSerializablePengguna(pengguna.id, pengguna.nama, pengguna.email)
val bytes2 = JavaObjectSerializer.serialize(javaObj)
println("Byte array : ${bytes2.size} bytes (lebih besar karena header stream)")
val restored2 = JavaObjectSerializer.deserialize(bytes2)
println("Restored : id=${restored2.id}, nama=${restored2.nama}, email=${restored2.email}")
println()
println("--- Pendekatan 3: Text (JSON-like) ---")
val jsonText = JsonTextSerializer.serialize(pengguna)
println("Text : $jsonText")
println("Byte size : ${JsonTextSerializer.ukuran(pengguna)} bytes")
println()
println("--- Perbandingan ukuran ---")
println("Manual binary : ${bytes1.size} bytes")
println("Java Serialize: ${bytes2.size} bytes (termasuk header + class descriptor)")
println("JSON text : ${JsonTextSerializer.ukuran(pengguna)} bytes")
println()
println("--- Skema evolusi: tambah field baru ---")
println("Versi 1: id, nama, email, aktif")
println("Versi 2: + tanggalDaftar (Long, millis sejak epoch)")
println("Strategi : tulis versi di byte pertama; baca di deserialize.")
println("Versi 1 : baca 4 field seperti di atas")
println("Versi 2 : baca 4 field + tanggalDaftar (jika ada)")
println("Backward : versi 2 dapat membaca data versi 1 (field baru diisi default)")
println("Forward : versi 1 dapat membaca data versi 2 (field baru diabaikan)")
}Output yang Dihasilkan:
=== Serialization Fundamentals ===
Data asli : Pengguna(id=1001, nama=Budi Santoso, [email protected], aktif=true)
--- Pendekatan 1: Manual binary ---
Byte array : 42 bytes
Hex : 01 00 00 00 00 00 00 03 e9 00 0c 42 75 64 69 20 53 61 6e 74 ...
Restored : Pengguna(id=1001, nama=Budi Santoso, [email protected], aktif=true)
Identik : true
--- Pendekatan 2: Java Serializable ---
Byte array : 133 bytes (lebih besar karena header stream)
Restored : id=1001, nama=Budi Santoso, [email protected]
--- Pendekatan 3: Text (JSON-like) ---
Text : {"id":1001,"nama":"Budi Santoso","email":"[email protected]","aktif":true}
Byte size : 73 bytes
--- Perbandingan ukuran ---
Manual binary : 42 bytes
Java Serialize: 133 bytes (termasuk header + class descriptor)
JSON text : 73 bytes
--- Skema evolusi: tambah field baru ---
Versi 1: id, nama, email, aktif
Versi 2: + tanggalDaftar (Long, millis sejak epoch)
Strategi : tulis versi di byte pertama; baca di deserialize.
Versi 1 : baca 4 field seperti di atas
Versi 2 : baca 4 field + tanggalDaftar (jika ada)
Backward : versi 2 dapat membaca data versi 1 (field baru diisi default)
Forward : versi 1 dapat membaca data versi 2 (field baru diabaikan)Mari kita bedah bagian kunci dari kode di atas:
data class Pengguna: model data dengan empat field—id,nama,email,aktif.data classotomatis menghasilkanequalssehingga kita dapat memverifikasi bahwarestored == original.ManualSerializer: pendekatan binary kustom.DataOutputStreammenulis setiap field dalam format biner—writeLonguntukid(8 byte),writeUTFuntukString(panjang + byte UTF-8),writeBooleanuntukaktif(1 byte). Byte pertama1adalah versi format—dipakai untuk skema evolusi.JavaSerializablePengguna: implementasijava.io.Serializable. JVM otomatis menangani serialisasi via reflection. Output lebih besar (133 byte) karena header stream dan class descriptor disertakan.JsonTextSerializer: pendekatan text. StringBuilder menggabungkan field menjadi JSON. Output 73 byte—lebih besar dari binary manual tetapi dapat dibaca dan diperiksa. Field string harus di-escape untuk karakter"dan\.- Perbandingan ukuran: manual binary 42 byte, Java Serializable 133 byte, JSON 73 byte. Binary paling efisien untuk persistence/cache; JSON paling mudah untuk network API.
- Verifikasi identitas:
restored1 == penggunamengembalikantrue. Ini membuktikan serialisasi + deserialisasi menghasilkan object yang sama persis—tidak ada data yang hilang.
Skema Evolusi #
Skema data hampir selalu berubah. Versi 1 model Pengguna punya empat field; versi 2 menambah tanggalDaftar: Long. Tanpa strategi evolusi, data lama tidak dapat dibaca oleh kode baru. Dua arah evolusi: backward compatibility (kode baru membaca data lama) dan forward compatibility (kode lama membaca data baru).
Strategi umum untuk backward compatibility adalah menambah field baru dengan default value. Saat deserialisasi data versi 1, field baru diisi default—mis. tanggalDaftar = 0L. Strategi untuk forward compatibility adalah mengabaikan field tidak dikenali. Saat versi 1 membaca data versi 2, field tanggalDaftar dilewati—kode lama tidak tahu field itu.
// Versi 1
data class Pengguna(val id: Long, val nama: String)
// Versi 2: tambah field dengan default value
data class Pengguna(
val id: Long,
val nama: String,
val tanggalDaftar: Long = 0L // backward compatible
)Use Case: Persistence, Network, Cache #
| Use case | Format umum | Alasan |
|---|---|---|
| Persistence (database/file) | Binary, JSON | Binary lebih kompak; JSON dapat dibaca manual |
| Network (HTTP, MQ) | JSON, ProtoBuf | JSON dapat diperiksa; ProtoBuf kompak + schema |
| Cache (Redis, Memcached) | Binary, JSON | Binary lebih cepat; JSON dapat diperiksa via redis-cli |
Pilihan format tergantung kebutuhan. JSON paling umum untuk REST API karena dapat dibaca browser dan tool HTTP. ProtoBuf paling efisien untuk RPC internal karena skema di-generate dari .proto file. Binary kustom paling hemat untuk cache lokal.
Referensi Lanjutan #
- kotlinx.serialization
— halaman GitHub
kotlinx.serializationsebagai referensi arsitektur serialisasi Kotlin
Penutup #
Serialisasi adalah jembatan antara object Kotlin dalam memori dan representasi yang dapat dipersistensi atau dikirim. Tiga pendekatan—binary manual, Java Serializable, text JSON—memiliki trade-off masing-masing. Skema evolusi memastikan data lama tetap dapat dibaca saat model berkembang. Chapter berikutnya akan masuk ke library kotlinx.serialization yang menyediakan serialisasi otomatis dengan anotasi @Serializable dan compiler plugin.