Serialization adalah proses mengubah objek di memori menjadi urutan byte yang dapat disimpan ke file atau dikirim melalui jaringan. Deserialization adalah kebalikannya — byte dikembalikan menjadi objek. Kemampuan ini penting untuk caching, RPC, message queue, dan persistence. Kotlin mewarisi mekanisme serialization dari Java lewat marker interface java.io.Serializable, namun untuk kode produksi modern, lebih disarankan menggunakan kotlinx.serialization yang aman secara type dan lintas-platform.
Konsep Dasar Serialization #
Ada dua pendekatan serialization bawaan di JVM:
| Pendekatan | Cara Kerja | Kelebihan | Kekurangan |
|---|---|---|---|
Serializable (marker interface) | Reflection otomatis membaca semua field non-transient | Sederhana, satu baris deklarasi | Format tidak terkontrol, ukuran besar, rawan serangan deserialization |
Externalizable (interface aktif) | Programmer menulis writeExternal() dan readExternal() secara manual | Format compact, kontrol penuh, cepat | Lebih banyak kode, harus maintain format saat class berubah |
Serializable dan Externalizable #
Mari kita bandingkan kedua pendekatan dalam satu program:
import java.io.Externalizable
import java.io.ObjectInput
import java.io.ObjectInputStream
import java.io.ObjectOutput
import java.io.ObjectOutputStream
import java.io.Serializable
import java.nio.file.Files
// Serializable klasik (Java-style) — cukup beri marker interface
data class PenggunaSerializable(val nama: String, val umur: Int) : Serializable
// Externalizable — kontrol penuh atas format byte
class PenggunaExternalizable : Externalizable {
var nama: String = ""
var umur: Int = 0
constructor() // wajib ada constructor tanpa argumen
constructor(nama: String, umur: Int) : this() {
this.nama = nama
this.umur = umur
}
override fun writeExternal(out: ObjectOutput) {
out.writeUTF(nama)
out.writeInt(umur)
}
override fun readExternal(input: ObjectInput) {
nama = input.readUTF()
umur = input.readInt()
}
override fun toString(): String = "PenggunaExternalizable(nama='$nama', umur=$umur)"
}
fun main() {
// === Serializable ===
val u1 = PenggunaSerializable("Budi", 25)
val tmpSer = Files.createTempFile("ser-", ".dat")
ObjectOutputStream(Files.newOutputStream(tmpSer)).use { it.writeObject(u1) }
val loaded1 = ObjectInputStream(Files.newInputStream(tmpSer)).use { it.readObject() as PenggunaSerializable }
println("Serializable: asli=$u1, load=$loaded1, sama=${u1 == loaded1}")
// === Externalizable ===
val u2 = PenggunaExternalizable("Siti", 30)
val tmpExt = Files.createTempFile("ext-", ".dat")
ObjectOutputStream(Files.newOutputStream(tmpExt)).use { it.writeObject(u2) }
val loaded2 = ObjectInputStream(Files.newInputStream(tmpExt)).use { it.readObject() as PenggunaExternalizable }
println("Externalizable: asli=$u2, load=$loaded2")
// === Cek ukuran byte ===
println("Ukuran Serializable: ${Files.size(tmpSer)} bytes")
println("Ukuran Externalizable: ${Files.size(tmpExt)} bytes")
// Cleanup
Files.deleteIfExists(tmpSer)
Files.deleteIfExists(tmpExt)
}Output yang Dihasilkan:
Serializable: asli=PenggunaSerializable(nama=Budi, umur=25), load=PenggunaSerializable(nama=Budi, umur=25), sama=true
Externalizable: asli=PenggunaExternalizable(nama='Siti', umur=30), load=PenggunaExternalizable(nama='Siti', umur=30)
Ukuran Serializable: 87 bytes
Ukuran Externalizable: 56 bytesMari kita bedah baris-baris penting di atas:
data class PenggunaSerializable(...) : Serializable: cukup beri marker interfaceSerializable. Tidak ada method yang perlu diimplementasikan — JVM akan otomatis membaca semua property.class PenggunaExternalizable : Externalizable: programmer mengontrol format dengan menulis dua method:writeExternal()danreadExternal().ObjectOutputStream(...).use { it.writeObject(u1) }:ObjectOutputStreammembungkusOutputStreamdan menulis objek ke byte stream..use { }memastikan stream ditutup otomatis.ObjectInputStream(...).use { it.readObject() as PenggunaSerializable }: membaca objek kembali. Wajib ada cast ke tipe yang diharapkan karenareadObject()mengembalikanAny.- Perbedaan ukuran: Serializable menulis metadata kelas (nama package, nama kelas, serialVersionUID) untuk setiap objek, sedangkan Externalizable hanya menulis data eksplisit yang kita spesifikasikan.
serialVersionUID #
Setiap class Serializable sebaiknya mendeklarasikan serialVersionUID untuk mengontrol kompatibilitas:
data class Pengguna(val nama: String, val umur: Int) : Serializable {
companion object {
private const val serialVersionUID: Long = 1L
}
}Tanpa deklarasi ini, JVM menghitung UID berdasarkan struktur class. Perubahan kecil seperti menambah property dapat mengubah UID dan menyebabkan deserialization gagal.
Kapan Menggunakan Serializable vs Externalizable vs JSON #
Serializable: prototype cepat, data internal aplikasi, tidak ada kebutuhan kompatibilitas versi.Externalizable: data internal yang butuh compact dan cepat, format stabil dalam jangka panjang.kotlinx.serializationJSON: data yang dikirim ke service eksternal, perlu human-readable, lintas bahasa.kotlinx.serializationProtoBuf: data internal dengan kinerja tinggi dan schema evolution.- Hindari
Serializableuntuk data dari sumber tidak terpercaya (lihat CVE-2015-4852 dan kerentanan serupa).
Best Practice #
- Selalu deklarasikan
serialVersionUIDeksplisit. - Tandai field sensitif dengan
@Transientagar tidak ikut diserialisasi. - Untuk aplikasi baru, gunakan
kotlinx.serialization— bukanSerializable. - Validasi data setelah deserialization dengan
@PostLoad(JPA) atauinitblock.
Referensi Lanjutan #
- Kotlin stdlib kotlin.io
— paket
kotlin.iountuk input/output stream yang dipakai serialisasi - Java SE 21 java.io package
— ringkasan paket
java.ioJava SE 21 untukObjectInputStreamdanObjectOutputStream
Selanjutnya #
Setelah memahami konsep dasar serialization, chapter berikutnya akan membahas JSON serialization dengan kotlinx.serialization — pendekatan modern yang direkomendasikan untuk kode produksi.