Главная страница

Ответы на вопросы по ревью 4. Java io. Ключевым понятием здесь является понятие потока


Скачать 1.93 Mb.
НазваниеJava io. Ключевым понятием здесь является понятие потока
Дата03.07.2022
Размер1.93 Mb.
Формат файлаdoc
Имя файлаОтветы на вопросы по ревью 4.doc
ТипДокументы
#623608
страница38 из 39
1   ...   31   32   33   34   35   36   37   38   39

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


Сериализация объектов Java позволяет вам взять любой объект, который реализует интерфейс Serializable и включит его в последовательность байт, которые могут быть полностью восстановлены для регенерации оригинального объекта. Это также выполняется при передаче по сети, что означает, что механизм сериализации автоматически поддерживается на различных операционных системах. То есть, вы можете создать объект на машине с Windows, сериализовать его и послать по сети на Unix машину, где он будет корректно реконструирован. Вам не нужно будет беспокоиться о представлении данных на различных машинах, порядке следования байт и любых других деталях.

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

Сериализация объектов была добавлена в язык для поддержки двух главных особенностей. Удаленный вызов методов (RMI) в Java позволяет объектам существовать на другой машине и вести себя так, как будто они существуют на вашей машине. Когда посылается сообщение удаленному объекту, необходима сериализация объекта для транспортировки аргументов и возврата значений. RMI обсуждается в Главе 15.

Сериализация объектов так же необходима для JavaBeans, описанных в Главе 13. Когда используется компонент (Bean), информация о его состоянии обычно конфигурируется во время дизайна. Эта информации о состоянии должна сохранятся, а затем восстанавливаться, когда программа запускается; cериализация объектов выполняет эту задачу.

Сериализация объекта достаточно проста, если объект реализует интерфейс Serializable (этот интерфейс похож на флаг и не имеет методов). Когда сериализация была добавлена в язык, многие стандартные библиотеки классов были изменены, чтобы сделать их сериализованными, включая все оболочки примитивных типов, все контейнерные классы и многие другие. Даже объект Class может быть сериализован. (Смотрите Главу 12 о реализации этого.)

Для сериализации объекта вы создаете определенный сорт объекта OutputStream, а затем вкладываете его в объект ObjectOutputStream. После этого вам достаточно вызвать writeObject( ) и ваш объект будет сериализован и послан в OutputStream. Чтобы провести обратный процесс, вы вкладываете InputStream внутрь ObjectInputStream и вызываете readObject( ). То, что приходит, обычно это ссылка на родительский Object, так что вы должны выполнить обратное приведение, чтобы сделать вещи правильными.

Особенно полезное свойство сериализации объектов состоит в том, что при этом сохраняется не только образ объекта, а за ним также следуют все ссылки, содержащиеся в вашем объекте. Эти объекты также сохраняются, а за ними следуют все ссылки из каждого объекта, и т.д. Иногда это называется “паутиной объектов”, так как единственный объект может быть присоединен к чему-то, и может содержать массив ссылок на объекты точно так же, как и на члены объектов. Если вы поддерживаете собственную схему сериализации объектов, код, поддерживающий все эти ссылки, может свести с ума. Однако сериализация объектов в Java, по видимому, осуществляет это безупречно, используя, несомненно, оптимизированный алгоритм, который исследует всю паутину объектов. Следующий пример проверяет механизм сериализации, создавая “цепочку” связанных объектов, каждый из которых имеет ссылку на следующий сегмент цепочки точно так же, как и массив ссылок указывает на объекты различных классов:

//: c11:Worm.java

// Демонстрация сериализации объектов.

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 {

// Генерируется случайно значение типа int:

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;

// Значение i == Номеру сегмента

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)

throws ClassNotFoundException, IOException {

Worm w = new Worm(6, 'a');

System.out.println("w = " + w);

ObjectOutputStream out =

new ObjectOutputStream(

new FileOutputStream("worm.out"));

out.writeObject("Worm storage");

out.writeObject(w);

out.close(); // Также очищается вывод

ObjectInputStream in =

new ObjectInputStream(

new FileInputStream("worm.out"));

String s = (String)in.readObject();

Worm w2 = (Worm)in.readObject();

System.out.println(s + ", w2 = " + w2);

ByteArrayOutputStream bout =

new ByteArrayOutputStream();

ObjectOutputStream out2 =

new ObjectOutputStream(bout);

out2.writeObject("Worm storage");

out2.writeObject(w);

out2.flush();

ObjectInputStream in2 =

new ObjectInputStream(

new ByteArrayInputStream(

bout.toByteArray()));

s = (String)in2.readObject();

Worm w3 = (Worm)in2.readObject();

System.out.println(s + ", w3 = " + w3);

}

} ///:

