Сериализация в java сериализация на обекти въведение



Дата29.01.2017
Размер348.55 Kb.
#13792
Сериализация в JAVA
Сериализация на обекти - въведение

Java 1.1 е добавил интересна черта, наречена сериализация на обекти която поз­волява всеки обект който реализира Serializable интерфейса да се превърне в последователност от байтове, от която после може напълно да се възстанови ори­гиналния обект. Това важи и за предаване на обекти по мрежата, което означава, че ме­ха­низ­мът на сериализация компенсира автоматично разликите в операционните системи. Тоест може да създадете обект на Windows машина, да го сериализирате, да го изпратите през мрежата на Unix машина където той ще бъде коректно реконструиран. Не е необходимо да се безпокоите за типовете на данните на двете системи, под­реж­да­нето на байтовете или други детайли.

Сама по себе си сериализацията на обекти е интересна с това, че позволява да се имплементира персистентност в лека форма. Запомнете, че прсистентноста означава, че продължителноста на съществуване на обекта не се определя от това дали програмата се изпълнява, т.е. обекта съществува и между две извиквания на програмата. Като вземете сериализируем обект, запишете го на диска, след което възстановите този обект, може да постигнете ефекта на персистентност. Причината тя да се нарича “лека” е че не може просто да я посочите чрез някаква ключова ду­ма като “persistent” и да оставите системата да се грижи за детайлите (макар че това може би ще стане в бъдеще). Вместо това трябва явно да се сериализира и де-сериализира обектът в програмата.

Сериализацията на обекти в Java позволява да вземем обект, който имплементира интерфейса Serializable, и да го превърнем в последователност от байтове, които по-късно могат да бъдат възстановени до изходния обект.

Сериализацията на обекти има за цел да представи две основни възможности:


  • Отдалечено извикване на методи (RMI) позволява на обекти, съществуващи на друга машина да имат поведение като обектите, съществуващи на вашата машина. Когато изпращаме съобщения на отдалечени обекти сериализацията е необходима за преноса на аргументите и връщаните стойности

  • Когато се използват JavaBeans, информацията за състоянието на един Bean трябва да бъде съхранена и по-късно възстановена при стартирането на програмата

Когато сериализираме обект трябва да го запишем в някакъв поток – напр. към файл, “канал” или друг компютър по мрежата.

При обратният процес – възстановяването му, трябва да го прочетем от поток – напр. от файл.

За да сериализираме обект трябва да създадем обект от класа OutputStream и да го “обвием” в ObjectOutputStream, т.е. да предадем като параметър на конструктора на класа ObjectOutputStream обекта от класа OutputStream. В този момент трябва само да извикаме метода writeObject() и обекта ще бъде сериализиран и изпратен към потока. При обратния процес обвиваме InputStream в ObjectInputStream и извикваме метода readObject(). Като резултат този метод връща референция към обект от класа Object и ще трябва да преобразуваме явно до класа, от който е нашият обект.

При сериализацията не сме ограничени да записваме само обекти в потока, можем да записваме също и базови типове с функции writeInt(), writeFloat(), writeDouble(). Няма ограничение и за броя и/или типа на обектите (защото всъщност записваме обекти от тип Object), които записваме в потока – в такива случай важно е само когато ги възстановяваме обратно да ги прочетем в същия ред (и да направим нужните преобразувания, защото при четене получаваме обекти от класа Object).

Полезен аспект на сериализацията е, че тя не само запазва изображение на обекта, но следва и всички референции, съдържащи се в този обект, и запазва и тези обекти, и следва всички референции във всеки от тези обекти и т.н. Следващият Пример 2 демонстрира механизма на сериализация като създава два обекта от различни класове като единия от тях съдържа референции към други обекти, и ги сериализира.
package streamtest;

import java.util.*;

import java.io.*;
/*

ПРИМЕР 2 - сериализация на обекти

*/
// 1. Създаване на класове, обекти от които ще бъдат сериализирани

class Boss implements Serializable

{

String bossName;



Vector emp;

Boss(String bname, Vector emp)

{

bossName = bname;



this.emp = emp;

}

}


class Employee implements Serializable

{

int yearExperience;

String personName;

Employee(String pname, int years)

{

yearExperience = years;



personName = pname;

}

}


public class SerializeTest

