Object di memori hidup hanya selama program berjalan; kadang kita ingin menyimpan kondisinya — sesi pengguna, hasil perhitungan panjang, struktur cache — dan memulihkannya di lain waktu tanpa memetakan ulang setiap field secara manual. Mekanisme serialization Java menjawab kebutuhan itu: ObjectOutputStream.writeObject() menjelajah seluruh object graph dan menuangkannya menjadi format biner baku, sedangkan ObjectInputStream.readObject() membangun ulang objek-objek tersebut dari byte yang sama. Pada Chapter ini kita praktikkan roundtrip lengkap, lalu memahami tiga pengendali penting: interface Serializable, modifier transient, dan serialVersionUID.

Cara Kerja writeObject dan readObject #

Serialization bekerja dengan refleksi. Ketika writeObject() menerima sebuah objek, JVM memeriksa class-nya: class harus mengimplementasikan interface penanda Serializable, jika tidak NotSerializableException dilempar. Untuk class yang lolos, JVM membaca seluruh field non-static non-transient — termasuk milik superclass yang serializable — lalu menulis metadata class (nama, serialVersionUID, daftar field) diikuti nilai field ke stream. Proses ini berulang rekursif untuk setiap objek yang dirujuk, dengan jaminan setiap objek hanya ditulis sekali: referensi berikutnya ditulis sebagai back reference, sehingga object graph berbagi referensi tetap utuh saat dipulihkan.

Class sederhana berikut menjadi bahan percobaan kita — perhatikan tiga penanda pentingnya:

java Pegawai.java
import java.io.Serializable;

public class Pegawai implements Serializable {
    private static final long serialVersionUID = 1L;

    private final String nama;
    private final int gajiPokok;
    private transient String tokenSementara;

    public Pegawai(String nama, int gajiPokok) {
        this.nama = nama;
        this.gajiPokok = gajiPokok;
    }

    public void setTokenSementara(String token) {
        this.tokenSementara = token;
    }

    public String nama() {
        return nama;
    }

    public int gajiPokok() {
        return gajiPokok;
    }

    public String tokenSementara() {
        return tokenSementara;
    }
}

Tiga baris di class tersebut mengendalikan seluruh perilaku serialization-nya. Interface Serializable tidak memuat method apa pun — class ini hanyalah marker yang memberi izin kepada JVM. Field serialVersionUID menjadi nomor versi yang dicocokkan antara penulis dan pembaca stream. Modifier transient pada tokenSementara menandai field yang sengaja dikecualikan — kredensial sementara, data sensitif, atau cache yang tidak berarti setelah program berhenti.

Sekarang kita simpan objek Pegawai ke berkas pegawai.ser, lalu membangunnya kembali dalam proses yang sama sebagai pembuktian:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class SimpanPulihkan {
    public static void main(String[] args)
            throws IOException, ClassNotFoundException {
        Path berkas = Path.of("pegawai.ser");

        Pegawai asli = new Pegawai("Budi", 7_500_000);
        asli.setTokenSementara("TOKEN-RAHASIA-123");

        try (ObjectOutputStream keluar = new ObjectOutputStream(
                Files.newOutputStream(berkas))) {
            keluar.writeObject(asli);
        }
        System.out.println("ukuran berkas : " + Files.size(berkas) + " byte");

        try (ObjectInputStream masuk = new ObjectInputStream(
                Files.newInputStream(berkas))) {
            Pegawai pulih = (Pegawai) masuk.readObject();
            System.out.println("nama          : " + pulih.nama());
            System.out.println("gaji pokok    : " + pulih.gajiPokok());
            System.out.println("token         : " + pulih.tokenSementara());
            System.out.println("objek sama    : " + (pulih == asli));
            System.out.println("isi sama      : " + (pulih.nama().equals(asli.nama())
                    && pulih.gajiPokok() == asli.gajiPokok()));
        }
    }
}

Output yang Dihasilkan:

text
ukuran berkas : 79 byte
nama          : Budi
gaji pokok    : 7500000
token         : null
objek sama    : false
isi sama      : true

