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:

PendekatanCara KerjaKelebihanKekurangan
Serializable (marker interface)Reflection otomatis membaca semua field non-transientSederhana, satu baris deklarasiFormat tidak terkontrol, ukuran besar, rawan serangan deserialization
Externalizable (interface aktif)Programmer menulis writeExternal() dan readExternal() secara manualFormat compact, kontrol penuh, cepatLebih banyak kode, harus maintain format saat class berubah

Serializable dan Externalizable #

Mari kita bandingkan kedua pendekatan dalam satu program:

kotlin
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:

text
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 bytes

Mari kita bedah baris-baris penting di atas:

  1. data class PenggunaSerializable(...) : Serializable: cukup beri marker interface Serializable. Tidak ada method yang perlu diimplementasikan — JVM akan otomatis membaca semua property.
  2. class PenggunaExternalizable : Externalizable: programmer mengontrol format dengan menulis dua method: writeExternal() dan readExternal().
  3. ObjectOutputStream(...).use { it.writeObject(u1) }: ObjectOutputStream membungkus OutputStream dan menulis objek ke byte stream. .use { } memastikan stream ditutup otomatis.
  4. ObjectInputStream(...).use { it.readObject() as PenggunaSerializable }: membaca objek kembali. Wajib ada cast ke tipe yang diharapkan karena readObject() mengembalikan Any.
  5. 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:

kotlin
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.serialization JSON: data yang dikirim ke service eksternal, perlu human-readable, lintas bahasa.
  • kotlinx.serialization ProtoBuf: data internal dengan kinerja tinggi dan schema evolution.
  • Hindari Serializable untuk data dari sumber tidak terpercaya (lihat CVE-2015-4852 dan kerentanan serupa).

Best Practice #

  • Selalu deklarasikan serialVersionUID eksplisit.
  • Tandai field sensitif dengan @Transient agar tidak ikut diserialisasi.
  • Untuk aplikasi baru, gunakan kotlinx.serialization — bukan Serializable.
  • Validasi data setelah deserialization dengan @PostLoad (JPA) atau init block.

Referensi Lanjutan #

Selanjutnya #

Setelah memahami konsep dasar serialization, chapter berikutnya akan membahas JSON serialization dengan kotlinx.serialization — pendekatan modern yang direkomendasikan untuk kode produksi.