{

public static void main(String[] args) throws FileNotFoundException, ClassNotFoundException, IOException

{

// 2. Инициализация на обектите, които ще бъдат сериализирани



Vector empls = new Vector();

empls.add(new Employee("Стефан Димитров", 3));

empls.add(new Employee("Иван Петров", 12));

empls.add(new Employee("Илия Стоев", 7));


Boss boss = new Boss("Димитър Петков", empls);

ObjectOutputStream out = null; // изходен поток

ObjectInputStream in = null; // входящ поток
// 3. Записване на обектите в изходния поток - към файл

System.out.print("Записване на обектите в изходния поток... ");



try

{

FileOutputStream outFile = new FileOutputStream("d:/bossInfo.out");



out = new ObjectOutputStream(outFile);

out.writeObject(boss);

out.writeObject(new Employee("Павел Стоянов", 17));

out.writeInt(313);

out.flush();

}

catch(IOException e)

{

e.printStackTrace();



}

finally

{

out.close();



System.out.println("край.");

}
// 4. Прочитане на обекти от входен поток - от файл

System.out.println("Прочитане на обектите от входния поток: ");

try

{

FileInputStream inFile = new FileInputStream("d:/bossInfo.out");



in = new ObjectInputStream(inFile);
Object obj1 = in.readObject();

Boss restoredBoss = (Boss)obj1;

System.out.println("Име на мениджър: " + restoredBoss.bossName);

System.out.println("Брой служители: " + restoredBoss.emp.size());

System.out.println("------------");
Enumeration en = restoredBoss.emp.elements();

while(en.hasMoreElements())

{

Employee emp = (Employee)en.nextElement();



System.out.println("Име на служителя: " + emp.personName);

System.out.println("Трудов стаж: " + emp.yearExperience);

System.out.println("------------");

}
Object obj2 = in.readObject();

Employee emp = (Employee)obj2;

System.out.println("\nВъншен консултант: " + emp.personName);


int obj3 = in.readInt();

System.out.println("\nПрост тип: " + obj3);

}

catch(IOException ex)

{

ex.printStackTrace();



}

catch(ClassNotFoundException ex)

{

ex.printStackTrace();



}

finally

{

in.close();



}

}

}



Следва описание на примера.

  1. Създаване на класовете

Дефинираме два класа, обекти от които ще сериализираме. Единственото нещо, на което тук си струва да се обърне внимание е, че за да сериализираме обект класът, на който е инстанция, трябва да имплементира интерфейса Serializable. Този интерфейс е просто флаг и не притежава никакви методи. И двата ни класа имплементират този интерфейс.

Класът Boss има атрибут Vector, в който ще бъдат записани референции към обекти от класа Employee. Чрез този атрибут ще демонстрираме как при сериализацията на този обект ще бъдат сериализирани и всички обекти, към които той има референции и после ще могат да бъдат възстановени.

Класът Employee е съвсем прост, обект от него ще запишем след обекта от класа Boss, за да покажем, че в потока можем да записваме обекти от различни класове.


  1. Инициализация на обектите

Тук създаваме 3 обекта от класа Employee и ги добавяме в обект от класа Vector, който ще предадем като параметър на конструктора на класа Boss, за да бъде инициализиран неговият атрибут Vector. Декларираме двата обекта, които ще бъдат съответно изходен и входен поток.

  1. Записване на обектите в изходния поток - към файл

За целта създаваме обект от класа FileOutputStream, който ще ни служи за изходен поток към файл. След това чрез този създаден обект създаваме обект от класа ObjectOutputStream, чиято цел ще бъде да сериализира обектите и да ги записва в изходния поток – чрез методите writeXXX(param). Записваме в изходния поток обекта от класа Boss (при записване на обект от какъв да е клас става неявно преобразуване до базовия клас Object), обект от класа Employee и число от тип int. Методът flush() записва всички буферирани данни, ако има такива, и изчиства данните в буферите. В частта finally извикваме метода close(), който затваря потока и освобождава заетите ресурси.

  1. Прочитане на обекти от входен поток - от файл

За целта създаваме обект от класа FileInputStream, който ще ни служи за входен поток от файл. След това чрез този създаден обект създаваме обект от класа ObjectInputStream, чиято цел ще бъде да чете обектите от входния поток и да ги възстановява – чрез метода readXXX(). При възстановяване на обектите трябва да се съобразим с реда, по който са били записани обектите – когато ги прочитаме обектите ще бъдат подредени в реда на записването им. Това е важно, защото трябва да извършим съответните преобразувания, тъй като обектите, които прочитаме от потока са от клас Object. Първият обект, който прочитаме и записваме в обекта obj1 ще е този, който сме записали пръв, т.е. от класа Boss, вторият – от Employee, и третият – цяло число. След възстановяването на обекта от класа Boss извеждаме информация от референциите на обектите, които бяха записани в атрибута му от клас Vector, за да се уверим, че тези обекти също са били сериализирани и сега могат да бъдат възстановени. След това извеждаме информация от втория и третия обект (който прочитаме като цяло число, защото знаем, че такова сме записали преди това).

В частта finally извикваме метода close(), който затваря потока и освобождава заетите ресурси.



Забележки:

Ако се опитаме да прочетем повече обекти, отколкото има в потока, това води до java.io.EOFException изключение.

Ако не знаем колко обекта има в потока (но поне знаем от кой клас са) можем да ги четем в цикъл като проверяваме след всяко прочитане дали има още байтове за четене в потока – чрез метода available() на обекта от класа ObjectInputStream, който връща като цяло число броя байтове, които могат да бъдат прочетени без разделяне на блокове, останали в потока. Друг вариант е в try-catch блок да четем обектите в безкраен цикъл като при настъпване на изключение от класа java.io.EOFException ще знаем, че сме прочели всички обекти от потока – този подход е по-удачен, защото методът available() връща брой байтове само ако следва елемент от прост тип (int, double, …) в потока.

Намиране на класа

