JavaRush /Blog Java /Random-MS /Serialisasi sebagaimana adanya. Bahagian 1
articles
Tahap

Serialisasi sebagaimana adanya. Bahagian 1

Diterbitkan dalam kumpulan
Pada pandangan pertama, penyiaran kelihatan seperti proses yang remeh. Sebenarnya, apa yang lebih mudah? Mengisytiharkan kelas untuk melaksanakan antara muka java.io.Serializable- dan itu sahaja. Anda boleh menyusun kelas tanpa masalah. Serialisasi sebagaimana adanya.  Bahagian 1 - 1Secara teorinya, ini adalah benar. Dalam amalan, terdapat banyak kehalusan. Ia berkaitan dengan prestasi, penyahserialisasian, kepada keselamatan kelas. Dan dengan banyak lagi aspek. Kehalusan sedemikian akan dibincangkan. Artikel ini boleh dibahagikan kepada bahagian berikut:
  • Kehalusan mekanisme
  • Mengapa ia diperlukan?Externalizable
  • Prestasi
  • tetapi sebaliknya
  • Keselamatan Data
  • Pensirian ObjekSingleton
Mari kita teruskan ke bahagian pertama -

Kehalusan mekanisme

Pertama sekali, soalan cepat. Berapa banyak cara yang ada untuk menjadikan objek boleh bersiri? Amalan menunjukkan bahawa lebih daripada 90% pembangun menjawab soalan ini dengan cara yang lebih kurang sama (sehingga perkataan) - hanya ada satu cara. Sementara itu, terdapat dua daripadanya. Tidak semua orang mengingati yang kedua, apatah lagi mengatakan apa-apa yang boleh difahami tentang ciri-cirinya. Jadi apakah kaedah ini? Semua orang ingat yang pertama. Ini adalah pelaksanaan yang telah disebutkan java.io.Serializabledan tidak memerlukan sebarang usaha. Kaedah kedua juga adalah pelaksanaan antara muka, tetapi yang berbeza: java.io.Externalizable. Tidak seperti java.io.Serializable, ia mengandungi dua kaedah yang perlu dilaksanakan - writeExternal(ObjectOutput)dan readExternal(ObjectInput). Kaedah ini mengandungi logik bersiri/deserialisasi. Komen.SerializableDalam perkara berikut , saya kadangkala akan merujuk kepada bersiri dengan pelaksanaan sebagai standard, dan pelaksanaan Externalizablesebagai lanjutan. Satu lagikomen. Saya sengaja tidak menyentuh sekarang mengenai pilihan kawalan siri standard seperti mentakrifkan readObjectdan writeObject, kerana Saya rasa kaedah ini agak tidak betul. Kaedah-kaedah ini tidak ditakrifkan dalam antara muka Serializabledan, sebenarnya, sesuai untuk mengatasi batasan dan menjadikan penyirian standard fleksibel. ExternalizableKaedah yang memberikan fleksibiliti telah dibina ke dalamnya sejak awal lagi . Jom tanya satu soalan lagi. Bagaimanakah siri standard sebenarnya berfungsi, menggunakan java.io.Serializable? Dan ia berfungsi melalui API Refleksi. Itu. kelas dihuraikan sebagai satu set medan, setiap satunya ditulis ke aliran keluaran. Saya fikir adalah jelas bahawa operasi ini tidak optimum dari segi prestasi. Kami akan mengetahui berapa banyaknya kemudian. Terdapat satu lagi perbezaan utama antara dua kaedah bersiri yang disebutkan. Iaitu, dalam mekanisme penyahserialisasian. Apabila digunakan, Serializablepenyahserikatan berlaku seperti ini: memori diperuntukkan untuk objek, selepas itu medannya diisi dengan nilai daripada aliran. Pembina objek tidak dipanggil. Di sini kita perlu mempertimbangkan keadaan ini secara berasingan. Okay, kelas kita boleh bersiri. Dan ibu bapanya? Pilihan sepenuhnya! Lebih-lebih lagi, jika anda mewarisi kelas daripada Object- ibu bapa pastinya TIDAK boleh bersiri. Dan walaupun Objectkita tidak tahu apa-apa tentang medan, ia mungkin wujud dalam kelas induk kita sendiri. Apa yang akan berlaku kepada mereka? Mereka tidak akan masuk ke aliran bersiri. Apakah nilai yang akan mereka ambil apabila penyahserialisasian? Mari 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;
        }
    }
}
Ia adalah telus - kami mempunyai kelas induk tidak boleh bersiri dan kelas anak boleh bersiri. Dan inilah yang berlaku:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
Iaitu, semasa penyahserikatan, pembina tanpa parameter kelas NON-serialization induk dipanggil . Dan jika tiada pembina sedemikian, ralat akan berlaku semasa penyahserialisasian. Pembina objek kanak-kanak, yang kami deserialisasi, tidak dipanggil, seperti yang dinyatakan di atas. Beginilah cara mekanisme standard bertindak apabila digunakan Serializable. Apabila menggunakannya, Externalizablekeadaannya berbeza. Pertama, pembina tanpa parameter dipanggil, dan kemudian kaedah readExternal dipanggil pada objek yang dibuat, yang sebenarnya membaca semua datanya. Oleh itu, mana-mana kelas yang melaksanakan antara muka Externalizable mesti mempunyai pembina awam tanpa parameter! Selain itu, kerana semua keturunan kelas sedemikian juga akan dipertimbangkan untuk melaksanakan antara muka Externalizable, mereka juga mesti mempunyai pembina tanpa parameter! Mari pergi lebih jauh. Terdapat pengubah suai medan seperti transient. Ini bermakna bahawa medan ini tidak boleh bersiri. Walau bagaimanapun, seperti yang anda sendiri fahami, arahan ini hanya memberi kesan kepada mekanisme siri standard. Apabila digunakan, Externalizabletiada siapa yang mengganggu untuk menyiri medan ini, serta menolaknya. Jika medan diisytiharkan sementara, maka apabila objek dinyahsiri, ia mengambil nilai lalai. Satu lagi perkara yang agak halus. Dengan bersiri standard, medan yang mempunyai pengubah suai statictidak bersiri. Sehubungan itu, selepas penyahserialisasian medan ini tidak mengubah nilainya. Sudah tentu, semasa pelaksanaan, Externalizabletiada siapa yang mengganggu untuk bersiri dan menyahsiri bidang ini, tetapi saya sangat mengesyorkan untuk tidak melakukan ini, kerana ini boleh membawa kepada ralat halus. Medan dengan pengubah suai finalbersiri seperti yang biasa. Dengan satu pengecualian - mereka tidak boleh dinyahsiri apabila menggunakan Externalizable. Kerana final-поляmereka mesti dimulakan dalam pembina, dan selepas itu adalah mustahil untuk menukar nilai medan ini dalam readExternal. Sehubungan itu, jika anda perlu mensirikan objek yang mempunyai final-field, anda hanya perlu menggunakan serialisasi standard. Satu lagi perkara yang ramai orang tidak tahu. Siri standard mengambil kira susunan medan diisytiharkan dalam kelas. Walau apa pun, ini berlaku dalam versi terdahulu; dalam JVM versi 1.6 pelaksanaan Oracle, pesanan tidak lagi penting, jenis dan nama medan adalah penting. Komposisi kaedah berkemungkinan besar mempengaruhi mekanisme standard, walaupun pada hakikatnya medan secara amnya mungkin tetap sama. Untuk mengelakkan ini, terdapat mekanisme berikut. Untuk setiap kelas yang melaksanakan antara muka Serializable, satu lagi medan ditambah pada peringkat penyusunan -private static final long serialVersionUID. Medan ini mengandungi pengecam versi unik bagi kelas bersiri. Ia dikira berdasarkan kandungan kelas - medan, perintah pengisytiharan mereka, kaedah, perintah pengisytiharan mereka. Sehubungan itu, dengan sebarang perubahan dalam kelas, medan ini akan mengubah nilainya. Medan ini ditulis kepada strim apabila kelas disiri. Ngomong-ngomong, ini mungkin satu-satunya kes yang saya ketahui apabila statica -field bersiri. Semasa penyahserikatan, nilai medan ini dibandingkan dengan nilai kelas dalam mesin maya. Jika nilai tidak sepadan, pengecualian seperti ini dilemparkan:
java.io.InvalidClassException: test.ser2.ChildExt;
    local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
                                   local class serialVersionUID = 1465687698753363969