Keluaran tersebut memuat empat fakta yang layak dibedah:

  1. Baris 12-13 (new Pegawai(...) + token): objek asli dibuat dengan token sementara terpasang, agar pengaruh transient terlihat saat dibaca kembali.
  2. Baris 15-18 (ObjectOutputStream di atas Files.newOutputStream): ObjectOutputStream adalah dekorator di atas OutputStream apa pun — berkas di sini, tetapi bisa juga ByteArrayOutputStream untuk menyimpan ke memori. writeObject(asli) menuangkan seluruh graph: metadata class, field nama dan gajiPokok, plus objek String yang dirujuk field nama.
  3. Baris 20 (Files.size): 79 byte untuk satu objek kecil — perhatikan bahwa mayoritas byte adalah metadata class, bukan data; serialization biner Java memang boros untuk objek kecil.
  4. Baris 22-31 (readObject + cast): nilai field nama dan gajiPokok pulih utuh, sedangkan tokenSementara bernilai null karena field transient tidak pernah ditulis. Perbandingan pulih == asli bernilai false — pembacaan selalu membuat objek baru — tetapi isi field terbukti sama, itulah arti roundtrip yang berhasil.

serialVersionUID dan Kompatibilitas Stream #

Setiap class serializable memiliki nomor versi. Bila programmer tidak mendeklarasikannya, compiler menghitung serialVersionUID dari struktur class — nama field, tipe, urutan — sehingga penambahan field atau perubahan tipe di versi class berikutnya menghasilkan nomor berbeda. Saat readObject() menemukan nomor pada stream yang tidak cocok dengan nomor class di JVM, exception InvalidClassException dilempar dan objek gagal dibangun. Mendeklarasikan serialVersionUID secara eksplisit seperti 1L pada Pegawai memutus kalkulasi otomatis itu: kita yang memutuskan kapan versi berubah, dan perubahan kecil seperti penambahan field baru tetap bisa membaca stream lama (field baru mendapat nilai default).

Mekanisme yang barusan kita buktikan ini tampak praktis — dan memang demikian untuk data lokal yang kita percaya. Namun format biner serialization punya sisi gelap keamanan yang menjelaskan mengapa banyak tim melarangnya untuk data dari pihak lain. Chapter berikutnya membahas risiko tersebut: bagaimana deserialisasi data tak terpercaya menjadi vektor serangan, kasus historis yang membuatnya menjadi perhatian industri, serta filter JEP 290 yang menjadi garis pertahanan modern.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

Pegawai.java

Unduh berkas
java
import java.io.Serializable;

public class Pegawai implements Serializable {
    private static final long serialVersionUID = 1L;

    private final String nama;
    private final int gajiPokok;
    private transient String tokenSementara;

    public Pegawai(String nama, int gajiPokok) {
        this.nama = nama;
        this.gajiPokok = gajiPokok;
    }

    public void setTokenSementara(String token) {
        this.tokenSementara = token;
    }

    public String nama() {
        return nama;
    }

    public int gajiPokok() {
        return gajiPokok;
    }

    public String tokenSementara() {
        return tokenSementara;
    }
}

SimpanPulihkan.java

Unduh berkas
java
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class SimpanPulihkan {
    public static void main(String[] args)
            throws IOException, ClassNotFoundException {
        Path berkas = Path.of("pegawai.ser");

        Pegawai asli = new Pegawai("Budi", 7_500_000);
        asli.setTokenSementara("TOKEN-RAHASIA-123");

        try (ObjectOutputStream keluar = new ObjectOutputStream(
                Files.newOutputStream(berkas))) {
            keluar.writeObject(asli);
        }
        System.out.println("ukuran berkas : " + Files.size(berkas) + " byte");

        try (ObjectInputStream masuk = new ObjectInputStream(
                Files.newInputStream(berkas))) {
            Pegawai pulih = (Pegawai) masuk.readObject();
            System.out.println("nama          : " + pulih.nama());
            System.out.println("gaji pokok    : " + pulih.gajiPokok());
            System.out.println("token         : " + pulih.tokenSementara());
            System.out.println("objek sama    : " + (pulih == asli));
            System.out.println("isi sama      : " + (pulih.nama().equals(asli.nama())
                    && pulih.gajiPokok() == asli.gajiPokok()));
        }
    }
}