Може би се чудите какво ли ще е необходимо за възстановяването на обект от не­говото сериализирано състояние. Да кажем например че сте сериализирали обект и сте го изпратили през мрежа на друга машина. Би ли могла програма на да­­лечната машина да реконструира обекта използвайки само данните от файла?

Най-добрият начин да се отговори на този въпрос е (както обикновено) чрез експе­римент. Следният файл отива в поддиректорията за тази глава:

//: c10:Alien.java

// A serializable class

import java.io.*;
public class Alien implements Serializable {

} ///:~


Файлът който създава и сериализира Alien обект отива в същата директория:

//: c10:FreezeAlien.java

// Create a serialized output file

import java.io.*;


public class FreezeAlien {

public static void main(String[] args)

throws Exception {

ObjectOutput out =

new ObjectOutputStream(

new FileOutputStream("file.x"));

Alien zorcon = new Alien();

out.writeObject(zorcon);

}

} ///:~


Наместо да хваща и обработва изключения, тази програма възприема бързия подход да изхвърля изключенията извън main( ), така че за тях се съобщава на командния ред (т.е. от ОС-б.пр.).

Щом програмата се компилира и пусне, копирайте получения файл file.x в под­ди­ректория наречена xfiles, където отива и следния код:

//: c10:xfiles:ThawAlien.java

// Try to recover a serialized file without the

// class of object that's stored in that file.

package c10.xfiles;

import java.io.*;
public class ThawAlien {

public static void main(String[] args)

throws Exception {

ObjectInputStream in =

new ObjectInputStream(

new FileInputStream("file.x"));

Object mystery = in.readObject();

System.out.println(

mystery.getClass().toString());

}

} ///:~



Тази програма отваря файла и чете обекта mystery успешно. Ако обаче се опи­та­те да намерите нещо за обекта – което изисква Class обектът за Alien –вир­ту­ал­ната машина (JVM) не може да намери Alien.class (докато той не се случи на Classpath, където не бива да бъде в този пример). Получавате ClassNotFoundException. (За пореден път всякакви свидетелства за живота на alien (извън­зем­ни-б.пр.) изчезват преди да бъдат проверени!)

Ако ще правите нещо с обект който е реконструиран, трябва да осигурите че JVM може да намери съответния .class файл или локално на пътя за класовете или някъде в Internet.


Управление на сериализацията


Както се вижда, нормалният сериализационен механизъм е тривиален за из­полз­ване. А ако има специални нужди? Може би имате специални изисквания към сигурността и не искате да сериализирате някои части от вашия обект, а мо­же би просто няма смисъл някакъв подобект да се сериализира и изпраща по­не­же при използване ще се генерира отново.

Може да управлявате процеса на сериализация чрез използване на Externalizable интерфейса вместо Serializable интерфейса. Интерфейсът Externalizable разширява Serializable интерфейса и добавя два метода, writeExternal( ) и readExternal( ), които автоматично се викат по време на се­риа­лизацията и обратния процес (съответно-б.пр.) и позволяват да се направят спе­циалните неща.

Следният пример показва прости реализации на интерфейсните методи на Externalizable. Забележете че Blip1 и Blip2 са почти идентични с изключение на малка разлика (вижте дали ще я откриете като четете кода):

//: c10:Blips.java

// Simple use of Externalizable & a pitfall

import java.io.*;

import java.util.*;
class Blip1 implements Externalizable {

public Blip1() {

System.out.println("Blip1 Constructor");

}

public void writeExternal(ObjectOutput out)



throws IOException {

System.out.println("Blip1.writeExternal");

}

public void readExternal(ObjectInput in)



throws IOException, ClassNotFoundException {

System.out.println("Blip1.readExternal");

}

}
class Blip2 implements Externalizable {



Blip2() {

System.out.println("Blip2 Constructor");

}

public void writeExternal(ObjectOutput out)



throws IOException {

System.out.println("Blip2.writeExternal");

}

public void readExternal(ObjectInput in)



throws IOException, ClassNotFoundException {

System.out.println("Blip2.readExternal");

}

}
public class Blips {



public static void main(String[] args) {

System.out.println("Constructing objects:");

Blip1 b1 = new Blip1();

Blip2 b2 = new Blip2();

try {

ObjectOutputStream o =



new ObjectOutputStream(

new FileOutputStream("Blips.out"));

System.out.println("Saving objects:");

o.writeObject(b1);

o.writeObject(b2);

o.close();

// Now get them back:

ObjectInputStream in =

new ObjectInputStream(

new FileInputStream("Blips.out"));

System.out.println("Recovering b1:");

b1 = (Blip1)in.readObject();

// OOPS! Throws an exception:

//! System.out.println("Recovering b2:");

//! b2 = (Blip2)in.readObject();

} catch(Exception e) {

e.printStackTrace();

}

}



} ///:~

Изходът от програмата е:

Constructing objects:

Blip1 Constructor

Blip2 Constructor

Saving objects:

Blip1.writeExternal

Blip2.writeExternal

Recovering b1:

Blip1 Constructor

Blip1.readExternal