Walau bagaimanapun, terdapat cara untuk, jika tidak memintas, kemudian menipu cek ini. Ini boleh berguna jika set medan kelas dan susunannya sudah ditentukan, tetapi kaedah kelas mungkin berubah. Dalam kes ini, siri tidak berisiko, tetapi mekanisme standard tidak akan membenarkan data dinyahsiri menggunakan kod bait kelas yang diubah suai. Tetapi, seperti yang saya katakan, dia boleh ditipu. Iaitu, tentukan medan dalam kelas secara manual private static final long serialVersionUID. Pada dasarnya, nilai bidang ini boleh menjadi apa-apa sahaja. Sesetengah orang lebih suka untuk menetapkannya sama dengan tarikh kod itu diubah suai. Ada juga yang menggunakan 1L. Untuk mendapatkan nilai standard (yang dikira secara dalaman), anda boleh menggunakan utiliti serialver yang disertakan dalam SDK. Setelah ditakrifkan dengan cara ini, nilai medan akan ditetapkan, oleh itu penyahserialisasian akan sentiasa dibenarkan. Lebih-lebih lagi, dalam versi 5.0, kira-kira perkara berikut muncul dalam dokumentasi: adalah sangat disyorkan bahawa semua kelas boleh bersiri mengisytiharkan medan ini secara eksplisit, kerana pengiraan lalai sangat sensitif terhadap butiran struktur kelas, yang mungkin berbeza-beza bergantung pada pelaksanaan pengkompil, dan dengan itu menyebabkan InvalidClassExceptionakibat yang tidak dijangka. penyahserialisasian. Adalah lebih baik untuk mengisytiharkan medan ini sebagai private, kerana ia merujuk semata-mata kepada kelas di mana ia diisytiharkan. Walaupun pengubahsuai tidak dinyatakan dalam spesifikasi. Sekarang mari kita pertimbangkan aspek ini. Katakan kita mempunyai struktur kelas ini:
public class A{
    public int iPublic;
    protected int iProtected;
    int iPackage;
    private int iPrivate;
}

