Kadang data perlu disimpan apa adanya sebagai byte tanpa perantara teks, misalnya hasil pengukuran sensor atau kumpulan nilai numerik yang padat. Topik ini membahas dua tingkatan cara melakukannya. Keluarga DataOutputStream menyimpan nilai primitif satu per satu dengan ukuran yang pasti, sedangkan ObjectOutputStream menyimpan objek utuh lewat mekanisme serialisasi, yakni pengubahan objek menjadi deret byte yang dapat dibaca kembali menjadi objek.
Menulis nilai primitif ke berkas biner#
Kelas DataOutputStream menyediakan satu metode tulis untuk setiap tipe primitif, dan ukurannya tidak bergantung pada sistem. Sebuah int selalu menempati empat byte, double selalu delapan byte, sedangkan boolean hanya satu byte. Metode writeUTF menyimpan String dengan panjangnya terlebih dahulu, sehingga pembaca tahu berapa byte yang harus diambil. Berkas hasilnya tidak dapat dibaca editor teks karena isinya memang bukan teks, melainkan byte biner yang ditafsirkan lewat kode.
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class DemoDataBiner {
public static void main(String[] args) throws IOException {
try (DataOutputStream keluar = new DataOutputStream(
new FileOutputStream("hasil.bin"))) {
keluar.writeInt(42);
keluar.writeDouble(87.5);
keluar.writeBoolean(true);
keluar.writeUTF("Kuis Aljabar");
}
try (DataInputStream masuk = new DataInputStream(
new FileInputStream("hasil.bin"))) {
int nomor = masuk.readInt();
double nilai = masuk.readDouble();
boolean selesai = masuk.readBoolean();
String judul = masuk.readUTF();
System.out.println("nomor : " + nomor);
System.out.println("nilai : " + nilai);
System.out.println("selesai : " + selesai);
System.out.println("judul : " + judul);
}
}
}nomor : 42
nilai : 87.5
selesai : true
judul : Kuis AljabarUrutan pembacaan harus persis mengikuti urutan penulisan, karena berkas biner tidak membawa penanda tipe di antara nilainya. Program pembaca yang memanggil readDouble duluan akan menafsirkan byte milik int sebagai bagian dari double, dan hasilnya kacau tanpa peringatan. Menjalankan programnya dari terminal cukup dengan satu perintah berikut.
# berada di folder tempat DemoDataBiner.java disimpan
java DemoDataBiner.javanomor : 42
nilai : 87.5
selesai : true
judul : Kuis AljabarMenyimpan objek dengan ObjectOutputStream#
ObjectOutputStream melompat satu tingkat: objek utuh beserta seluruh field-nya ditulis lewat metode writeObject, dan ObjectInputStream membacanya kembali lewat readObject. Syaratnya hanya satu, yaitu kelas objeknya mengimplementasikan Serializable, sebuah marker interface yang tidak punya metode dan hanya menandai kelas tersebut boleh diserialisasi. Tabel berikut membandingkan kedua pendekatan pada topik ini.
| Aspek | DataOutputStream | ObjectOutputStream |
|---|---|---|
| Yang disimpan | nilai primitif satu per satu | objek utuh beserta field-nya |
| Bentuk berkas | deret byte yang diatur sendiri | format serialisasi Java |
| Syarat pada kelas | tidak ada | implements Serializable |
Program berikut menyimpan objek Pengaturan ke berkas pengaturan.dat. Salah satu fieldnya ditandai transient, yakni penanda bahwa field itu tidak ikut diserialisasi dan nilainya tidak disertakan ke dalam berkas.
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class DemoSimpanPengaturan {
public static void main(String[] args) throws IOException {
Pengaturan p = new Pengaturan();
p.namaPengguna = "budi";
p.modeGelap = true;
p.ukuranHuruf = 14;
p.tokenSesi = "abc-123";
try (ObjectOutputStream keluar = new ObjectOutputStream(
new FileOutputStream("pengaturan.dat"))) {
keluar.writeObject(p);
}
System.out.println("Objek Pengaturan tersimpan ke pengaturan.dat");
}
}
class Pengaturan implements Serializable {
private static final long serialVersionUID = 1L;
String namaPengguna;
boolean modeGelap;
int ukuranHuruf;
transient String tokenSesi;
}Objek Pengaturan tersimpan ke pengaturan.datProgram kedua membaca berkas yang baru dibuat. Metode readObject mengembalikan Object, sehingga hasilnya di-cast ke kelas asalnya, dan metode itu juga melempar ClassNotFoundException karena definisi kelas dimuat saat pembacaan berlangsung.
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class DemoBacaPengaturan {
public static void main(String[] args) throws IOException, ClassNotFoundException {
try (ObjectInputStream masuk = new ObjectInputStream(
new FileInputStream("pengaturan.dat"))) {
Pengaturan p = (Pengaturan) masuk.readObject();
System.out.println("namaPengguna : " + p.namaPengguna);
System.out.println("modeGelap : " + p.modeGelap);
System.out.println("ukuranHuruf : " + p.ukuranHuruf);
System.out.println("tokenSesi : " + p.tokenSesi);
}
}
}
class Pengaturan implements Serializable {
private static final long serialVersionUID = 1L;
String namaPengguna;
boolean modeGelap;
int ukuranHuruf;
transient String tokenSesi;
}namaPengguna : budi
modeGelap : true
ukuranHuruf : 14
tokenSesi : nullKedua program dijalankan berurutan, karena program kedua membaca berkas yang dibuat program pertama.
# program pertama membuat berkasnya, program kedua membacanya
java DemoSimpanPengaturan.java
java DemoBacaPengaturan.javaObjek Pengaturan tersimpan ke pengaturan.dat
namaPengguna : budi
modeGelap : true
ukuranHuruf : 14
tokenSesi : nullObjek yang kembali adalah salinan dengan isi field yang sama, sedangkan tokenSesi yang transient kembali sebagai null. Pola ini biasa dipakai untuk field yang dapat dibangun ulang, seperti data cache atau sesi masuk yang tidak perlu dipertahankan antar eksekusi. Alur penyimpanan dan pembacaannya digambarkan berikut ini.
serialVersionUID dan kelas yang berubah#
Deret byte hasil serialisasi menyimpan nama kelas beserta serialVersionUID, yaitu nomor versi kelas yang menemani datanya. Saat membaca, JVM membandingkan nomor versi di berkas dengan nomor versi kelas yang ada di kode, dan ketidakcocokannya melempar InvalidClassException. Bila nomor ini tidak dinyatakan eksplisit, JVM menghitungnya sendiri dari bentuk kelas, sehingga perubahan sekecil apa pun pada field mengubah nomornya dan berkas lama tak terbaca. Program berikut meniru kelas yang sudah berubah, dengan nomor versi 2, lalu mencoba membaca berkas dari versi 1.
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class DemoKelasBerubah {
public static void main(String[] args) {
try (ObjectInputStream masuk = new ObjectInputStream(
new FileInputStream("pengaturan.dat"))) {
Pengaturan p = (Pengaturan) masuk.readObject();
System.out.println("namaPengguna: " + p.namaPengguna);
} catch (InvalidClassException e) {
System.out.println("Kelas tidak cocok: " + e.getMessage());
} catch (IOException | ClassNotFoundException e) {
System.out.println("Gagal membaca: " + e.getMessage());
}
}
}
class Pengaturan implements Serializable {
private static final long serialVersionUID = 2L;
String namaPengguna;
boolean modeGelap;
int ukuranHuruf;
transient String tokenSesi;
}Kelas tidak cocok: Pengaturan; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2# simpan ulang dengan kelas versi 1, lalu baca dengan kelas versi 2
java DemoSimpanPengaturan.java
java DemoKelasBerubah.javaObjek Pengaturan tersimpan ke pengaturan.dat
Kelas tidak cocok: Pengaturan; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2Pesan errornya menyebut kedua nomor versi, yaitu 1 dari berkas dan 2 dari kelas lokal, sehingga penyebabnya langsung terlihat. Dengan serialVersionUID yang dinyatakan eksplisit, perubahan field yang kompatibel masih dapat dibaca, dan keputusan kapan data lama perlu dibuang berada di tangan pengembang, bukan menjadi kebetulan hasil perhitungan otomatis.
Batasan yang perlu diwaspadai#
Serialisasi bawaan menautkan berkas pada kode Java, baik nama kelas, isi field, maupun format internalnya. Berkas yang ditulis Java sulit dibaca bahasa lain tanpa alat tambahan, dan setiap perubahan kelas harus dijaga lewat nomor versinya, sehingga format ini cocok untuk berkas sementara dan data yang umurnya seumur aplikasinya. Untuk pertukaran data antar sistem yang terdiri atas bahasa berbeda, format teks seperti JSON lazim dipilih karena tidak menuntut definisi kelas di sisi pembaca.
Alasan yang lebih serius adalah keamanan. readObject tidak sekadar mengisi field, tetapi menjalankan kode yang melekat pada kelas yang dibacanya, dan deret byte dari sumber tak dikenal dapat dirancang untuk memicu eksekusi kode saat proses itu berjalan. Field yang bertipe objek ikut terserialisasi bersama induknya, sehingga satu panggilan writeObject dapat membawa seluruh graf objek tanpa terlihat. Data rahasia seperti kata sandi juga sebaiknya tidak diserialisasi sama sekali, sebab byte yang tersimpan dapat dibaca dan disalin apa pun tandanya.
Kesalahan umum#
- Melupakan
implements Serializablepada kelas yang disimpan. MetodewriteObjectmelemparNotSerializableExceptionbegitu bertemu objek yang tidak boleh diserialisasi. - Mengubah urutan
readIntdanreadDoublepadaDataInputStream. Berkas biner tidak membawa penanda tipe, sehingga nilai terbaca kacau tanpa pesan error. - Menyunting kelas tanpa menetapkan
serialVersionUID. Nomor versi yang dihitung otomatis ikut berubah, dan berkas lama gagal dibaca denganInvalidClassException. - Mendeserialisasi berkas yang asal-usulnya tidak jelas. Risiko keamanannya nyata, sehingga data yang tidak dipercaya ditolak di depan pintu.
Ringkasan#
DataOutputStreammenyimpan nilai primitif dengan ukuran pasti, dan urutan pembacaannya harus persis mengikuti urutan penulisannya.ObjectOutputStreammenulis objekSerializableutuh, sedangkanObjectInputStreammembacanya kembali dengan cast dan siap menampungClassNotFoundException.transientmenjaga field agar tidak ikut terserialisasi, dan nilainya kembali sebagai default.serialVersionUIDmenandai versi kelas, dan ketidakcocokan versi memunculkanInvalidClassException.- Serialisasi bawaan bukan format pertukaran data lintas bahasa, dan data tak terpercaya tidak boleh dideserialisasi.
Referensi#
- Java Object Serialization Specification — Oracle — spesifikasi resmi format serialisasi beserta aturan serialVersionUID.
- ObjectOutputStream (Java SE 21) — Oracle
— dokumentasi
writeObjectdan syaratSerializable. - Object Streams — Oracle — tutorial resmi serialisasi objek beserta contohnya.