Причината че Blip2 обекта не е възстановен е че когато се прави това възниква изключение. Можете ли да забележите разликата между Blip1 и Blip2? Конструктора на Blip1 е public, докато този на Blip2 не е, а това предизвиква из­ключение при възстановяването. Опитайте да направите конструктора на Blip2 да е public като махнете //! коментарите за да видите коректните ре­зул­та­ти.

Когато b1 е възстановено вика се конструктора по подразбиране на Blip1. Това е различно от възстановяването на Serializable обект, където обектът се прави изцяло от запомнените негови битове, без извикване на конструктор(и). При Externalizable обект всичко с конструкторите си става както обикновено (вклю­чително инициализацията в точката на дефиниране на полетата), а тогава readExternal( ) се вика. Трябва да сте предупредени за това – в частност че кон­структорът по подразбиране винаги играе – за да направите коректно по­ве­де­нието на вашите Externalizable обекти.

Ето пример който показва какво трябва да се направи за пълно запомняне и възстановяване на Externalizable обект:

//: c10:Blip3.java

// Reconstructing an externalizable object

import java.io.*;

import java.util.*;


class Blip3 implements Externalizable {

int i;


String s; // No initialization

public Blip3() {

System.out.println("Blip3 Constructor");

// s, i not initialized

}

public Blip3(String x, int a) {



System.out.println("Blip3(String x, int a)");

s = x;


i = a;

// s & i initialized only in non-default

// constructor.

}

public String toString() { return s + i; }



public void writeExternal(ObjectOutput out)

throws IOException {

System.out.println("Blip3.writeExternal");

// You must do this:

out.writeObject(s); out.writeInt(i);

}

public void readExternal(ObjectInput in)



throws IOException, ClassNotFoundException {

System.out.println("Blip3.readExternal");

// You must do this:

s = (String)in.readObject();

i =in.readInt();

}

public static void main(String[] args) {



System.out.println("Constructing objects:");

Blip3 b3 = new Blip3("A String ", 47);

System.out.println(b3.toString());

try {


ObjectOutputStream o =

new ObjectOutputStream(

new FileOutputStream("Blip3.out"));

System.out.println("Saving object:");

o.writeObject(b3);

o.close();

// Now get it back:

ObjectInputStream in =

new ObjectInputStream(

new FileInputStream("Blip3.out"));

System.out.println("Recovering b3:");

b3 = (Blip3)in.readObject();

System.out.println(b3.toString());

} catch(Exception e) {

e.printStackTrace();

}

}



} ///:~

Полетата s и i се инициализират във втория конструктор, не в този по под­раз­би­ране. Това значи че ако не инициализирате s и i в readExternal, ще бъде null (понеже паметта на това място се нулира първо при създаването на обекта). Ако изкоментирате двата реда в кода следващи фразата “You must do this” и пуснете програмата, ще видите че когато обектът е възстановен, s е null и i е ну­ла.

Ако наследявате от Externalizable обект, типично ще викате версиите от ба­зо­вия клас на writeExternal( ) и readExternal( ) за правилно запомняне и въз­ста­но­вяване на компонентите.

Така че за да станат нещата както трябва необходимо е не само да напишете важ­ните данни на обекта чрез writeExternal( ) метода (няма поведение по под­раз­биране което да пише който и да е член на Externalizable обект), но също тряб­ва и да възстановите въпросните данни чрез readExternal( ) метода. Това мо­же да бъде малко смущаващо отначало понеже поведението по под­раз­би­ра­не при конструиране на Externalizable обект може да създаде впечатление, че ня­какъв вид запомняне и възстановяване става автоматично. Това не става.


Ключовата дума transient


В процеса на управление на сериализацията би могло да се случи конкретен обект да не е желателно да бъде запазван и изваждан от сериализационния ме­ха­низъм на Java автоматично. Това обикновено е случаят, когато подобектът но­си важна информация, която не бихте искали да се сериализира, като на­при­мер парола. Даже тази информация да е private в обекта, веднъж сериализирана тя може да стане достъпна за някой който е чел файла или подслушал пре­да­ва­не­то по мрежата.

Един начин да се предотврати сериализацията на важни ваши обекти е да се из­полз­ва Externalizable, както беше показано. Тогава нищо не се сериализира и тряб­­ва да посочите кое да бъде сериализирано явно в writeExternal( ).

Ако работите със Serializable обект, обаче, сериализацията става автоматично. За да се управлява това, може да включвате и изключвате сериализацията поле по поле чрез ключовата дума transient, която казва “Не се занимавай със за­паз­ва­нето и възстановяването на това - аз ще се погрижа.”

Например да вземем Login обект който пази информация за конкретна сесия. Да кажем, че след като е потвърдено влизането, трябва да се запазят данни, но без паролата. Най-лесният начин да се направи това е да се приложи Serializable и да се направи полето password да бъде transient. Ето как из­глеж­да това:

//: c10:Logon.java

// Demonstrates the "transient" keyword

import java.io.*;

import java.util.*;


