Java 1.1 е добавил интересна черта, наречена сериализация на обекти която позволява всеки обект който реализира Serializable интерфейса да се превърне в последователност от байтове, от която после може напълно да се възстанови оригиналния обект. Това е вярно даже и през мрежи, което значи че механизмът компенсира автоматично разликите в операционните системи. Тоест може да създадете обект на Windows машина, да го сериализирате, да го изпратите през мрежата на Unix машина където той ще бъде коректно реконструиран. Не е необходимо да се безпокоите за данновите типове на двете системи, подреждането на байтовете или други детайли.
Сама по себе си сериализацията на обекти е интересна с това, че позволява да се реализира лека устойчивост. Запомнете че обектовата устойчивост означава, че обектът няма време на живот колкото е времето на изпълнение на програмата – обектът съществува и между извикванията на програмата. Чрез вземане на сериализиран обект и записването му на диск, а после възстановяването му когато програмата се пусне пак, може да се постигне устойчивост. Причината тя да се нарича “лека” е че не може просто да я посочите чрез някаква ключова дума като “persistent” и да оставите системата да се грижи за детайлите (макар че това може би ще стане в бъдеще). Вместо това трябва явно да се сериализира и де-сериализира обектът в програмата.
Обектовата сериализация бе добавена в езика за поддържане на две главни черти. Remote method invocation (RMI) на Java 1.1 позволява обекти които живеят на друга машина да действат каточели са на вашата машина. Когато се изпращат съобщения на далечните обекти, сериализацията е необходима за изпращане на аргументите и връщането на стойности. RMI се разглежда в глава 15.
Сериализацията е също необходима за Java Beans, въведени с Java 1.1. Когато се използва Bean, неговата информация обикновено се задава по време на проектирането. Тази информация трябва да се запомни и после използва когато се пуска програмата; обектовата сериализация изпълнява тази задача.
Сериализирането на обект е доста просто, ако обектът реализира Serializable интерфейс (този интерфейс е само флаг и няма методи). В Java 1.1 много стандартни библиотечни класове са променени така че да могат да се сериализират, включително всички обвивки на примитивните типове, всичките класове-колекции и много други. Даже Class обекти може да се сериализират. (Виж глава 11 за такива неща.)
За да се сериализира обект трябва да се създаде някакъв OutputStream обект и после да се обгърне с ObjectOutputStream обект. В тази точка просто трябва да извикате writeObject( ) и вашият обект е сериализиран и изпратен в OutputStream. За да се направи обратното, обгръщате InputStream с ObjectInputStream и викате readObject( ). Това което пристига обратно е, както обикновено, ъпкастнат манипулатор на Object, така че трябва с даункастинг да оправите нещата.
Един особено умен аспект но сериализацията на обекти е че тя не само записва даден обект, но проследява съдържащите се в него манипулатори и записва и тези обекти, и проследява всеки от съдържащите се в тези обекти манипулатори и т.н. Това понякога се нарича “паяжина от обекти” с която отделен обект може да бъде свързан и тя включва масиви от манипулатори на обекти както и обекти-членове. Ако трябваше да се обслужва собствена потребителска схома на сериализация, поддържането на кода обклужващ това множество връзки би било изумяващо. Обаче обектовата сериализация в Java го прави безукорно, без съмнение използвайки оптимизиран алгоритъм. Следващият пример изпробва сериализационния механизъм създавайки “червей” от свързани обекти, всеки от които има връзка с обект от следващия член на червея, както и с масив от манипулатори към обекти в друг обект, Data:
//: c10:Worm.java
// Demonstrates object serialization in Java 1.1
import java.io.*;
class Data implements Serializable {
private int i;
Data(int x) { i = x; }
public String toString() {
return Integer.toString(i);
}
}
public class Worm implements Serializable {
// Generate a random int value:
private static int r() {
return (int)(Math.random() * 10);
}
private Data[] d = {
new Data(r()), new Data(r()), new Data(r())
};
private Worm next;
private char c;
// Value of i == number of segments
Worm(int i, char x) {
System.out.println(" Worm constructor: " + i);
c = x;
if(--i > 0)
next = new Worm(i, (char)(x + 1));
}
Worm() {
System.out.println("Default constructor");
}
public String toString() {
String s = ":" + c + "(";
for(int i = 0; i < d.length; i++)
s += d[i].toString();
s += ")";
if(next != null)
s += next.toString();
return s;
}
public static void main(String[] args) {
Worm w = new Worm(6, 'a');
System.out.println("w = " + w);
try {
ObjectOutputStream out =
new ObjectOutputStream(
new FileOutputStream("worm.out"));
out.writeObject("Worm storage");
out.writeObject(w);
out.close(); // Also flushes output
ObjectInputStream in =
new ObjectInputStream(
new FileInputStream("worm.out"));
String s = (String)in.readObject();
Worm w2 = (Worm)in.readObject();
System.out.println(s + ", w2 = " + w2);
} catch(Exception e) {
e.printStackTrace();
}
try {
ByteArrayOutputStream bout =
new ByteArrayOutputStream();
ObjectOutputStream out =
new ObjectOutputStream(bout);
out.writeObject("Worm storage");
out.writeObject(w);
out.flush();
ObjectInputStream in =
new ObjectInputStream(
new ByteArrayInputStream(
bout.toByteArray()));
String s = (String)in.readObject();
Worm w3 = (Worm)in.readObject();
System.out.println(s + ", w3 = " + w3);
} catch(Exception e) {
e.printStackTrace();
}
}
} ///:~
За да се направи интересно, масивът от Data обекти вътре в Worm се инициализира със случайни числа. (По този начин се избягва подозрението, че компилаторът държи някакви мета-данни.) Всеки сегмент на Worm е отбелязан с char който автоматично се генерира в процеса на рекурсивната генерация на свързания списък от Wormове. Когато създавате Worm, казвате на конструктора колко дълъг искате да бъде. За да направите next манипулатор (следващия-б.пр.) той вика Worm конструктор с дължина по-малка с единица и т.н. Последния next манипулатор е оставен като null, показвайки края на Worm (червея-б.пр.).
Работата е в това да се създаде нещо достатъчно сложно, което не би могло лесно да се сериализира (по друг начин-б.пр.). Актът на сериализация, обаче, е доста прост. Веднъж като се създаде ObjectOutputStream от някакъв друг поток, writeObject( ) сериализира обекта. Забележете викането на writeObject( ) за String, също така. Може също да пишете всичките примитивни типове данни използвайки същите методи като DataOutputStream (те споделят един и същ интерфейс).
Има два отделни try блока които изглеждат подобни. Първият пише и чете файл, а вторият, за разнообразие, чете и пише ByteArray. Може да четете и пишете обект използвайки сериализация към всеки DataInputStream или DataOutputStream включително, както ще видите в главата за мрежите, през мрежа. Изходът от едно пускане е:
Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(262):b(100):c(396):d(480):e(316):f(398)
Worm storage, w2 = :a(262):b(100):c(396):d(480):e(316):f(398)
Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398)
Може да видите, че възстановеният обект действително съдържа всички връзки от оригиналния обект.
Забележете че не се вика конструктор, нито даже и такъв по подразбиране, в процеса на десериализация на Serializable обект. Целият обект се възстановява от данните от InputStream.
Сериализацията на обекти е друга черта на Java 1.1 която не е част от новите Reader и Writer йерархии, а използва старите InputStream и OutputStream йерархии. Така може да се получат ситуации, при които сте принудени да смесите йерархиите.
Намиране на класа
Може би се чудите какво ли ще е необходимо за възстановяването на обект от неговото сериализирано състояние. Да кажем например че сте сериализирали обект и сте го изпратили през мрежа на друга маиена. Би ли могла програма на далечната машина да реконструира обекта използвайки само данните от файла?
Най-добрият начин да се отговори на този въпрос е (както обикновено) чрез експеримент. Следният файл отива в поддиректорията за тази глава:
//: 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( ). Може би това е единствения практичен начин намерен за решаване на промлема, но наистина е странен.
Промяна на версиите
Възможно е да поискате да промените версията на сериализуем клас (обекти от оригиналния клас може да са запазени в база данни, например). Това се поддържа, но вероятно ще го използвате в съвсем специални случаи, а освен това изисква по-голяма дълбочина на познанието, та няма да го разглеждаме тук. JDK1.1 HTML документите които може да свалите от Sun (и които могат да са част от вашия Java пакет с онлайн документи) покрива тази тема достатъчно пълно.
Използване на устойчивостта
Твърде привлекателно е да използвате сериализацията за запазване на данни за състоянието на програма и възстановяването на същото състояние впоследствие. Но преди да се направи това, трябва да се отговори на някои въпроси. Какво ще стане ако запазите два обекта и двата съдържащи манипулатор към трети? Когато възстановявате двата обекта в оригиналното им състояние, само една поява на третия обект ли ще има? Какво ще стане, ако сериализирате вашите обекти в различни файлове и ги десериализирате в различни места на програмата?
Ето пример, който показва проблема:
//: 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 променливите да могат да се поставят както трябва после.
Сподели с приятели: |