[an error occurred while processing this directive] IT • archiv :: Print

IT • archiv


[an error occurred while processing this directive]

[an error occurred while processing this directive]

Фиксация ваших объектов

[an error occurred while processing this directive](none) [an error occurred while processing this directive](none)[an error occurred while processing this directive] ::
[an error occurred while processing this directive](none)
[an error occurred while processing this directive]([an error occurred while processing this directive] Тодд Гриньер [an error occurred while processing this directive])

[an error occurred while processing this directive](none)

Открытие секретов API сериализации в Java.

Object Persistence
PDF versionPDF версия
Обзор
API сериализации в Java используется многими другими API в Java (например RMI и JavaBeans) для сохранения объектов в то время, когда виртуальная машина не работает. Вы также можете использовать Java API сериализации вручную для ваших задач сохранения постоянства объектов. Хотя принципы Java сериализации очень просты, некоторые из более запутанных частей API часто могут быть загадкой. В этой статье, Todd Greanier раскроет секреты Java API сериализации. (2700 слов)

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

Сериализация объекта — это процесс сохранения состояния объекта в последовательность байтов и также процесс перестройки этих байтов в новый объект в будущем. Java API сериализации предоставляет стандартный механизм работы с сериализацией объектов для Java разработчиков. Этот API мал и прост в использовании, предоставляемые классы и методы понятны.

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

В заключении этой статьи, вы будете иметь хорошее понимание этого мощного, но иногда плохо понимаемого Java API.

Сначала первое: Стандартный механизм

Начнем с основ. Для сохранения объекта в Java, мы должны иметь сохраняемый объект. Объект делается сериализуемым путем реализации интерфейса java.io.Serializable, что значит для нижележащего API, что объект может быть сохранен в байтовом представлении и восстановлен в будущем.

Воспользуемся класс постоянства для демонстрации механизма сериализации:

10 import java.io.Serializable;
20 import java.util.Date;
30 import java.util.Calendar;
40 public class PersistentTime implements Serializable
50 {
60 private Date time;
70
80 public PersistentTime()
90 {
100      time = Calendar.getInstance().getTime();
110    }
120
130    public Date getTime()
140    {
150      return time;
160    }
170  }

Как вы можете видеть, единственным отличием от создания обычного класса является реализация интерфейса java.io.Serializable в строке 40. Полностью пустой интерфейс Serializable является маркером — это простое разрешение механизму сериализации проверять класс на возможность его сохранения. Таким образом, обратим внимание на первое правило сериализации:

Следующий шаг — это, собственно, сохранение объекта. Это выполняется классом java.io.ObjectOutputStream. Этот класс является фильтрующим потоком — это надстройка над низкоуровневым потоком байтов (называемым узловым потоком) для работы с протоколом сериализации для нас. Узловые потоки могут быть использованы для записи в файловую систему или даже в сокет. Это означает, что мы можем легко передавать сохраняемый объект по сетевому кабелю и заново создавать его на другой машине!

Посмотрите на код, используемый для сохранения объекта PersistentTime:

10 import java.io.ObjectOutputStream;
20 import java.io.FileOutputStream;
30 import java.io.IOException;
40 public class FlattenTime
50 {
60 public static void main(String [] args)
70 {
80 String filename = "time.ser";
90 if(args.length > 0)
100     {
110       filename = args[0];
120     }
130     PersistentTime time = new PersistentTime();
140     FileOutputStream fos = null;
150     ObjectOutputStream out = null;
160     try
170     {
180       fos = new FileOutputStream(filename);
190       out = new ObjectOutputStream(fos);
200       out.writeObject(time);
210       out.close();
220     }
230     catch(IOException ex)
240     {
250       ex.printStackTrace();
260     }
270   }
280 }

Реальная работа происходит на строке 200 когда мы вызываем метод ObjectOutputStream.writeObject(), который запускает механизм сериализации и объект сохраняется (в данном случае в файл).

Для восстановления из файла мы можем использовать следующий код :