class Logon implements Serializable {

private Date date = new Date();

private String username;

private transient String password;

Logon(String name, String pwd) {

username = name;

password = pwd;

}

public String toString() {



String pwd =

(password == null) ? "(n/a)" : password;

return "logon info: \n " +

"username: " + username +

"\n date: " + date.toString() +

"\n password: " + pwd;

}

public static void main(String[] args) {



Logon a = new Logon("Hulk", "myLittlePony");

System.out.println( "logon a = " + a);

try {

ObjectOutputStream o =



new ObjectOutputStream(

new FileOutputStream("Logon.out"));

o.writeObject(a);

o.close();

// Delay:

int seconds = 5;

long t = System.currentTimeMillis()

+ seconds * 1000;

while(System.currentTimeMillis() < t)

;

// Now get them back:



ObjectInputStream in =

new ObjectInputStream(

new FileInputStream("Logon.out"));

System.out.println(

"Recovering object at " + new Date());

a = (Logon)in.readObject();

System.out.println( "logon a = " + a);

} catch(Exception e) {

e.printStackTrace();

}

}



} ///:~

Може да се види че полетата date и username са обикновени (не transient) и като такива са сериализирани автоматично. password обаче е transient, така че не е запомняно на диска; също и сериализационния механизъм не прави опит да го възстановява. Изходът е:

logon a = logon info:

username: Hulk

date: Sun Mar 23 18:25:53 PST 1997

password: myLittlePony

Recovering object at Sun Mar 23 18:25:59 PST 1997

logon a = logon info:

username: Hulk

date: Sun Mar 23 18:25:53 PST 1997

password: (n/a)

Когато обектът е възстановен, полето password е null. Забележете че toString( ) трябва да провери за null стойност на password понеже ако се опитате да мон­ти­рате String обект чрез претоварения ‘+’ оператор и тай намери null ма­ни­пу­ла­тор, ще получите NullPointerException. (По-нови версии на Java биха могли да имат код за избягване на този проблем.)

Може също да видите че полето date е запазено на диска и възстановено от не­го и не е запълвано наново (от системната дата - б.пр.).

Тъй като Externalizable обекти не запомнят никои свои полета на диска по под­раз­­биране, ключовата дума transient е за използване само със Serializable обек­ти.


Алтернатива на Externalizable


Ако не горите от желание да прилагате Externalizable интерфейс, има друг подход. Може да приложите Serializable интерфейс и да добавите (забележете че казвам “добавите” а не “подтиснете” или “реализирате”) методи наречени writeObject( ) и readObject( ) които автоматично ще бъдат викани когато обек­ти­те биват сериализирани или десериализирани, респективно. Тоест ако дадете те­зи методи, те ще бъдат използвани вместо стандартната сериализация.

Тези методи трябва да имат точно следните сигнатури:

private void

writeObject(ObjectOutputStream stream)

throws IOException;
private void

readObject(ObjectInputStream stream)

throws IOException, ClassNotFoundException

От гледна точка на дизайна нещата стават съвсем странни тука. Преди всичко, мо­же да се помисли, че понеже тези методи не са част от базов клас или Serializable интерфейс, те трябва да бъдат дефинирани в техни собствени ин­тер­фейси. Но забележете че те са дефинирани като private, което значи че тряб­ва да се викат само от други членове на техния клас. Обаче на практика не ги ви­кате от членове на класа, ами writeObject( ) и readObject( ) методите на ObjectOutputStream и ObjectInputStream обекти викат writeObject( ) и readObject( ) методите на вашите обекти. (Забележете моето страшно въз­дър­жа­не да навляза в остра критика тука заради използването на едни и същи име­на на методи. С една дума: смущаващо.) Може да се чудите защо Object­Out­putStream и ObjectInputStream обектите имат достъп до private ме­то­ди на ва­шия клас. Може само да предполагаме, че това е част от магията на сериа­ли­за­цията.

Във всеки случай, всичко дефинирано в interface е автоматично public така че ако writeObject( ) и readObject( ) трябва да бъдат private, те не могат да бъдат част от interface. След като трябва да спазите сигнатурите точно, ефектът е съ­щият като от реализация (прилагане) на interface.

Би изглеждало че когато викате ObjectOutputStream.writeObject( ), Serializable обектът който подавате е разпитван (чрез размишление, няма съм­не­ние) за да се види дали реализира свой собствен writeObject( ). Ако да, нор­малният процес на сериализация се пропуска и се вика writeObject( ). Съ­щата ситуация за readObject( ).

Има една друга особеност. Вътре във вашия writeObject( )може да изберете да използвате writeObject( ) действието по подразбиране викайки default­WriteObject( ). Подобно, в readObject( ) може да извикате defaultReadObject( ). Ето прост пример който показва как може да се управлява запазването и въз­ста­новяването на Serializable обект:

//: c10:SerialCtl.java

// Controlling serialization by adding your own

// writeObject() and readObject() methods.