public class B extends A implements Serializable{}
Dalam erti kata lain, kami mempunyai kelas yang diwarisi daripada ibu bapa yang tidak boleh bersiri. Adakah mungkin untuk menyusun kelas ini, dan apa yang diperlukan untuk ini? Apakah yang akan berlaku kepada pembolehubah kelas induk? Jawapannya begini. Ya, Banda boleh mensirikan contoh kelas. Apa yang diperlukan untuk ini? Tetapi kelas perlu Amempunyai pembina tanpa parameter, publicatau protected. Kemudian, semasa penyahserikatan, semua pembolehubah kelas Aakan dimulakan menggunakan pembina ini. Pembolehubah kelas Bakan dimulakan dengan nilai daripada aliran data bersiri. Secara teorinya, adalah mungkin untuk mentakrifkan dalam kelas Bkaedah yang saya bincangkan pada mulanya - readObjectdan writeObject, - pada permulaannya untuk melakukan (nyah-)sirialisasi pembolehubah kelas Bmelalui in.defaultReadObject/out.defaultWriteObject, dan kemudian (nyah-)sirialisasi pembolehubah yang tersedia dari kelas A(dalam kes kami ini adalah iPublic, iProtecteddan iPackage, jika Bia berada dalam pakej yang sama seperti A). Walau bagaimanapun, pada pendapat saya, lebih baik menggunakan serialisasi lanjutan untuk ini. Perkara seterusnya yang saya ingin sentuh ialah penyirian pelbagai objek. Katakan kita mempunyai 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 sebagaimana adanya.  Bahagian 1 - 2Apakah yang berlaku jika anda membuat siri contoh kelas A? Ia akan menyeret sepanjang contoh kelas B, yang, seterusnya, akan menyeret sepanjang contoh Cyang mempunyai rujukan kepada contoh A, yang sama dengan yang semuanya bermula. Lingkaran ganas dan rekursi tak terhingga? Nasib baik, tidak. Mari lihat kod ujian 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 buat? Kami mencipta contoh kelas A, Bdan C, memberi mereka pautan antara satu sama lain, dan kemudian bersiri setiap satu daripadanya. Kemudian kami menyahsiri mereka semula dan menjalankan satu siri semakan. Apa yang akan berlaku akibatnya:
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 boleh anda pelajari daripada ujian ini? Pertama. Rujukan objek selepas penyahserikatan adalah berbeza daripada rujukan sebelum itu. Dalam erti kata lain, semasa bersiri/deserialisasi objek telah disalin. Kaedah ini kadangkala digunakan untuk mengklon objek. Kesimpulan kedua adalah lebih penting. Apabila mensiri/menyahserialisasi berbilang objek yang mempunyai rujukan silang, rujukan tersebut kekal sah selepas penyahserikatan. Dalam erti kata lain, jika sebelum bersiri mereka menunjuk ke satu objek, maka selepas penyahserikatan mereka juga akan menunjuk ke satu objek. Satu lagi ujian kecil untuk mengesahkan 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 Bmempunyai rujukan kepada objek kelas C. Apabila bersiri, bia bersiri bersama-sama dengan contoh kelas С, selepas itu contoh c yang sama disiri tiga kali. Apa yang berlaku selepas penyahserikatan?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
Seperti yang anda boleh lihat, keempat-empat objek desiri sebenarnya mewakili satu objek - rujukan kepadanya adalah sama. Tepat seperti sebelum bersiri. Satu lagi perkara yang menarik - apakah yang akan berlaku jika kita melaksanakan secara serentak Externalizabledan Serializable? Seperti dalam soalan itu - gajah lawan ikan paus - siapa yang akan mengalahkan siapa? Akan mengatasi Externalizable. Mekanisme bersiri mula-mula menyemak kehadirannya, dan hanya kemudian untuk kehadirannya. SerializableJadi jika kelas B, yang melaksanakan Serializable, mewarisi daripada kelas A, yang melaksanakan Externalizable, medan kelas B tidak akan bersiri. Perkara terakhir ialah warisan. Apabila mewarisi daripada kelas yang melaksanakan Serializable, tiada tindakan tambahan perlu diambil. Siri ini akan dilanjutkan ke kelas kanak-kanak juga. Apabila mewarisi daripada kelas yang melaksanakan Externalizable, anda mesti mengatasi kaedah readExternal dan writeExternal kelas induk. Jika tidak, medan kelas kanak-kanak tidak akan bersiri. Dalam kes ini, anda harus ingat untuk memanggil kaedah induk, jika tidak, medan induk tidak akan bersiri. * * * Kami mungkin sudah selesai dengan butirannya. Namun, ada satu isu yang belum kita sentuh, bersifat global. Iaitu -