10 import java.io.ObjectInputStream;
20 import java.io.FileInputStream;
30 import java.io.IOException;
40 import java.util.Calendar;
50 public class InflateTime
60 {
70 public static void main(String [] args)
80 {
90 String filename = "time.ser";
100     if(args.length > 0)
110     {
120       filename = args[0];
130     }
140   PersistentTime time = null;
150   FileInputStream fis = null;
160   ObjectInputStream in = null;
170   try
180   {
190     fis = new FileInputStream(filename);
200     in = new ObjectInputStream(fis);
210     time = (PersistentTime)in.readObject();
220     in.close();
230   }
240   catch(IOException ex)
250   {
260     ex.printStackTrace();
270   }
280   catch(ClassNotFoundException ex)
290   {
300     ex.printStackTrace();
310   }
320   // напечатаем сохраненное время
330   System.out.println("Flattened time: " + time.getTime());
340   System.out.println();
350      // напечатаем текущее время
360   System.out.println("Current time: " + Calendar.getInstance().getTime());
370 }
380}

В коде выше, восстановление объекта происходит в строке 210 путем вызова метода ObjectInputStream.readObject(). Вызов метода считывает байты, которые мы предварительно сохранили, и создает объект, являющийся точной копией оригинала. Так как readObject() может прочитать любой сериализуемый объект, необходимо приведение к корректному типу. Это означает, что файл класса должен быть доступен из системы, в которой происходит восстановление . Другими словами, файл класса объекта и методы не сохраняются; сохраняется только состояние объекта.

Позже, в строке 360, мы просто вызываем метод getTime() для получения времени, которое сохранил оригинальный объект. Зафиксированное время сравнивается с текущим для демонстрации того, что механизм сериализации работает так как ожидалось.

Несериализуемые объекты

Базовый механизм сериализации в Java прост в использовании, но здесь есть еще несколько вещей, которые нужно знать. Как упоминалось ранее, только объекты, реализующие Serializable могут быть сохранены. Класс java.lang.Object не реализовывает этот интерфейс. Поэтому, не все объекты в Java могут быть сохранены автоматически. Хорошей новостью является то, что большинство из них — такие как AWT и Swing GUI компоненты, строки и массивы — сериализуемы.

С другой стороны, некоторые системные классы, такие как Thread, OutputStream и их подклассы, а также Socket несериализуемы. В действительности это не имеет никакого смысла. Например, поток, работающий в моей JVM может использовать память моей системы. Сохранение потока и попытка его запустить в вашей JVM не имеет смысла. Другим важным моментом в том, почему java.lang.Object не реализует интерфейс Serializable, является то, что любой создаваемый вами класс наследует только код Object (а не других сериализуемых классов), несериализуем до тех пор, пока вы не реализуете интерфейс самостоятельно (как сделано в предыдущем примере).

В этой ситуации присутствует проблема: что если мы имеем класс, который содержит экземпляр класса Thread? В этом случае, можем ли мы когда-либо сохранить объект этого типа? Ответ утвердителен, пока вы сообщаете механизму сериализации наши намерения маркируя наш объект класса Thread как transient.

Пусть мы хотим создать класс, производящий анимацию. Я на самом деле не приведу здесь код анимации. Вот используемый класс:

10 import java.io.Serializable;
20 public class PersistentAnimation implements Serializable, Runnable
30 {
40 transient private Thread animator;
50 private int animationSpeed;
60 public PersistentAnimation(int animationSpeed)
70 {
80 this.animationSpeed = animationSpeed;
90 animator = new Thread(this);
100     animator.start();
110   }
120       public void run()
130   {
140     while(true)
150     {
160       // выполнение анимации
170     }
180   }
 190 }