import java.io.*;
public class SerialCtl implements Serializable {

String a;

transient String b;

public SerialCtl(String aa, String bb) {

a = "Not Transient: " + aa;

b = "Transient: " + bb;

}

public String toString() {



return a + "\n" + b;

}

private void



writeObject(ObjectOutputStream stream)

throws IOException {

stream.defaultWriteObject();

stream.writeObject(b);

}

private void



readObject(ObjectInputStream stream)

throws IOException, ClassNotFoundException {

stream.defaultReadObject();

b = (String)stream.readObject();

}

public static void main(String[] args) {



SerialCtl sc =

new SerialCtl("Test1", "Test2");

System.out.println("Before:\n" + sc);

ByteArrayOutputStream buf =

new ByteArrayOutputStream();

try {


ObjectOutputStream o =

new ObjectOutputStream(buf);

o.writeObject(sc);

// Now get it back:

ObjectInputStream in =

new ObjectInputStream(

new ByteArrayInputStream(

buf.toByteArray()));

SerialCtl sc2 = (SerialCtl)in.readObject();

System.out.println("After:\n" + sc2);

} catch(Exception e) {

e.printStackTrace();

}

}

} ///:~



В този пример едното String поле е обикновено а другото transient, за да се ви­ди че не-transient полето се запазва чрез defaultWriteObject( ) метода и transi­ent полето се запазва и възстановява явно. Полетата се инициализират в кон­структора наместо в точката на дефиницията им за да се докаже, че те не са ини­циализирани автоматично по някакъв механизъм по време на де­се­риа­ли­за­ция­та.

Ако се готвите да използвате механизма по подразбиране за да пишете не-transient частите от вашия обект, трябва да извикате defaultWriteObject( ) като пър­ва операция във writeObject( ) и defaultReadObject( ) кято първа операция в readObject( ). Това са странни викания на методи. Сякаш, например, викате defaultWriteObject( ) за ObjectOutputStream без аргументи, и все пак някакси той се оглежда наоколо и знае необходимите манипулатори за да напише не-transient частите. Призрачно.

Запазването и възстановяването на transient обекти използва по-познат код. Все пак да помислим какво става тука. В main( ) се създава SerialCtl обект, по­сле се сериализира в ObjectOutputStream. (Забележете в този случай, че се из­полз­ва буфер вместо файл – и това може за ObjectOutputStream.) Сериа­ли­за­ция­та става на реда:

o.writeObject(sc);

Методът writeObject( ) трябва да провери sc дали има негов собствен writeObject( ) метод. (Не чрез преглед на интерфейса – няма такъв – или типа на класа, но чрез истински лов на метода използвайки рефлексия.) Ако има, той би­ва използван. Подобен подход е в сила и за readObject( ). Може би това е един­ствения практичен начин намерен за решаване на проблема, но наистина е стра­нен.

Използване на персистентност


Твърде привлекателно е да използвате сериализацията за запазване на данни за съ­стоянието на програма и възстановяването на същото състояние впо­след­ствие. Но преди да се направи това, трябва да се отговори на някои въпроси. Ка­кво ще стане ако запазите два обекта и двата съдържащи манипулатор към трети? Когато възстановявате двата обекта в оригиналното им състояние, само ед­на поява на третия обект ли ще има? Какво ще стане, ако сериализирате ва­ши­те обекти в различни файлове и ги десериализирате в различни места на про­гра­мата?

Ето пример, който показва проблема:

//: c10:MyWorld.java

import java.io.*;

import java.util.*;
class House implements Serializable {}
class Animal implements Serializable {

String name;

House preferredHouse;

Animal(String nm, House h) {

name = nm;

preferredHouse = h;

}

public String toString() {



return name + "[" + super.toString() +

"], " + preferredHouse + "\n";

}

}
public class MyWorld {



public static void main(String[] args) {

House house = new House();

ArrayList animals = new ArrayList();

animals.add(

new Animal("Bosco the dog", house));

animals.add(

new Animal("Ralph the hamster", house));

animals.add(

new Animal("Fronk the cat", house));

System.out.println("animals: " + animals);


try {

ByteArrayOutputStream buf1 =

new ByteArrayOutputStream();

ObjectOutputStream o1 =

new ObjectOutputStream(buf1);

o1.writeObject(animals);

o1.writeObject(animals); // Write a 2nd set

// Write to a different stream:

ByteArrayOutputStream buf2 =

new ByteArrayOutputStream();

ObjectOutputStream o2 =

new ObjectOutputStream(buf2);

o2.writeObject(animals);

// Now get them back:

ObjectInputStream in1 =

new ObjectInputStream(

new ByteArrayInputStream(

buf1.toByteArray()));

ObjectInputStream in2 =

new ObjectInputStream(

new ByteArrayInputStream(

buf2.toByteArray()));

ArrayList animals1 = (ArrayList)in1.readObject();

ArrayList animals2 = (ArrayList)in1.readObject();

ArrayList animals3 = (ArrayList)in2.readObject();

System.out.println("animals1: " + animals1);

System.out.println("animals2: " + animals2);

System.out.println("animals3: " + animals3);

} catch(Exception e) {

e.printStackTrace();

}

}

} ///:~



Едното интересно нещо тук е че може да използвате сериализацията към бай­то­во поле за правене на “дълбоко копие” от всякакви обекти които са Serializable. (Дълбоко копие значи че се дублицира цялата паяжина от обекти, а не само основния обект и манипулаторите в него.) Копирането е разгледано с дъл­бочина в глава 12.

Animal съдържа полета от тип House. В main( ) един ArrayList от тези Animals е създаден и после сериализиран в два отделни потока. Когато бъдат де­се­риа­ли­зи­рани и изведени, виждат се следните резултати от едно пускане (обектите ще бъ­дат в различни места на паметта при различните пускания):