Mengapa anda memerlukan Externalizable?

Mengapa kita memerlukan siri lanjutan sama sekali? Jawapannya mudah sahaja. Pertama, ia memberikan lebih fleksibiliti. Kedua, ia selalunya boleh memberikan keuntungan yang ketara dari segi jumlah data bersiri. Ketiga, terdapat aspek seperti prestasi, yang akan kita bincangkan di bawah . Semuanya nampak jelas dengan fleksibiliti. Sememangnya, kita boleh mengawal proses bersiri dan penyahserikatan seperti yang kita mahu, yang menjadikan kita bebas daripada sebarang perubahan dalam kelas (seperti yang saya katakan di atas, perubahan dalam kelas boleh memberi kesan besar kepada penyahserikatan). Oleh itu, saya ingin mengatakan beberapa perkataan tentang keuntungan dalam jumlah. Katakan kita mempunyai kelas berikut:
public class DateAndTime{

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

}
Selebihnya tidak penting. Medan boleh dibuat daripada jenis int, tetapi ini hanya akan meningkatkan kesan contoh. Walaupun pada hakikatnya medan mungkin ditaip intatas sebab prestasi. Walau apa pun, perkara itu jelas. Kelas mewakili tarikh dan masa. Ia menarik bagi kami terutamanya dari sudut pandangan bersiri. Mungkin perkara paling mudah untuk dilakukan ialah menyimpan cap masa yang ringkas. Ia adalah jenis panjang, i.e. apabila bersiri ia akan mengambil masa 8 bait. Di samping itu, pendekatan ini memerlukan kaedah untuk menukar komponen kepada satu nilai dan kembali, i.e. - kehilangan produktiviti. Kelebihan pendekatan ini adalah tarikh gila yang boleh dimuatkan dalam 64 bit. Ini adalah margin keselamatan yang besar, selalunya tidak diperlukan dalam realiti. Kelas yang diberikan di atas akan mengambil 2 + 5*1 = 7 bait. Tambahan overhed untuk kelas dan 6 bidang. Adakah terdapat cara untuk memampatkan data ini? Yang pasti. Saat dan minit berada dalam julat 0-59, i.e. untuk mewakilinya, 6 bit sudah cukup dan bukannya 8. Jam – 0-23 (5 bit), hari – 0-30 (5 bit), bulan – 0-11 (4 bit). Jumlah, segala-galanya tanpa mengambil kira tahun - 26 bit. Masih ada 6 bit lagi untuk saiz int. Secara teorinya, dalam beberapa kes ini mungkin cukup untuk setahun. Jika tidak, menambah bait lain meningkatkan saiz medan data kepada 14 bit, yang memberikan julat 0-16383. Ini lebih daripada cukup dalam aplikasi sebenar. Secara keseluruhan, kami telah mengurangkan saiz data yang diperlukan untuk menyimpan maklumat yang diperlukan kepada 5 bait. Jika tidak sehingga 4. Kelemahannya adalah sama seperti dalam kes sebelumnya - jika anda menyimpan tarikh yang dibungkus, maka kaedah penukaran diperlukan. Tetapi saya mahu melakukannya dengan cara ini: simpan dalam medan berasingan dan sirikannya dalam bentuk berpakej. Di sinilah masuk akal untuk digunakan 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 sahaja. Selepas bersiri, kami mendapat overhed setiap kelas, dua medan (bukannya 6) dan 5 bait data. Yang sudah jauh lebih baik. Pembungkusan lanjut boleh diserahkan kepada perpustakaan khusus. Contoh yang diberikan sangat mudah. Tujuan utamanya ialah untuk menunjukkan cara penyiaran lanjutan boleh digunakan. Walaupun kemungkinan keuntungan dalam jumlah data bersiri jauh dari kelebihan utama, pada pendapat saya. Kelebihan utama, sebagai tambahan kepada fleksibiliti... (teruskan dengan lancar ke bahagian seterusnya...) Pautan ke sumber: Serialisasi sebagaimana adanya
Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION