Serialisasi di Jawa. Tidak sesederhana itu



Serialisasi adalah proses yang menerjemahkan suatu objek ke dalam urutan byte, yang kemudian dapat dipulihkan sepenuhnya. Mengapa ini dibutuhkan? Faktanya adalah, selama eksekusi program normal, umur maksimum objek apa pun diketahui - dari peluncuran program hingga akhirnya. Serialisasi memungkinkan Anda untuk memperluas kerangka kerja ini dan "memberi kehidupan" pada objek dengan cara yang sama antara peluncuran program.

Bonus tambahan untuk semuanya adalah pelestarian lintas platform. Apa pun sistem operasi yang Anda miliki, serialisasi menerjemahkan objek menjadi aliran byte, yang dapat dipulihkan pada OS apa pun. Jika Anda perlu mentransfer objek melalui jaringan, Anda bisa membuat serial objek, menyimpannya ke file dan mentransfernya melalui jaringan ke penerima. Dia akan dapat memulihkan objek yang diterima. Serialisasi juga memungkinkan Anda untuk secara jarak jauh memanggil metode (Java RMI) yang ada di mesin yang berbeda dengan, mungkin, sistem operasi yang berbeda, dan bekerja dengan mereka seolah-olah mereka berada di mesin proses panggilan java.

Menerapkan mekanisme serialisasi cukup sederhana. Kelas Anda perlu mengimplementasikan antarmuka Serializable . Antarmuka ini adalah pengidentifikasi yang tidak memiliki metode, tetapi memberitahu jvm bahwa objek dari kelas ini dapat diserialisasi. Karena mekanisme serialisasi terhubung ke sistem input / output dasar dan menerjemahkan objek ke dalam aliran byte, untuk menjalankannya Anda perlu membuat aliran output OutputStream , kemas dalam ObjectOutputStream dan panggil metode writeObject (). Untuk mengembalikan objek, Anda perlu mengemas InputStream ke dalam ObjectInputStream dan memanggil metode readObject ().

Selama proses serialisasi, bersama dengan objek serializable, grafik objeknya disimpan. Yaitu semua objek yang terkait dengan ini, objek dari kelas lain juga akan bersambung dengan itu.

Pertimbangkan contoh membuat cerita bersambung dari objek Person kelas.

import java.io.*; class Home implements Serializable { private String home; public Home(String home) { this.home = home; } public String getHome() { return home; } } public class Person implements Serializable { private String name; private int countOfNiva; private String fatherName; private Home home; public Person(String name, int countOfNiva, String fatherName, Home home) { this.name = name; this.countOfNiva = countOfNiva; this.fatherName = fatherName; this.home = home; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", countOfNiva=" + countOfNiva + ", fatherName='" + fatherName + '\'' + ", home=" + home + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Home home = new Home("Vishnevaia 1"); Person igor = new Person("Igor", 2, "Raphael", home); Person renat = new Person("Renat", 2, "Raphael", home); //      ObjectOutputStream ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("person.out")); objectOutputStream.writeObject(igor); objectOutputStream.writeObject(renat); objectOutputStream.close(); //       ObjectInputStream ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream("person.out")); Person igorRestored = (Person) objectInputStream.readObject(); Person renatRestored = (Person) objectInputStream.readObject(); objectInputStream.close(); //    ByteArrayOutputStream ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream2 = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream2.writeObject(igor); objectOutputStream2.writeObject(renat); objectOutputStream2.flush(); //    ByteArrayInputStream ObjectInputStream objectInputStream2 = new ObjectInputStream( new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); Person igorRestoredFromByte = (Person) objectInputStream2.readObject(); Person renatRestoredFromByte = (Person) objectInputStream2.readObject(); objectInputStream2.close(); System.out.println("Before Serialize: " + "\n" + igor + "\n" + renat); System.out.println("After Restored From Byte: " + "\n" + igorRestoredFromByte + "\n" + renatRestoredFromByte); System.out.println("After Restored: " + "\n" + igorRestored + "\n" + renatRestored); } } 

Kesimpulan:

 Before Serialize: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@355da254} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@355da254} After Restored From Byte: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} After Restored: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} 