Когда мы создаем экземпляр класса PersistentAnimation, поток animator будет создан и запущен, как мы и ожидали. Мы пометили поток в строке 40 модификатором transient, чтобы сообщить механизму сериализации, что поле не будет сохраняться вместе с остальным состоянием объекта (в этом случае, это переменная speed). Вывод: вы должны отметить модификатором transient любое поле, которое не может быть сериализованым или любое поле, которое вы не хотите сериализовывать. Сериализация не обращет внимания на модификаторы доступа, такие как private — все долгоживущие поля рассматриваются как часть сохраняемого состояния объекта и допускаются к сохранению.

Поэтому мы можем добавить еще одно правило. Здесь оба правила относящиеся к сохраняемым объектам:

  • Правило 1: Чтобы быть сохраняемым объект должен реализовывать интерфейс Serializable или наследовать его реализацию из его иерархии объектов.
  • Правило 2: Сохраняемый объект должен отмечать все несериализуемые поля модификатором transient

Модификация стандартного протокола

Перейдем ко второму способу выполнения сериализации: модификации стандартного протокола. Хотя код анимации выше демонстрирует, как нить может быть включена в объект, и при этом объект остается сериализуемым, существует серьезная проблема, если мы рассмотрим, как Java создает объекты. К сведению, когда мы создаем объект с помощью ключевого слова new, конструктор объекта вызывается только тогда, когда создается новый экземпляр класса. Сохраняя этот базовый факт в памяти, вернемся к нашему коду анимации. Сначала, мы создаем объект типа PersistentAnimation, который запускает нить анимации. Далее, мы сериализуем объект с помощью кода:

PersistentAnimation animation = new PersistentAnimation(10);
FileOutputStream fos = ...
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(animation);

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

Но есть и хорошие новости. Мы можем заставить наш объект работать так, как мы хотим; мы можем сделать рестарт анимации при восстановлении объекта. Чтобы сделать это, мы можем, например, создать вспомогательный метод startAnimation(), который делает то, что должен делать конструктор. Мы можем затем вызвать этот метод из конструктора, после чего мы считываем объект. Неплохо, но это приводит к увеличению сложности. Теперь, кто-то, кто хочет использовать этот объект анимации, будет знать какой метод нужно вызвать после обычного процесса десериализации. Это не делает механизм цельным, хотя Java API сериализации обещает это разработчикам.

Однако здесь имеется странное хитрое рещение. Используя встроенные возможности механизма сериализации, разработчики могут усовершенствовать обычный процесс, предоставляя два метода в их файлах классов. Этими методами являются:

Оба этих метода объявлены (и должны быть объявлены) private, проверьте, что эти методы не уналедованы и не переопределены или не перегружены. Фокус в том, что виртуальная машина автоматически проверит, объявлены ли эти методы в течении вызова соответствующего метода. Виртуальная машина может вызывать private методы вашего класса когда она хочет, но другие объекты нет. Таким образом, целостность класса сохраняется и протокол сериализации может продолжить работу, как обычно. Протокол сериализации всегда используется тем же способом, путем вызова любого метода: ObjectOutputStream.writeObject() или ObjectInputStream.readObject(). Итак, хотя эти специализированные private методы предоставлены, сериализация объекта работает также по отношению к любому вызываемому объекту.

Принимая все это во внимание, посмотрим на исправленную версию PersistentAnimation который содержит эти private методы для предоставления нам контроля над процессом десериализаци, давая нам псевдоконструктор:

10 import java.io.Serializable;
20 public class PersistentAnimation implements Serializable, Runnable
30 {
40 transient private Thread animator;
50 private int animationSpeed;
60 public PersistentAnimation(int animationSpeed)
70 {
80 this.animationSpeed = animationSpeed;
90 startAnimation();
100   }
110       public void run()
120   {
130     while(true)
140     {
150       // do animation here
160     }
170   }
180   private void writeObject(ObjectOutputStream out) throws IOException
190   {
200     out.defaultWriteObject();
220   }
230   private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
240   {
250     // our "pseudo-constructor"
260     in.defaultReadObject();
270     // now we are a "live" object again, so let's run rebuild and start
280     startAnimation();
290
300   }
310   private void startAnimation()
320   {
330     animator = new Thread(this);
340     animator.start();
350   }
360 }

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

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

