JavaRush /Java Blog /Random-ID /Serialisasi apa adanya. Bagian 1
articles
Level 15

Serialisasi apa adanya. Bagian 1

Dipublikasikan di grup Random-ID
Sekilas, serialisasi tampak seperti proses yang sepele. Sungguh, apa yang lebih sederhana? Mendeklarasikan kelas untuk mengimplementasikan antarmuka java.io.Serializable- dan selesai. Anda dapat membuat serial kelas tanpa masalah. Serialisasi apa adanya.  Bagian 1 - 1Secara teoritis, ini benar. Dalam praktiknya, ada banyak kehalusan. Hal tersebut terkait dengan kinerja, deserialisasi, hingga keamanan kelas. Dan dengan banyak aspek lainnya. Kehalusan seperti itu akan dibahas. Artikel ini dapat dibagi menjadi beberapa bagian berikut:
  • Seluk-beluk mekanisme
  • Mengapa itu diperlukan?Externalizable
  • Pertunjukan
  • tetapi di sisi lain
  • Keamanan data
  • Serialisasi ObjekSingleton
Mari kita beralih ke bagian pertama -

Seluk-beluk mekanisme

Pertama-tama, pertanyaan singkat. Berapa banyak cara yang ada untuk membuat suatu objek dapat diserialkan? Praktek menunjukkan bahwa lebih dari 90% pengembang menjawab pertanyaan ini dengan cara yang kira-kira sama (hingga kata-katanya) - hanya ada satu cara. Sementara itu, ada dua di antaranya. Tidak semua orang mengingat yang kedua, apalagi mengatakan sesuatu yang dapat dipahami tentang fitur-fiturnya. Jadi apa saja metode-metode tersebut? Semua orang ingat yang pertama. Ini adalah implementasi yang telah disebutkan java.io.Serializabledan tidak memerlukan usaha apa pun. Metode kedua juga merupakan implementasi antarmuka, tetapi berbeda: java.io.Externalizable. Berbeda dengan java.io.Serializable, ini berisi dua metode yang perlu diterapkan - writeExternal(ObjectOutput)dan readExternal(ObjectInput). Metode ini berisi logika serialisasi/deserialisasi. Komentar.SerializableBerikut ini , saya terkadang merujuk pada serialisasi dengan implementasi sebagai standar, dan implementasi Externalizablesebagai perluasan. Lainkomentar. Saya sengaja tidak menyentuh opsi kontrol serialisasi standar seperti mendefinisikan readObjectdan writeObject, karena Menurut saya cara-cara ini agak salah. Metode-metode ini tidak didefinisikan dalam antarmuka Serializabledan, pada kenyataannya, merupakan alat bantu untuk mengatasi keterbatasan dan membuat serialisasi standar menjadi fleksibel. ExternalizableMetode yang memberikan fleksibilitas sudah dibangun sejak awal . Mari kita ajukan satu pertanyaan lagi. Bagaimana cara kerja serialisasi standar menggunakan java.io.Serializable? Dan itu bekerja melalui Reflection API. Itu. kelas diurai sebagai sekumpulan bidang, yang masing-masing ditulis ke aliran keluaran. Saya kira jelas bahwa operasi ini tidak optimal dari segi kinerja. Kita akan mengetahui berapa tepatnya nanti. Ada perbedaan besar lainnya antara kedua metode serialisasi yang disebutkan. Yakni pada mekanisme deserialisasi. Saat digunakan, Serializabledeserialisasi terjadi seperti ini: memori dialokasikan untuk suatu objek, setelah itu bidangnya diisi dengan nilai dari aliran. Konstruktor objek tidak dipanggil. Di sini kita perlu mempertimbangkan situasi ini secara terpisah. Oke, kelas kita dapat diserialkan. Dan orang tuanya? Sepenuhnya opsional! Terlebih lagi, jika Anda mewarisi kelas dari Object- induknya pasti TIDAK dapat diserialkan. Dan meskipun Objectkita tidak tahu apa-apa tentang field, field tersebut mungkin ada di kelas induk kita. Apa yang akan terjadi pada mereka? Mereka tidak akan masuk ke aliran serialisasi. Nilai apa yang akan mereka ambil saat deserialisasi? Mari kita lihat contoh ini:
package ru.skipy.tests.io;

import java.io.*;

/**
 * ParentDeserializationTest
 *
 * @author Eugene Matyushkin aka Skipy
 * @since 05.08.2010
 */
public class ParentDeserializationTest {