Dalam contoh ini, kelas Home dibuat untuk menunjukkan bahwa ketika membuat serial objek objek, grafik objeknya di-serialisasi dengannya. Kelas Home juga harus mengimplementasikan antarmuka Serializable , jika tidak java.io.NotSerializableException akan terjadi. Contoh ini juga menjelaskan serialisasi menggunakan kelas ByteArrayOutputStream .

Kesimpulan yang menarik dapat diambil dari hasil eksekusi program: ketika mengembalikan objek yang memiliki referensi ke objek yang sama sebelum serialisasi, objek ini akan dikembalikan hanya sekali . Ini dapat dilihat pada tautan yang sama pada objek setelah pemulihan:

 After Restored From Byte: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} After Restored: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} 

Namun, juga terlihat bahwa ketika merekam dengan dua aliran keluaran (kami memiliki ObjectInputStream dan ByteArrayOutputStream ), objek rumah akan dibuat ulang, meskipun faktanya itu sudah dibuat sebelumnya di salah satu aliran. Kami melihat ini di berbagai alamat objek rumah yang diterima dalam dua aliran. Ternyata jika Anda membuat serial dengan satu aliran output, lalu mengembalikan objek, maka kami memiliki jaminan memulihkan jaringan objek secara lengkap tanpa duplikat yang tidak perlu. Tentu saja, selama eksekusi program keadaan objek dapat berubah, tetapi ini adalah hati nurani programmer.

Masalah

Contoh ini juga menunjukkan bahwa ketika memulihkan objek, ClassNotFoundException dapat terjadi. Apa alasannya? Faktanya adalah bahwa kita dapat dengan mudah membuat serial objek dari kelas Person ke file, mentransfernya melalui jaringan ke teman kita, yang dapat mengembalikan objek dengan aplikasi lain di mana kelas Person tidak ada.

Serialisasi. Bagaimana membuat?

Bagaimana jika Anda ingin mengelola serialisasi sendiri? Misalnya, objek Anda menyimpan nama pengguna dan kata sandi pengguna. Anda perlu membuat cerita bersambung untuk transmisi lebih lanjut melalui jaringan. Melewati kata sandi dalam hal ini sangat tidak bisa diandalkan. Bagaimana cara mengatasi masalah ini? Ada dua cara. Pertama, gunakan kata kunci sementara . Kedua, alih-alih mewujudkan minat Serializable , gunakan ekstensi - antarmuka yang dapat dieksternalisasi . Pertimbangkan contoh metode pertama dan kedua untuk membandingkannya.

Cara pertama - Serialisasi menggunakan transient

 import java.io.*; public class Logon implements Serializable { private String login; private transient String password; public Logon(String login, String password) { this.login = login; this.password = password; } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon igor = new Logon("IgorIvanovich", "Khoziain"); Logon renat = new Logon("Renat", "2500RUB"); System.out.println("Before: \n" + igor); System.out.println(renat); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out")); out.writeObject(igor); out.writeObject(renat); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out")); igor = (Logon) in.readObject(); renat = (Logon) in.readObject(); System.out.println("After: \n" + igor); System.out.println(renat); } } 

Kesimpulan:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} After: Logon{login='IgorIvanovich', password='null'} Logon{login='Renat', password='null'} 

Cara kedua - Serialisasi dengan implementasi antarmuka Externalizable

 import java.io.*; public class Logon implements Externalizable { private String login; private String password; public Logon() { } public Logon(String login, String password) { this.login = login; this.password = password; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(login); } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { login = (String) in.readObject(); } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon igor = new Logon("IgorIvanovich", "Khoziain"); Logon renat = new Logon("Renat", "2500RUB"); System.out.println("Before: \n" + igor); System.out.println(renat); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out")); out.writeObject(igor); out.writeObject(renat); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out")); igor = (Logon) in.readObject(); renat = (Logon) in.readObject(); System.out.println("After: \n" + igor); System.out.println(renat); } } 

Kesimpulan:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} After: Logon{login='IgorIvanovich', password='null'} Logon{login='Renat', password='null'} 