Прекратить эту сериализацию!

Хорошо, мы увидели простоту процесса сериализации, сейчас увидим несколько больше. Что если вы создаете класс, чей класс-предок сериализуем, но вы не хотите чтобы новый класс был сериализуемым? Вы не можете убрать реализацию интерфейса, следовательно если ваш класс-предок реализовал Serializable, ваш новый класс реализовывает его тоже (объединение двух правил, приведенных выше). Для прекращения автоматической сериализации, вы снова однажды можете использовать private методы для генерации исключения NotSerializableException. Здесь показано, как это можно сделать:

10 private void writeObject(ObjectOutputStream out) throws IOException
20 {
30 throw new NotSerializableException("Не сегодня!");
40 }
50 private void readObject(ObjectInputStream in) throws IOException
60 {
70 throw new NotSerializableException("Не сегодня!");
80 }

Любая попытка записи или чтения этого объекта всегда будет генерировать исключение. Запомните, так как эти методы объявлены как private, никто не может изменить ваш код без наличия доступных исходных текстов — Java не допускает перекрытие этих методов.

Создание вашего собственного протокола: интерфейс Externalizable

Наше обсуждение было бы не полным без упоминания третьей возможности для сериализации: создания вашего собственного протокола с помощью интерфейса Externalizable. Вместо реализации интерфейса Serializable, вы можете реализовать Externalizable, который содержит два метода:

Переопределите эти методы для предоставления вашего протокола. В отличие от предыдущих двух вариантов сериализации, здесь ничего не предоставляется. Следовательно, протокол полностью в ваших руках. Несмотря на то, что это более сложный сценарий, он также наиболее контролируемый. Пример ситуации для альтернативного типа сериализации: чтение и запись PDF файлов в Java приложении. Если вы знаете как писать и читать PDF файлы (необходимую последовательность байтов), вы можете предоставить PDF-специфичный протокол в методах writeExternal и readExternal.

Однако, как и ранее, здесь нет различия в том, какая реализация Externalizable используется классом. Только вызовите writeObject() или readObject и, вуаля, эти методы интерфейса Externalizable будут вызваны автоматически.

Сложности

Имеются некоторые вещи в протоколе сериализации, которые могут выглядеть очень странно для неподготовленных разработчиков. Конечно, цель этой статьи — подготовить вас! Итак, поговорим о некоторых затруднениях и посмотрим, почему они существуют и как их обрабатывать.

Кэширование объектов в потоке

Сначала рассмотрим ситуацию в которой объект записан в поток и затем записывается снова. По умолчанию, ObjectOutputStream будет сохранять ссылку на объект, записанный в него. Это значит, что если состояние записанного объекта записано и затем записано снова, тогда новое состояние объекта не будет сохранено! Здесь кусок кода, показывающий эту проблему в действии:

10 ObjectOutputStream out = new ObjectOutputStream(...);
20 MyObject obj = new MyObject(); //должен реализовывать интерфейс Serializable
30 obj.setState(100);
40 out.writeObject(obj); // сохраняет объект с состоянием = 100
50 obj.setState(200);
60 out.writeObject(obj); // не сохраняет новое состояние объекта

Имеется два пути для контроля этой ситуации. Первый, вы можете всегда закрывать поток после записи, гарантируя, что каждый раз записывается новый объект. Второй, вы можете вызвать метод ObjectOutputStream.reset(), который скажет потоку освободить кэш ссылок, которые он содержит, и по новому запросу на запись она будет действительно производиться. Будте внимательны, метод reset сбрасывает весь кэш объектов, значит все записанные объекты могут быть перезаписанны.

Контроль версий

Второе затруднение. Представьте, что вы создали класс, создали его экземпляр, и записали его в поток объектов. Этот сохраненный объект некоторое время находится в файловой системе. Тем временем, вы обновили файл класса, возможно добавили новое поле. Что произойдет, когда вы попытаетесь прочитать сохраненный объект?