    public static void main(String[] args){
        try {
            System.out.println("Creating...");
            Child c = new Child(1);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            c.field = 10;
            System.out.println("Serializing...");
            oos.writeObject(c);
            oos.flush();
            baos.flush();
            oos.close();
            baos.close();
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            System.out.println("Deserializing...");
            Child c1 = (Child)ois.readObject();
            System.out.println("c1.i="+c1.getI());
            System.out.println("c1.field="+c1.getField());
        } catch (IOException ex){
            ex.printStackTrace();
        } catch (ClassNotFoundException ex){
            ex.printStackTrace();
        }
    }

    public static class Parent {
        protected int field;
        protected Parent(){
            field = 5;
            System.out.println("Parent::Constructor");
        }
        public int getField() {
            return field;
        }
    }

    public static class Child extends Parent implements Serializable{
        protected int i;
        public Child(int i){
            this.i = i;
            System.out.println("Child::Constructor");
        }
        public int getI() {
            return i;
        }
    }
}
Ini transparan - kami memiliki kelas induk yang tidak dapat diserialkan dan kelas anak yang dapat diserialkan. Dan inilah yang terjadi:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
Artinya, selama deserialisasi, konstruktor tanpa parameter dari kelas induk NON-serializable disebut . Dan jika tidak ada konstruktor seperti itu, kesalahan akan terjadi selama deserialisasi. Konstruktor objek anak, yang kita deserialisasi, tidak dipanggil, seperti yang disebutkan di atas. Beginilah perilaku mekanisme standar saat digunakan Serializable. Saat menggunakannya, Externalizablesituasinya berbeda. Pertama, konstruktor tanpa parameter dipanggil, dan kemudian metode readExternal dipanggil pada objek yang dibuat, yang sebenarnya membaca semua datanya. Oleh karena itu, setiap kelas yang mengimplementasikan antarmuka Externalizable harus memiliki konstruktor publik tanpa parameter! Selain itu, karena semua turunan dari kelas tersebut juga akan dianggap mengimplementasikan antarmuka Externalizable, mereka juga harus memiliki konstruktor tanpa parameter! Ayo melangkah lebih jauh. Ada pengubah bidang seperti transient. Artinya, bidang ini tidak boleh diserialkan. Namun, seperti yang Anda pahami sendiri, instruksi ini hanya mempengaruhi mekanisme serialisasi standar. Saat digunakan, Externalizabletidak ada yang mau membuat serialisasi bidang ini, serta menguranginya. Jika suatu bidang dinyatakan sementara, maka ketika objek tersebut dideserialisasi, ia akan mengambil nilai default. Hal lain yang agak halus. Dengan serialisasi standar, bidang yang memiliki pengubah statictidak diserialisasi. Oleh karena itu, setelah deserialisasi bidang ini tidak mengubah nilainya. Tentu saja, selama implementasi, Externalizabletidak ada yang mau membuat serialisasi dan deserialisasi bidang ini, tetapi saya sangat menyarankan untuk tidak melakukan ini, karena ini dapat menyebabkan kesalahan halus. Bidang dengan pengubah finaldiserialkan seperti bidang biasa. Dengan satu pengecualian - mereka tidak dapat dideserialisasi saat menggunakan Externalizable. Karena final-поляmereka harus diinisialisasi di konstruktor, dan setelah itu tidak mungkin mengubah nilai bidang ini di readExternal. Oleh karena itu, jika Anda perlu membuat serialisasi objek yang memiliki final-field, Anda hanya perlu menggunakan serialisasi standar. Hal lain yang tidak diketahui banyak orang. Serialisasi standar memperhitungkan urutan bidang yang dideklarasikan dalam suatu kelas. Bagaimanapun, hal ini terjadi pada versi sebelumnya; dalam implementasi Oracle JVM versi 1.6, urutan tidak lagi penting, jenis dan nama bidang menjadi penting. Komposisi metode kemungkinan besar akan mempengaruhi mekanisme standar, meskipun faktanya secara umum bidangnya mungkin tetap sama. Untuk menghindarinya, ada mekanisme berikut. Untuk setiap kelas yang mengimplementasikan antarmuka Serializable, satu bidang lagi ditambahkan pada tahap kompilasi -private static final long serialVersionUID. Bidang ini berisi pengidentifikasi versi unik dari kelas serial. Itu dihitung berdasarkan isi bidang kelas, urutan deklarasinya, metode, urutan deklarasinya. Oleh karena itu, jika ada perubahan di kelas, bidang ini akan mengubah nilainya. Bidang ini ditulis ke aliran ketika kelas diserialkan. Ngomong-ngomong, ini mungkin satu-satunya kasus yang saya ketahui ketika static-field diserialkan. Selama deserialisasi, nilai bidang ini dibandingkan dengan nilai kelas di mesin virtual. Jika nilainya tidak cocok, pengecualian seperti ini akan muncul:
java.io.InvalidClassException: test.ser2.ChildExt;
    local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
                                   local class serialVersionUID = 1465687698753363969