Perbedaan pertama antara dua opsi yang menarik perhatian Anda adalah ukuran kode. Saat mengimplementasikan antarmuka Externalizable , kita perlu mengganti dua metode: writeExternal () dan readExternal () . Dalam metode writeExternal () , kami menunjukkan bidang mana yang akan diserialisasi dan bagaimana, di readExternal () cara membacanya. Saat menggunakan kata transient, kami secara eksplisit menunjukkan bidang atau bidang mana yang tidak perlu diserialisasi. Kami juga mencatat bahwa dalam metode kedua, kami secara eksplisit membuat konstruktor default, apalagi yang publik. Mengapa ini dilakukan? Mari kita coba jalankan kode tanpa konstruktor ini. Dan lihat hasilnya:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} Exception in thread "main" java.io.InvalidClassException: Logon; no valid constructor at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169) at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2043) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431) at Logon.main(Logon.java:45) 

Kami mendapat pengecualian java.io.InvalidClassException . Apa alasannya? Jika Anda mengikuti jejak stack, Anda bisa menemukan bahwa ada garis-garis di konstruktor dari kelas ObjectStreamClass :

  if (externalizable) { cons = getExternalizableConstructor(cl); } else { cons = getSerializableConstructor(cl); 

Untuk antarmuka Externalizable , metode konstruktor getExternalizableConstructor () akan dipanggil , di dalamnya melalui Reflection kita akan mencoba untuk mendapatkan konstruktor default dari kelas yang kita pulihkan objeknya. Jika kami tidak dapat menemukannya, atau tidak tersedia untuk umum , maka kami mendapatkan pengecualian. Anda dapat mengatasi situasi ini sebagai berikut: jangan membuat konstruktor secara eksplisit di kelas dan mengisi bidang menggunakan setter dan dapatkan nilai dengan getter. Kemudian, ketika mengkompilasi kelas, konstruktor default akan dibuat, yang akan tersedia untuk getExternalizableConstructor () . Untuk Serializable, metode getSerializableConstructor () mendapatkan konstruktor dari kelas Object dan mencari kelas yang diinginkan darinya, jika tidak menemukannya, kita akan mendapatkan ClassNotFoundException . Ternyata perbedaan utama antara Serializable dan Externalizable adalah bahwa yang pertama tidak memerlukan konstruktor untuk membuat pemulihan objek. Ini hanya akan pulih sepenuhnya dari byte. Untuk yang kedua, selama restorasi, sebuah objek pertama akan dibuat menggunakan konstruktor pada titik deklarasi, dan kemudian nilai bidangnya dari byte yang diterima selama serialisasi akan ditulis ke dalamnya. Secara pribadi, saya lebih suka metode pertama, itu jauh lebih sederhana. Selain itu, bahkan jika kita masih perlu mengatur perilaku serialisasi, kita tidak dapat menggunakan Externalizable , serta mengimplementasikan Serializable dengan menambahkan (tanpa mengesampingkan) metode writeObject () dan readObject () ke dalamnya. Tetapi agar mereka β€œbekerja”, tanda tangan mereka harus diperhatikan dengan ketat.

 import java.io.*; public class Talda implements Serializable { private String name; private String description; public Talda(String name, String description) { this.name = name; this.description = description; } private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); System.out.println("Our writeObject"); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); System.out.println("Our readObject"); } @Override public String toString() { return "Talda{" + "name='" + name + '\'' + ", description='" + description + '\'' + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Talda partizanka = new Talda("Partizanka", "Viiiski"); System.out.println("Before: \n" + partizanka); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Talda.out")); out.writeObject(partizanka); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Talda.out")); partizanka = (Talda) in.readObject(); System.out.println("After: \n" + partizanka); } } 

Kesimpulan:

 Before: Talda{name='Partizanka', description='Viiiski'} Our writeObject Our readObject After: Talda{name='Partizanka', description='Viiiski'} 

Di dalam metode kami yang ditambahkan, defaultWriteObject () dan defaultReadObject () dipanggil. Mereka bertanggung jawab atas serialisasi default, seolah-olah itu berfungsi tanpa metode yang kami tambahkan.

Sebenarnya, ini hanya puncak gunung es, jika Anda terus mempelajari mekanisme serialisasi, maka dengan tingkat probabilitas yang tinggi, Anda dapat menemukan lebih banyak nuansa, menemukan yang kami katakan: "Serialisasi ... tidak begitu sederhana."

Source: https://habr.com/ru/post/id431524/


All Articles