Книга е още в много ранна фаза на написване



страница65/73
Дата25.07.2016
Размер13.53 Mb.
#6732
1   ...   61   62   63   64   65   66   67   68   ...   73

Сериализация на обекти


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( ) методите на вашите обекти. (Забележете моето страшно въз­дър­жа­не да навляза в остра критика тука заради използването на едни и същи име­на на методи. С една дума: смущаващо.) Може да се чудите защо Object­OutputStream и 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( ). Може би това е един­ствения практичен начин намерен за решаване на промлема, но наистина е стра­нен.

Промяна на версиите


Възможно е да поискате да промените версията на сериализуем клас (обекти от ори­гиналния клас може да са запазени в база данни, например). Това се под­дър­жа, но вероятно ще го използвате в съвсем специални случаи, а освен това изиск­ва по-голяма дълбочина на познанието, та няма да го разглеждаме тук. 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 про­менливите да могат да се поставят както трябва после.





Сподели с приятели:
1   ...   61   62   63   64   65   66   67   68   ...   73




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

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