animals: [Bosco the dog[Animal@1cc76c], House@1cc769

, Ralph the hamster[Animal@1cc76d], House@1cc769

, Fronk the cat[Animal@1cc76e], House@1cc769

]

animals1: [Bosco the dog[Animal@1cca0c], House@1cca16



, Ralph the hamster[Animal@1cca17], House@1cca16

, Fronk the cat[Animal@1cca1b], House@1cca16

]

animals2: [Bosco the dog[Animal@1cca0c], House@1cca16



, Ralph the hamster[Animal@1cca17], House@1cca16

, Fronk the cat[Animal@1cca1b], House@1cca16

]

animals3: [Bosco the dog[Animal@1cca52], House@1cca5c



, Ralph the hamster[Animal@1cca5d], House@1cca5c

, Fronk the cat[Animal@1cca61], House@1cca5c

]

Разбира се очаква се десериализираните обекти да имат различни адреси от ори­гиналите си. Но забележете че в animals1 и animals2 се появяват едни и съ­щи адреси, включително позоваванията на House обект който двата си спо­де­лят. От друга страна, когато animals3 се възстановява, няма начин системата да знае че обектите във втория поток са синоними на обекти в първия, така че тя пра­ви напълно отделна система обекти.



Докато сериализирате всичко в един поток ще може да възстановите същата си­сте­ма обекти която сте записали, без нежелано дублициране на обекти. Разбира се, бихте могли да промените състоянието на обектите през времето между за­пис­ването на първия и последния, но това си е ваша отговорност – обектите ще бъ­дат записани в състоянието в което са в момента (и с каквито връзки са с дру­ги обекти) на сериализацията им.

Най-сигурното нещо по въпроса е да се направи “атомарна” операция. Ако сериализирате някакви неща, свършите някои работи, сериализирате още и т.н., то­гава няма да запазите системата сигурно. Вместо това, сложете всичките обек­­ти които отразяват състоянието на системата в единствена колекция и за­пи­­шете тази колекция в една операция. После може да я възстановите с един­стве­но викане на метод, също така.

Следният пример е за въображаема CAD система която демонстрира подхода. Освен това се разглежда и въпросът за използване на static полета – ако по­глед­не­те в документацията ще видите че Class е Serializable, така че трябва да е лес­но да се запомнят static полета просто чрез сериализация на Class обект. То­ва изглежда смислен подход, във всеки случай.

//: c10:CADState.java

// Saving and restoring the state of a

// pretend CAD system.

import java.io.*;

import java.util.*;


abstract class Shape implements Serializable {

public static final int

RED = 1, BLUE = 2, GREEN = 3;

private int xPos, yPos, dimension;

private static Random r = new Random();

private static int counter = 0;

abstract public void setColor(int newColor);

abstract public int getColor();

public Shape(int xVal, int yVal, int dim) {

xPos = xVal;

yPos = yVal;

dimension = dim;

}

public String toString() {



return getClass().toString() +

" color[" + getColor() +

"] xPos[" + xPos +

"] yPos[" + yPos +

"] dim[" + dimension + "]\n";

}

public static Shape randomFactory() {



int xVal = r.nextInt() % 100;

int yVal = r.nextInt() % 100;

int dim = r.nextInt() % 100;

switch(counter++ % 3) {

default:

case 0: return new Circle(xVal, yVal, dim);

case 1: return new Square(xVal, yVal, dim);

case 2: return new Line(xVal, yVal, dim);

}

}

}


class Circle extends Shape {

private static int color = RED;

public Circle(int xVal, int yVal, int dim) {

super(xVal, yVal, dim);

}

public void setColor(int newColor) {



color = newColor;

}

public int getColor() {



return color;

}

}


class Square extends Shape {

private static int color;

public Square(int xVal, int yVal, int dim) {

super(xVal, yVal, dim);

color = RED;

}

public void setColor(int newColor) {



color = newColor;

}

public int getColor() {



return color;

}

}


class Line extends Shape {

private static int color = RED;

public static void

serializeStaticState(ObjectOutputStream os)

throws IOException {

os.writeInt(color);

}

public static void



deserializeStaticState(ObjectInputStream os)

throws IOException {

color = os.readInt();

}

public Line(int xVal, int yVal, int dim) {



super(xVal, yVal, dim);

}

public void setColor(int newColor) {



color = newColor;

}

public int getColor() {



return color;

}

}


public class CADState {

public static void main(String[] args)

throws Exception {

ArrayList shapeTypes, shapes;

if(args.length == 0) {

shapeTypes = new ArrayList();

shapes = new ArrayList();

// Add handles to the class objects:

shapeTypes.add(Circle.class);

shapeTypes.add(Square.class);

shapeTypes.add(Line.class);

// Make some shapes:

for(int i = 0; i < 10; i++)

shapes.add(Shape.randomFactory());

// Set all the static colors to GREEN:

for(int i = 0; i < 10; i++)

((Shape)shapes.get(i))

.setColor(Shape.GREEN);

// Save the state vector:

ObjectOutputStream out =

new ObjectOutputStream(

new FileOutputStream("CADState.out"));

out.writeObject(shapeTypes);

Line.serializeStaticState(out);

out.writeObject(shapes);

} else { // There's a command-line argument

ObjectInputStream in =

new ObjectInputStream(

new FileInputStream(args[0]));

// Read in the same order they were written:

shapeTypes = (ArrayList)in.readObject();

Line.deserializeStaticState(in);

shapes = (ArrayList)in.readObject();

}

// Display the shapes:



System.out.println(shapes);

}

} ///:~