Плохой новостью является то, что будет вызвано исключение — java.io.InvalidClassException — так как все классы с возможностью сохраненияe автоматически получают уникальный идентификатор. Если идентификатор класса не совпадает с идентификатором сохраненного объекта, генерируется исключительная ситуация. Однако, если вы задумаетесь, почему будет вызвано исключение только из-за того, что я добавил поле? Почему полю не может быть присвоено значение по умолчанию и потом записано в следующий раз?

Да, это возможно, но потребуются небольшие манипуляции с кодом. Идентификатор, являющийся частью всех классов содержится в поле serialVersionUID. Если вы хотите контролировать версионность, вы просто подставляете поле serialVersionUID вручную и обеспечиваете ее постоянство при изменениях, вносимых в класс. Вы можете использовать утилиту serialver, поставляемую с дистрибутивом JDK, для просмотра кода по-умолчанию (по-умолчанию это хэш-код объекта).

Пример использования serialver с классом с именем Baz:

> serialver Baz
> Baz: static final long 
serialVersionUID = 10275539472837495L;

Просто скопируйте возвращенную строку с идентификатором версии и вставьте ее в ваш код. (В Windows, вы можете запустить эту утилиту с параметром -show для упрощения процедуры копирования и вставки.) Теперь, если вы вносите любые изменения в класс Baz, удостоверьтесь что указан тот же идентификатор версии и все будет в порядке.

Контроль версий работает отлично пока изменения совместимы. Совместимым изменением является добавление или удаление метода или поля. Несовместимыим изменениями являются изменение иерархии объектов или удаление реализации интерфейса Serializable. Полный список совместимых и несовместимых изменений приведен в спцификации Java сериализации (смотри Ресурсы).

Рассмотрение производительности

Наше третье затруднение: стандартный механизм, несмотря на простоту использования, не лучший исполнитель. Я записывал объект Date в файл 1000 раз, повторив эту процедуру 100 раз. Среднее время записи объекта Date было 115 милисекунд. Затем я записал вручную объект Date, используя стандартные способы ввода/вывода и то же количество итераций; среднее время было 52 милисекунды. Почти половина времени! Здесь часто возникает противоречие между удобством и производительностью, и сериализация не доказывает обратного. Если в первую очередь принимать во внимание скорость для вашего приложения, вы можете подумать о создании своего протокола.

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

Заключение

Сериализация в Java проста в изучении и почти также проста в реализации. Понимание трех разных путей реализации сериализации помогает придать гибкость вашему будущему API. Мы достаточно полно рассмотрели механизм сериализации в этой статье, и я надеюсь, это поможет вам лучше разбираться в нем. В заключение, как всегда в кодировании, необходимо руководствоваться здравым смыслом в рамках знакомства с API. Эта статья дает основу понимания Java API сериализации, но я рекомендую использовать спецификацию для знакомства с его тонкостями.

Об авторе

Todd Greanier, технический директор ComTech Training, преподавал и разрабатывал на Java с момента ее представления общественности. Эксперт в распределенных Java технологиях, он преподавал классы в широком кругу областей, включая JDBC, RMI, CORBA, UML, Swing, сервлеты/JSP, безопасность, JavaBeans, Enterprise Java Beans, и мультипоточность. Также он ведет семинары для корпораций, направленные на их специфические нужды. Тодд живет в верхнем New York с его женой, Стэйси, и его котом, Бином.

Ресурсы

Reprinted with permission from the July 2000 edition of JavaWorld magazine. Copyright © ITworld.com, Inc., an IDG Communications company. View the original article at: http://www.javaworld.com/ javaworld/jw-07-2000/jw-0714-flatten.html

[an error occurred while processing this directive]
[an error occurred while processing this directive] Перевод на русский © Алексей Коштерек, 2000
< Вернуться на caйт :: Copyright © 1999 — 2010, IT • archiv.