Сериализация в 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();
}
}
}
Следва описание на примера.
-
Създаване на класовете
Дефинираме два класа, обекти от които ще сериализираме. Единственото нещо, на което тук си струва да се обърне внимание е, че за да сериализираме обект класът, на който е инстанция, трябва да имплементира интерфейса Serializable. Този интерфейс е просто флаг и не притежава никакви методи. И двата ни класа имплементират този интерфейс.
Класът Boss има атрибут Vector, в който ще бъдат записани референции към обекти от класа Employee. Чрез този атрибут ще демонстрираме как при сериализацията на този обект ще бъдат сериализирани и всички обекти, към които той има референции и после ще могат да бъдат възстановени.
Класът Employee е съвсем прост, обект от него ще запишем след обекта от класа Boss, за да покажем, че в потока можем да записваме обекти от различни класове.
-
Инициализация на обектите
Тук създаваме 3 обекта от класа Employee и ги добавяме в обект от класа Vector, който ще предадем като параметър на конструктора на класа Boss, за да бъде инициализиран неговият атрибут Vector. Декларираме двата обекта, които ще бъдат съответно изходен и входен поток.
-
Записване на обектите в изходния поток - към файл
За целта създаваме обект от класа FileOutputStream, който ще ни служи за изходен поток към файл. След това чрез този създаден обект създаваме обект от класа ObjectOutputStream, чиято цел ще бъде да сериализира обектите и да ги записва в изходния поток – чрез методите writeXXX(param). Записваме в изходния поток обекта от класа Boss (при записване на обект от какъв да е клас става неявно преобразуване до базовия клас Object), обект от класа Employee и число от тип int. Методът flush() записва всички буферирани данни, ако има такива, и изчиства данните в буферите. В частта finally извикваме метода close(), който затваря потока и освобождава заетите ресурси.
-
Прочитане на обекти от входен поток - от файл
За целта създаваме обект от класа 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( ) методите на вашите обекти. (Забележете моето страшно въздържане да навляза в остра критика тука заради използването на едни и същи имена на методи. С една дума: смущаващо.) Може да се чудите защо ObjectOutputStream и ObjectInputStream обектите имат достъп до private методи на вашия клас. Може само да предполагаме, че това е част от магията на сериализацията.
Във всеки случай, всичко дефинирано в interface е автоматично public така че ако writeObject( ) и readObject( ) трябва да бъдат private, те не могат да бъдат част от interface. След като трябва да спазите сигнатурите точно, ефектът е същият като от реализация (прилагане) на interface.
Би изглеждало че когато викате ObjectOutputStream.writeObject( ), Serializable обектът който подавате е разпитван (чрез размишление, няма съмнение) за да се види дали реализира свой собствен writeObject( ). Ако да, нормалният процес на сериализация се пропуска и се вика writeObject( ). Същата ситуация за readObject( ).
Има една друга особеност. Вътре във вашия writeObject( )може да изберете да използвате writeObject( ) действието по подразбиране викайки defaultWriteObject( ). Подобно, в 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( ) метода и transient полето се запазва и възстановява явно. Полетата се инициализират в конструктора наместо в точката на дефиницията им за да се докаже, че те не са инициализирани автоматично по някакъв механизъм по време на десериализацията.
Ако се готвите да използвате механизма по подразбиране за да пишете не-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 променливите да могат да се поставят както трябва после.
Сподели с приятели: |