Shape класът implements Serializable, така че всичко, което е наследил от Shape е автоматично Serializable също така. Всеки Shape съдържа данни, а все­ки извлечен Shape клас съдържа static поле което определя цвета на всички Shapeове от него тип. (Слагането на static поле в базовия клас ще даде само ед­но поле, понеже static полетата не се дублицират в извлечените класове.) Ме­то­ди­те на базовия клас могат да бъдат подтиснати за да се зададе цвят за раз­лич­ни­те типове (static методите не се свързват динамично, така че тези са нор­мал­ни методи). randomFactory( ) методът създава различен Shape всеки път, ко­га­то се вика, използвайки случайни стойности за данните на Shape.

Circle и Square са праволинейни разширения на Shape; единствената разлика е че Circle инициализира color в точката на дефиницията а Square го ини­циа­ли­зи­ра в конструктора. Ще дискутираме Line по-късно.

В main( ), един ArrayList се използва за Class обектите и друг за формите. Ако не зададете аргумент на командния ред shapeTypes ArrayList се създава и Class обекти се добавят, а после shapes ArrayList се създава и Shape се до­ба­вят. После всички static color стойности се поставят да бъдат GREEN, накрая всич­ко се сериализира във файла CADState.out.

Ако дадете аргумент на командния ред (предполагаемо CADState.out), този файл се отваря и използва за възстановяване на състоянието на програмата. В две­те ситуации, резултиращият ArrayList от Shapeове се извежда. Резултатите от едно пускане са:

>java CADState

[class Circle color[3] xPos[-51] yPos[-99] dim[38]

, class Square color[3] xPos[2] yPos[61] dim[-46]

, class Line color[3] xPos[51] yPos[73] dim[64]

, class Circle color[3] xPos[-70] yPos[1] dim[16]

, class Square color[3] xPos[3] yPos[94] dim[-36]

, class Line color[3] xPos[-84] yPos[-21] dim[-35]

, class Circle color[3] xPos[-75] yPos[-43] dim[22]

, class Square color[3] xPos[81] yPos[30] dim[-45]

, class Line color[3] xPos[-29] yPos[92] dim[17]

, class Circle color[3] xPos[17] yPos[90] dim[-76]

]
>java CADState CADState.out

[class Circle color[1] xPos[-51] yPos[-99] dim[38]

, class Square color[0] xPos[2] yPos[61] dim[-46]

, class Line color[3] xPos[51] yPos[73] dim[64]

, class Circle color[1] xPos[-70] yPos[1] dim[16]

, class Square color[0] xPos[3] yPos[94] dim[-36]

, class Line color[3] xPos[-84] yPos[-21] dim[-35]

, class Circle color[1] xPos[-75] yPos[-43] dim[22]

, class Square color[0] xPos[81] yPos[30] dim[-45]

, class Line color[3] xPos[-29] yPos[92] dim[17]

, class Circle color[1] xPos[17] yPos[90] dim[-76]

]

Може да се види че стойностите xPos, yPos, и dim са били запазени и въз­ста­но­ве­ни всичките успешно, но има нещо нередно с възстановяването на static ин­фор­мацията. Навсякъде е вкарано ‘3’, но не се възстановява същото. Circles имат стойност 1 (RED, което е дефиницията), а Square-те имат стойност 0 (пом­нете, те се инициализират в конструктора). Сякаш static-те не са се се­риа­ли­зирали въобще! Това е така – макар и Class да е Serializable, той не прави ка­кво­то се очаква. Така че ако искате да сериализирате static, трябва сами да го пра­вите.



За това са serializeStaticState( ) и deserializeStaticState( ) static методите в Line. Може да се види, че те явно се викат в процесите на запазване и въз­ста­но­вя­ва­не. (Забележете че трябва да се поддържа редът на запазване и възстановяване.) Та­ка за да се направи CADState.java да работи коректно трябва (1) Да се до­ба­ви serializeStaticState( ) и deserializeStaticState( ) към фигурите, (2) Да се махне ArrayList shapeTypes и всичкият свързан с него код, (3) Да се добавят из­вик­ва­ния на съответните методи във фигурите.

Друго нещо за което може да се наложи да мислите е сигурността, понеже сериа­лизацията също запазва private данни. Ако сигурността е важна, такива по­лета ще се отбележат с transient. Но тогава ще трябва да си намерите си­гу­рен начин (кодиране и пр. - бел.пр.) за запазване на стойностите, така че private про­менливите да могат да се поставят както трябва после.





Сериализация





Сподели с приятели:




©obuch.info 2024
отнасят до администрацията

    Начална страница