Чтобы сделать пример интереснее, массив объектов Data внутри Worm инициализируется случайными числами. (Этот способ не дает компилятору представление о типе хранимой мета информации.) Каждый сегмент цепочки (Worm) помечается символом (char), который генерируется автоматически в процессе рекурсивной генерации связанного списка Worm. Когда вы создаете Worm, вы говорите конструктору необходимую вам длину. Чтобы сделать следующуюссылку (next), вызывается конструктор Worm с длиной на единичку меньше, и т.д. Последняя ссылка next остается равной null, указывая на конец цепочки Worm.

Все это сделано для создания чего-то достаточно сложного, что не может быть легко сериализовано. Однако действия, направленные на сериализацию, достаточно просты. Как только создается объект ObjectOutputStream из некоторого другого потока, writeObject( ) сериализует объект. Обратите внимание, что вызов writeObject( ) для String такой же. Вы также можете записать все примитивные типы, используя тот же метод DataOutputStream (они задействуют тот же интерфейс).

Здесь есть две различные секции кода, которые выглядят одинаково. Первая пишет и читает файл, а вторая, для разнообразия, пишет и читает ByteArray. Вы можете прочесть и записать объект, используя сериализацию для любого DataInputStream или DataOutputStream, включая, как вы увидите в Главе 15, сеть. Вывод после одного запуска имеет вид:

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 не вызывается ни конструктор, ни даже конструктор по умолчанию.

Сериализация объектов является byte-ориентированной, и поэтому используется иерархия InputStream и OutputStream.

Нахождение класса


Вы можете задаться вопросом, что необходимо для восстановления объекта из его сериализованного состояния. Например, предположим, вы сериализовали объект и послали его в файл или по сети на другую машину. Может ли программа на другой машине реконструировать объект, используя только содержимое файла?

Лучшим способом для ответа на этот вопрос (как обычно) будет проведение эксперимента. Следующий файл содержится в поддиректории для этой главы:

//: c11:Alien.java

// Сериализуемый класс.

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

} ///:

Файл, который создает и сериализует объект Alien, содержится в том же директории:

//: c11:FreezeAlien.java

// Создает файл сериализации.

import java.io.*;
public class FreezeAlien {

// Выбрасывает исключение на консоль:

public static void main(String[] args)

throws IOException {

ObjectOutput out =

new ObjectOutputStream(

new FileOutputStream("X.file"));

Alien zorcon = new Alien();

out.writeObject(zorcon);

}

} ///:

С точки зрения поимки и обработки исключений, эта программа выбрала быстрый и грязный способ передачи исключений наружу main( ), так что информация о них будет помещаться в командной строке.

Как только программа будет скомпилирована и запущена, скопируйте результирующий файл X.file в поддиректорий, под названием xfiles, где имеется следующий код:

//: c11:xfiles:ThawAlien.java

// Пробуем восстановить сериализованный файл

// без объекта класса, хранимого в файле.

import java.io.*;
public class ThawAlien {

public static void main(String[] args)

throws IOException, ClassNotFoundException {

ObjectInputStream in =

new ObjectInputStream(

new FileInputStream("X.file"));

Object mystery = in.readObject();

System.out.println(mystery.getClass());

}

} ///:

Эта программа открывает файл и успешно читает объект mystery. Однако, как только вы попробуете найти что-нибудь об объекте — что требует Class объекта для Alien — виртуальная машина Java (JVM) не сможет найти Alien.class (если он не будет указан в Classpath, чего не должно случится в этом примере). Вы получите ClassNotFoundException. (Еще раз: все свидетельства иной жизни исчезнут прежде, чем доказательства ее существования могут быть проверены!)

Если вы хотите многое сделать после восстановления объекта, который был сериализован, вы должны убедится, что JVM может найти соответствующий .class либо по локальному пути классов, либо где-то в Internet.

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


Как вы можете видеть, стандартный механизм сериализации тривиален в использовании. Но что, если вам нужны специальные требования? Может быть, вы имеете особые требования по безопасности и вы не хотите сериализовать часть вашего объекта, или, может быть, не имеет смысла сериализовать один из подобъектов, если эта часть будет вновь создана при восстановлении объекта.

Вы можете управлять процессом сериализации, реализовав интерфейс Externalizable вместо интерфейса Serializable. Интерфейс Externalizable расширяет интерфейс Serializable и добавляет два метода: writeExternal( ) и readExternal( ), которые автоматически вызываются для вашего объекта во время сериализации и десериализации, так что вы можете выполнить специальные операции.

Следующий пример показывает простую реализацию методов интерфейса Externalizable. Обратите внимание, что Blip1 и Blip2 почти идентичны, за исключением тонких различий (проверьте, сможете ли вы найти их в коде):

//: c11:Blips.java

// Простое использование Externalizable & ловушка.

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)

throws IOException, ClassNotFoundException {

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

Blip1 b1 = new Blip1();

Blip2 b2 = new Blip2();

ObjectOutputStream o =

new ObjectOutputStream(

new FileOutputStream("Blips.out"));

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

o.writeObject(b1);

o.writeObject(b2);

o.close();

// Теперь получаем их обратно:

ObjectInputStream in =

new ObjectInputStream(

new FileInputStream("Blips.out"));

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

b1 = (Blip1)in.readObject();

// OOPS! Выброшено исключение:

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

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

}

} ///:

Вывод для этой программы:

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:

//: c11:Blip3.java

// Реконструирование externalizable объекта.

import java.io.*;

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

int i;

String s; // Без инициализации

public Blip3() {

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

// s, i не инициализируется

}

public Blip3(String x, int a) {

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

s = x;

i = a;

// s & i инициализируются только в

// конструкторе не по умолчанию.

}

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

public void writeExternal(ObjectOutput out)

throws IOException {

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

// Вы обязаны сделать это:

out.writeObject(s);

out.writeInt(i);

}

public void readExternal(ObjectInput in)

throws IOException, ClassNotFoundException {

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

// Вы обязаны сделать это:

s = (String)in.readObject();

i =in.readInt();

}

public static void main(String[] args)

throws IOException, ClassNotFoundException {

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

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

System.out.println(b3);

ObjectOutputStream o =

new ObjectOutputStream(

new FileOutputStream("Blip3.out"));

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

o.writeObject(b3);

o.close();

// Теперь получим обратно:

ObjectInputStream in =

new ObjectInputStream(

new FileInputStream("Blip3.out"));

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

b3 = (Blip3)in.readObject();

System.out.println(b3);

}

} ///:

Поля s и i инициализируются только во втором конструкторе, но не в конструкторе по умолчанию. Это значит, что если вы не инициализируете s и i в readExternal( ), они будут равны null (так как хранилище объектов заполняется нулями при первом шаге создания объектов). Если вы закомментируете две строки кода, следующих за фразой “Вы обязаны сделать это”, и запустите программу, вы увидите, что при восстановлении объекта s равно null, а i равно нулю.

Если вы наследуете от объекта с интерфейсом Externalizable, обычно вы будете вызывать методы writeExternal( ) и readExternal( ) базового класса для обеспечения правильного хранения и восстановления компонент базового класса.

Таким образом, чтобы сделать все правильно, вы должны не только записать важные данные из объекта в методе writeExternal( ) (здесь нет стандартного поведения, при котором записывается любой член объекта с интерфейсом Externalizable), но вы также должны восстановить эти данные в методе readExternal( ). Сначала это может немного смущать, потому что поведение конструктора по умолчанию объекта с интерфейсом Externalizable может представить все, как некоторый вид автоматического сохранения и восстановления. Но это не так.

Ключевое слово transient


Когда вы управляете сериализацией, возможно появление определенных подобъектов, для который вы не захотите применять механизм сериализации Java для автоматического сохранения и восстановления. В основном это происходит в том случае, если такой подобъект представляет важную информацию, которую вы не хотите сериализовать, например, пароль. Даже если такая информация имеет модификатор private в объекте, так как она сериализуется, то кто-либо может получить доступ для чтения файла или перехватить сетевую передачу.

Один способ предохранения важной части вашего объекта от сериализации, заключающий в реализации Externalizable, показан в предыдущем разделе. Но при этом ничего автоматически не сериализуется и вы должны явно сеарилизовать только нужные вам части внутри writeExternal( ).

Однако если вы работаете с Serializable объектом, вся сериализация происходит автоматически. Для управления этим, вы можете выключить сериализацию полей индивидуально, используя ключевое слово transient, которое говорит: “Не беспокойтесь о сохранении и восстановлении этого — я позабочусь об этом”.

В качестве примера рассмотрим объект Login, хранящий информацию об определенной сессии подключения. Предположим, что как только вы проверили имя пользователя, вы хотите сохранить данные, но без пароля. Простейшим способом является реализация Serializable и пометка поля password ключевым словом transient. Вот как это выглядит:

//: c11:Logon.java

// Демонстрация ключевого слова "transient".

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 +

"\n password: " + pwd;

}

public static void main(String[] args)

throws IOException, ClassNotFoundException {

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

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

ObjectOutputStream o =

new ObjectOutputStream(

new FileOutputStream("Logon.out"));

o.writeObject(a);

o.close();

// Задержка:

int seconds = 5;

long t = System.currentTimeMillis()

+ seconds * 1000;

while(System.currentTimeMillis() < t)

;

// Теперь получаем его обратно:

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);

}

} ///:

Вы можете видеть, что поля 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 методам вашего класса. Мы можем только иметь в виду, что эта часть составляет магию сериализации.

В любом случае, все, что определено в интерфейсе, автоматически становится public, поэтому, если writeObject( ) и readObject( ) должны быть private, то они не могут быть частью интерфейса. Так как вы должны следовать точным сигнатурам, получаемый эффект тот же самые, как если бы вы реализовали interface.

Может показаться, что когда вы вызываете ObjectOutputStream.writeObject( ), объект с интерфейсом Serializable, который вы передаете, опрашивается (используя рефлексию, не имеет значения) на предмет реализации своего собственного writeObject( ). Если это так, то нормальный процесс сериализации пропускается, и вызывается writeObject( ). Аналогичная ситуация наблюдается и для readObject( ).

Есть еще один поворот. Внутри вашего writeObject( ) вы можете выбрать выполнение стандартного действия writeObject( ), вызвав defaultWriteObject( ). Точно так же, внутри readObject( ) вы можете вызвать defaultReadObject( ). Вот пример, который демонстрирует, как вы можете управлять хранением и восстановлением объектов с интерфейсом Serializable:

//: c11:SerialCtl.java

// Управление сериализацией, путем добавления

// собственных методов writeObject() и readObject().

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)

throws IOException, ClassNotFoundException {

SerialCtl sc =

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

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

ByteArrayOutputStream buf =

new ByteArrayOutputStream();

ObjectOutputStream o =

new ObjectOutputStream(buf);

o.writeObject(sc);

// Теперь получим это назад:

ObjectInputStream in =

new ObjectInputStream(

new ByteArrayInputStream(

buf.toByteArray()));

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

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

}

} ///:

В этом примере есть одно обычное поле 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( ). Возможно это чисто практический способ, которым можно решить проблему, но он, несомненно, странен.

Работа с версиями


Возможно, что вам захочется изменить версию сериализованного класса (объекты оригинального класса могут храниться, например, в базе данных). Это допустимо, но вы, вероятно, будете делать это только в специальных случаях, так как это требует дополнительного глубокого понимания, которого мы не достигнем здесь. Документация по JDK в формате HTML, доступная на java.sun.com, описывает эту тему достаточно полно.

Вы также должны обратить внимание, что в HTML документация JDK многие комментарии начинаются с предупреждения:

Внимание: Сериализованные объекты этого класса не будут совместимы с будущими выпусками Swing. Существующая поддержка сериализации подходит для кратковременного хранения или для RMI между приложениями. ...

Это происходит потому, что механизм работы с версиями слишком прост для надежной работы во всех ситуациях, особенно с JavaBeans. Он работает корректно для дизайна и это то, о чем говорит это предупреждение.

Использование устойчивости


Достаточно привлекательно использовать технологию сериализации для хранения некоторых состояний вашей программы, чтобы в последствии вы могли легко восстановить программу до текущего состояния. Но прежде, чем сделать это, необходимо ответить на некоторые вопросы. Что случится, если вы сериализуете два объекта, оба из которых имеют ссылки на один объект? Когда вы восстановите эти два объекта из их сериализованного состояния, будите ли вы иметь только один экземпляр третьего объекта? Что, если вы сериализуете два объекта в различные файлы, а десериализуете их в различных частях кода?

Вот пример, показывающий эту проблему:

//: c11: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)

throws IOException, ClassNotFoundException {

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);
ByteArrayOutputStream buf1 =

new ByteArrayOutputStream();

ObjectOutputStream o1 =

new ObjectOutputStream(buf1);

o1.writeObject(animals);

o1.writeObject(animals); // Запись второго класса

// Запись в другой поток:

ByteArrayOutputStream buf2 =

new ByteArrayOutputStream();

ObjectOutputStream o2 =

new ObjectOutputStream(buf2);

o2.writeObject(animals);

// Теперь получаем назад:

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);

}

} ///:

Одна вещь, которая интересна здесь, состоит в возможности использовать сериализацию объекта через массив байт, как способ выполнения “глубокого копирования” любого объекта с интерфейсом Serializable. (Глубокое копирование означает, что вы дублируете всю паутину объектов, а не просто основной объект и принадлежащие ему ссылки.) Более глубоко копирование освещено в Приложении А.

Объекты Animal содержат поля типа House. В main( ) создается ArrayList из этих Animal, и он сериализуется дважды в один поток, а затем снова в другой поток. Когда это десериализуется и распечатается, вы увидите следующий результат одного запуска (объекты будут располагаться в разных участках памяти при каждом запуске):

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 является сериализуемым, то должно быть легким хранение static поля простой сериализацией объекта Class. Тем не менее, такой подход выглядит достаточно важным.

//: c11:CADState.java

// Запись и восстановление состояния

// симулятора системы CAD.

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() +

" 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();

// Добавляем ссылку в объект класса:

shapeTypes.add(Circle.class);

shapeTypes.add(Square.class);

shapeTypes.add(Line.class);

// Создаем какие-то образы:

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

shapes.add(Shape.randomFactory());

// Устанавливаем все статические цвета в GREEN:

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

((Shape)shapes.get(i))

.setColor(Shape.GREEN);

// Запись вектора состояния:

ObjectOutputStream out =

new ObjectOutputStream(

new FileOutputStream("CADState.out"));

out.writeObject(shapeTypes);

Line.serializeStaticState(out);

out.writeObject(shapes);

} else { // Есть аргументы командной строки

ObjectInputStream in =

new ObjectInputStream(

new FileInputStream(args[0]));

// Читаем в том же порядке, в котором была запись:

shapeTypes = (ArrayList)in.readObject();

Line.deserializeStaticState(in);

shapes = (ArrayList)in.readObject();

}

// Отображаем образы:

System.out.println(shapes);

}

} ///:

Класс Shape реализует интерфейс Serializable, так что все, что наследуется от Shape, автоматически реализует Serializable. Каждый Shape содержит данные, а каждый наследуемый от Shape класс содержит статическое поле, определяющее цвет всех этих Shape. (Помещение статического поля в базовый класс приведет к тому, что будет существовать только одно поле, так как статическое поле не дублируется для наследуемых классов.) Методы базового класса могут быть перекрыты для установки цвета для различных типов (статические методы не имеют динамических ограничений, так что это обычные методы). Метод randomFactory( ) создает различные объекты Shape при каждом вызове, используя случайные значения для данных Shape.

Circle и Square являются прямым расширением Shape; отличия только в том, что Circle инициализирует color в точке определения, а Square инициализирует его в конструкторе. Дискуссию относительно Line пока отложим.

В main( ) используется один ArrayList для хранения объектов Class, а другой для хранения образов. Если вы не задействовали аргумент командной строки, создается shapeTypes ArrayList, и добавляются объекты Class, а затем создается ArrayList shapes, и в него добавляются объекты 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]

]

Вы можете видеть, что значения xPosyPos и dim были успешно сохранены и восстановлены, но при восстановлении статической информации произошли какие-то ошибки. Везде на входе имели “3”, но на выходе этого не получили. Circle имеет значение 1 (RED, как это определено), а Square имеет значение 0 (Помните, что он инициализировался в конструкторе). Это похоже на то, что static не сериализовался совсем! Это верно, несмотря на то, что класс Class реализует интерфейс Serializable, он не делает того, что вы от него ожидаете. Так что если вы хотите сериализовать statics, вы должны сделать это сами.

Это то, для чего нужны статические методы serializeStaticState( ) и deserializeStaticState( ) в Line. Вы можете видеть, что они явно вызываются как часть процесса сохранения и восстановления. (Обратите внимание, что порядок записи в файл сериализации и чтения из него должен сохранятся). Таким образом, чтобы CADState.java работал корректно, вы должны:

  1. Добавить serializeStaticState( ) и deserializeStaticState( ) к образам.

  2. Удалить ArrayList shapeTypes и весь код, относящийся к нему.

  3. Добавить вызов новых статических методов сериализации и десериализации образов.

Другую проблему вы можете получить, думая о безопасности, так как сериализация сохраняет данные с модификатором private. Если вы имеете проблемы безопасности, эти поля должны помечаться, как transient. Затем вы должны разработать безопасный способ для хранения такой информации, чтобы когда вы делали восстановление, вы могли установить эти private переменные.
1   ...   31   32   33   34   35   36   37   38   39


написать администратору сайта