Namun demikian, ada cara untuk, jika tidak mengabaikan, maka menipu pemeriksaan ini. Hal ini dapat berguna jika kumpulan bidang kelas dan urutannya sudah ditentukan, namun metode kelas dapat berubah. Dalam hal ini, serialisasi tidak berisiko, tetapi mekanisme standar tidak mengizinkan data dideserialisasi menggunakan bytecode dari kelas yang dimodifikasi. Tapi, seperti saya katakan, dia bisa ditipu. Yaitu, tentukan field di kelas secara manual private static final long serialVersionUID. Pada prinsipnya, nilai bidang ini bisa berupa apa saja. Beberapa orang lebih suka menyetelnya sama dengan tanggal kode diubah. Bahkan ada yang menggunakan 1L. Untuk mendapatkan nilai standar (nilai yang dihitung secara internal), Anda dapat menggunakan utilitas serialver yang disertakan dalam SDK. Setelah didefinisikan dengan cara ini, nilai bidang akan ditetapkan, sehingga deserialisasi akan selalu diperbolehkan. Selain itu, di versi 5.0, kira-kira yang berikut ini muncul di dokumentasi: sangat disarankan agar semua kelas yang dapat diserialkan mendeklarasikan bidang ini secara eksplisit, karena perhitungan default sangat sensitif terhadap detail struktur kelas, yang dapat bervariasi tergantung pada implementasi kompiler, dan dengan demikian menyebabkan InvalidClassExceptionkonsekuensi yang tidak terduga.deserialisasi. Lebih baik mendeklarasikan bidang ini sebagai private, karena itu hanya merujuk pada kelas di mana ia dideklarasikan. Meski pengubahnya tidak ditentukan dalam spesifikasi. Sekarang mari kita pertimbangkan aspek ini. Katakanlah kita memiliki struktur kelas ini:
public class A{
    public int iPublic;
    protected int iProtected;
    int iPackage;
    private int iPrivate;
}

public class B extends A implements Serializable{}
Dengan kata lain, kita memiliki kelas yang diwarisi dari induk yang tidak dapat diserialkan. Apakah mungkin untuk membuat serial kelas ini, dan apa yang diperlukan untuk ini? Apa yang akan terjadi pada variabel kelas induk? Jawabannya adalah ini. Ya, BAnda dapat membuat serial sebuah instance kelas. Apa yang dibutuhkan untuk ini? Tetapi kelas tersebut harus Amemiliki konstruktor tanpa parameter, publicatau protected. Kemudian, selama deserialisasi, semua variabel kelas Aakan diinisialisasi menggunakan konstruktor ini. Variabel kelas Bakan diinisialisasi dengan nilai dari aliran data serial. Secara teoritis, dimungkinkan untuk mendefinisikan di kelas Bmetode yang saya bicarakan di awal - readObjectdan writeObject, - yang pada awalnya melakukan (de-)serialisasi variabel kelas Bmelalui in.defaultReadObject/out.defaultWriteObject, dan kemudian (de-)serialisasi variabel yang tersedia dari kelas A(dalam kasus kami ini adalah iPublic, iProtecteddan iPackage, jika Bberada dalam paket yang sama dengan A). Namun menurut saya, lebih baik menggunakan serialisasi yang diperluas untuk ini. Poin berikutnya yang ingin saya sentuh adalah serialisasi beberapa objek. Katakanlah kita memiliki struktur kelas berikut:
public class A implements Serializable{
    private C c;
    private B b;
    public void setC(C c) {this.c = c;}
    public void setB(B b) {this.b = b;}
    public C getC() {return c;}
    public B getB() {return b;}
}
public class B implements Serializable{
    private C c;
    public void setC(C c) {this.c = c;}
    public C getC() {return c;}
}
public class C implements Serializable{
    private A a;
    private B b;
    public void setA(A a) {this.a = a;}
    public void setB(B b) {this.b = b;}
    public B getB() {return b;}
    public A getA() {return a;}
}
Serialisasi apa adanya.  Bagian 1 - 2Apa yang terjadi jika Anda membuat serial sebuah instance kelas A? Ini akan menyeret sebuah instance dari kelas tersebut B, yang, pada gilirannya, akan menyeret sebuah instance Cyang memiliki referensi ke instance tersebut A, yang sama dengan yang memulai semuanya. Lingkaran setan dan rekursi tanpa batas? Untungnya, tidak. Mari kita lihat kode pengujian berikut:
// initiaizing
A a = new A();
B b = new B();
C c = new C();
// setting references
a.setB(b);
a.setC(c);
b.setC(c);
c.setA(a);
c.setB(b);
// serializing
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
oos.writeObject(b);
oos.writeObject(c);
oos.flush();
oos.close();
// deserializing
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
A a1 = (A)ois.readObject();
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
// testing
System.out.println("a==a1: "+(a==a1));
System.out.println("b==b1: "+(b==b1));
System.out.println("c==c1: "+(c==c1));
System.out.println("a1.getB()==b1: "+(a1.getB()==b1));
System.out.println("a1.getC()==c1: "+(a1.getC()==c1));
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1.getA()==a1: "+(c1.getA()==a1));
System.out.println("c1.getB()==b1: "+(c1.getB()==b1));
Apa yang kita lakukan? Kami membuat sebuah instance dari kelas A, Bdan C, memberi mereka tautan satu sama lain, dan kemudian membuat serialisasi masing-masing kelas. Kemudian kami membatalkan serialisasinya kembali dan menjalankan serangkaian pemeriksaan. Apa yang akan terjadi sebagai hasilnya:
a==a1: false
b==b1: false
c==c1: false
a1.getB()==b1: true
a1.getC()==c1: true
b1.getC()==c1: true
c1.getA()==a1: true
c1.getB()==b1: true
Jadi, apa yang bisa Anda pelajari dari tes ini? Pertama. Referensi objek setelah deserialisasi berbeda dengan referensi sebelumnya. Dengan kata lain, selama serialisasi/deserialisasi objek disalin. Metode ini terkadang digunakan untuk mengkloning objek. Kesimpulan kedua lebih penting. Saat membuat serial/deserialisasi beberapa objek yang memiliki referensi silang, referensi tersebut tetap valid setelah deserialisasi. Dengan kata lain, jika sebelum serialisasi mereka menunjuk ke satu objek, maka setelah deserialisasi mereka juga akan menunjuk ke satu objek. Tes kecil lainnya untuk mengonfirmasi hal ini:
B b = new B();
C c = new C();
b.setC(c);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
oos.writeObject(c);
oos.writeObject(c);
oos.writeObject(c);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
C c2 = (C)ois.readObject();
C c3 = (C)ois.readObject();
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1==c2: "+(c1==c2));
System.out.println("c1==c3: "+(c1==c3));
Objek kelas Bmemiliki referensi ke objek kelas C. Ketika diserialkan, bia diserialkan bersama dengan turunan kelas С, setelah itu turunan c yang sama diserialisasikan sebanyak tiga kali. Apa yang terjadi setelah deserialisasi?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
Seperti yang Anda lihat, keempat objek yang dideserialisasi sebenarnya mewakili satu objek - referensi ke objek tersebut sama. Persis seperti sebelum serialisasi. Hal menarik lainnya - apa yang akan terjadi jika kita menerapkan Externalizabledan secara bersamaan Serializable? Seperti dalam pertanyaan itu – gajah versus paus – siapa yang akan mengalahkan siapa? Akan diatasi Externalizable. Mekanisme serialisasi pertama-tama memeriksa keberadaannya, dan baru kemudian keberadaannya. SerializableJadi jika kelas B, yang mengimplementasikan Serializable, mewarisi dari kelas A, yang mengimplementasikan Externalizable, maka bidang kelas B tidak akan diserialisasi. Poin terakhir adalah warisan. Saat mewarisi dari kelas yang mengimplementasikan Serializable, tidak ada tindakan tambahan yang perlu dilakukan. Serialisasi akan meluas ke kelas anak juga. Saat mewarisi dari kelas yang mengimplementasikan Externalizable, Anda harus mengganti metode readExternal dan writeExternal kelas induk. Jika tidak, bidang kelas anak tidak akan diserialkan. Dalam hal ini, Anda harus ingat untuk memanggil metode induk, jika tidak, bidang induk tidak akan diserialkan. * * * Kita mungkin sudah selesai dengan detailnya. Namun, ada satu isu yang belum kita bahas, yang bersifat global. Yaitu -

Mengapa Anda memerlukan Eksternalisasi?

Mengapa kita memerlukan serialisasi tingkat lanjut? Jawabannya sederhana. Pertama, ini memberikan lebih banyak fleksibilitas. Kedua, hal ini sering kali dapat memberikan keuntungan yang signifikan dalam hal volume data serial. Ketiga, ada aspek kinerja, yang akan kita bahas di bawah ini . Segalanya tampak jelas dengan fleksibilitas. Memang, kita dapat mengontrol proses serialisasi dan deserialisasi sesuai keinginan kita, yang membuat kita tidak bergantung pada perubahan apa pun di kelas (seperti yang saya katakan di atas, perubahan di kelas dapat sangat memengaruhi deserialisasi). Oleh karena itu, saya ingin menyampaikan beberapa patah kata tentang peningkatan volume. Katakanlah kita memiliki kelas berikut:
public class DateAndTime{

  private short year;
  private byte month;
  private byte day;
  private byte hours;
  private byte minutes;
  private byte seconds;

}
Sisanya tidak penting. Bidang dapat dibuat dengan tipe int, tetapi ini hanya akan meningkatkan efek contoh. Meskipun pada kenyataannya kolom tersebut mungkin diketik intkarena alasan kinerja. Bagaimanapun, maksudnya sudah jelas. Kelas mewakili tanggal dan waktu. Ini menarik bagi kami terutama dari sudut pandang serialisasi. Mungkin hal termudah untuk dilakukan adalah menyimpan stempel waktu sederhana. Ini bertipe panjang, yaitu. ketika diserialkan akan memakan waktu 8 byte. Selain itu, pendekatan ini memerlukan metode untuk mengubah komponen menjadi satu nilai dan sebaliknya, yaitu. – hilangnya produktivitas. Keuntungan dari pendekatan ini adalah tanggal yang benar-benar gila yang dapat ditampung dalam 64 bit. Ini adalah batas keamanan yang sangat besar, yang sering kali tidak diperlukan dalam kenyataan. Kelas yang diberikan di atas akan memakan waktu 2 + 5*1 = 7 byte. Ditambah overhead untuk kelas dan 6 bidang. Apakah ada cara untuk mengompresi data ini? Untuk ya. Detik dan menit berada pada kisaran 0-59, yaitu. untuk mewakilinya, 6 bit sudah cukup, bukan 8. Jam – 0-23 (5 bit), hari – 0-30 (5 bit), bulan – 0-11 (4 bit). Total, semuanya tanpa memperhitungkan tahun - 26 bit. Masih ada 6 bit tersisa untuk ukuran int. Secara teoritis, dalam beberapa kasus, hal ini mungkin cukup untuk satu tahun. Jika tidak, menambahkan byte lain akan meningkatkan ukuran bidang data menjadi 14 bit, yang memberikan kisaran 0-16383. Ini lebih dari cukup untuk aplikasi nyata. Secara total, kami telah mengurangi ukuran data yang diperlukan untuk menyimpan informasi yang diperlukan menjadi 5 byte. Jika tidak sampai 4. Kerugiannya sama seperti pada kasus sebelumnya - jika Anda menyimpan tanggal yang dikemas, maka diperlukan metode konversi. Tapi saya ingin melakukannya dengan cara ini: menyimpannya di bidang terpisah dan membuat cerita bersambung dalam bentuk paket. Di sinilah masuk akal untuk menggunakan Externalizable:
// data is packed into 5 bytes:
//  3         2         1
// 10987654321098765432109876543210
// hhhhhmmmmmmssssssdddddMMMMyyyyyy yyyyyyyy
public void writeExternal(ObjectOutput out){
    int packed = 0;
    packed += ((int)hours) << 27;
    packed += ((int)minutes) << 21;
    packed += ((int)seconds) << 15;
    packed += ((int)day) << 10;
    packed += ((int)month) << 6;
    packed += (((int)year) >> 8) & 0x3F;
    out.writeInt(packed);
    out.writeByte((byte)year);
}

public void readExternal(ObjectInput in){
    int packed = in.readInt();
    year = in.readByte() & 0xFF;
    year += (packed & 0x3F) << 8;
    month = (packed >> 6) & 0x0F;
    day = (packed >> 10) & 0x1F;
    seconds = (packed >> 15) & 0x3F;
    minutes = (packed >> 21) & 0x3F;
    hours = (packed >> 27);
}
Sebenarnya, itu saja. Setelah serialisasi, kita mendapatkan overhead per kelas, dua bidang (bukan 6) dan 5 byte data. Yang sudah jauh lebih baik. Pengemasan lebih lanjut dapat diserahkan ke perpustakaan khusus. Contoh yang diberikan sangat sederhana. Tujuan utamanya adalah untuk menunjukkan bagaimana serialisasi tingkat lanjut dapat digunakan. Meskipun kemungkinan peningkatan volume data serial jauh dari keuntungan utama, menurut saya. Keuntungan utama, selain fleksibilitas... (lancar beralih ke bagian berikutnya...) Tautan ke sumber: Serialisasi apa adanya
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION