Класс Java.io.ObjectOutputStream
Класс Java.io.ObjectOutputStream записывает примитивные типы данных и графики объектов Java в OutputStream. Объекты могут быть прочитаны (восстановлены) с помощью ObjectInputStream.
Объявление класса
Ниже приводится объявление для класса Java.io.ObjectOutputStream:
public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants
Конструкторы классов
защищенный ObjectOutputStream ()
Это обеспечивает способ для подклассов, которые полностью переопределяют ObjectOutputStream, чтобы не выделять частные данные, только используемые этой реализацией ObjectOutputStream.
ObjectOutputStream (OutputStream out)
Это создает ObjectOutputStream, который записывает в указанный OutputStream.
защищенный ObjectOutputStream ()
Это обеспечивает способ для подклассов, которые полностью переопределяют ObjectOutputStream, чтобы не выделять частные данные, только используемые этой реализацией ObjectOutputStream.
ObjectOutputStream (OutputStream out)
Это создает ObjectOutputStream, который записывает в указанный OutputStream.
Методы класса
Sr.No. | Метод и описание |
---|---|
1 | Защищенный void annotateClass (Class cl) |
Подклассы могут реализовать этот метод, чтобы позволить данным класса быть сохраненными в потоке.
Подклассы могут реализовывать этот метод для хранения пользовательских данных в потоке вместе с дескрипторами для динамических прокси-классов.
Этот метод закрывает поток.
Этот метод записывает в этот поток нестатические и непереходные поля текущего класса.
Этот метод истощает любые буферизованные данные в ObjectOutputStream.
Этот метод позволяет потоку выполнять замену объектов в потоке.
Этот метод очищает поток.
Этот метод извлекает объект, используемый для буферизации постоянных полей для записи в поток.
Этот метод позволит доверенным подклассам ObjectOutputStream заменять один объект другим во время сериализации.
Этот метод сброса будет игнорировать состояние любых объектов, уже записанных в поток.
Этот метод указывает версию протокола потока для использования при записи потока.
Этот метод записывает массив байтов.
Этот метод записывает вложенный массив байтов.
Этот метод записывает байт.
Этот метод записывает логическое значение.
Этот метод записывает 8-битный байт.
Этот метод записывает строку в виде последовательности байтов.
Этот метод записывает 16-битный символ.
Этот метод записывает String как последовательность символов.
Этот метод записывает указанный дескриптор класса в ObjectOutputStream.
Этот метод записывает 64-битный дубль.
Этот метод записывает буферизованные поля в поток.
Этот метод записывает 32-разрядное число с плавающей точкой.
Этот метод записывает 32-битное int.
Этот метод записывает 64-битную длину.
Этот метод записывает указанный объект в ObjectOutputStream.
Этот метод используется подклассами для переопределения метода writeObject по умолчанию.
Этот метод записывает 16-битное сокращение.
Этот метод предоставляется, чтобы подклассы могли добавлять или добавлять свой собственный заголовок в поток.
Этот метод записывает «неразделенный» объект в ObjectOutputStream.
Подклассы могут реализовать этот метод, чтобы позволить данным класса быть сохраненными в потоке.
Подклассы могут реализовывать этот метод для хранения пользовательских данных в потоке вместе с дескрипторами для динамических прокси-классов.
Этот метод закрывает поток.
Этот метод записывает в этот поток нестатические и непереходные поля текущего класса.
Этот метод истощает любые буферизованные данные в ObjectOutputStream.
Этот метод позволяет потоку выполнять замену объектов в потоке.
Этот метод очищает поток.
Этот метод извлекает объект, используемый для буферизации постоянных полей для записи в поток.
Этот метод позволит доверенным подклассам ObjectOutputStream заменять один объект другим во время сериализации.
Этот метод сброса будет игнорировать состояние любых объектов, уже записанных в поток.
Этот метод указывает версию протокола потока для использования при записи потока.
Этот метод записывает массив байтов.
Этот метод записывает вложенный массив байтов.
Этот метод записывает байт.
Этот метод записывает логическое значение.
Этот метод записывает 8-битный байт.
Этот метод записывает строку в виде последовательности байтов.
Этот метод записывает 16-битный символ.
Этот метод записывает String как последовательность символов.
Этот метод записывает указанный дескриптор класса в ObjectOutputStream.
Этот метод записывает 64-битный дубль.
Этот метод записывает буферизованные поля в поток.
Этот метод записывает 32-разрядное число с плавающей точкой.
Этот метод записывает 32-битное int.
Этот метод записывает 64-битную длину.
Этот метод записывает указанный объект в ObjectOutputStream.
Этот метод используется подклассами для переопределения метода writeObject по умолчанию.
Этот метод записывает 16-битное сокращение.
Этот метод предоставляется, чтобы подклассы могли добавлять или добавлять свой собственный заголовок в поток.
Этот метод записывает «неразделенный» объект в ObjectOutputStream.
Методы унаследованы
Этот класс наследует методы от следующих классов –
Потоки вывода, OutputStream
Стандартная библиотека Java имеет весьма развитые средства вывода данных. Все возможности вывода данных сосредоточены в пакете java.io.
Существуют две параллельные иерархии классов вывода : OutputStream и Writer. Класс Writer введен в последних версиях Java.
В данной статье рассматривается вопрос использования потоков для вывода данных в файл. Иерархии выходных OutputStream потоков представлена на следующем рисунке.
Поток Stream— это абстрактное значение источника или приёмника данных, которые способны обрабатывать информацию. Есть два типа потоков: байтовые и символьные. В некоторых ситуациях символьные потоки более эффективны, чем байтовые. Классы, производные от классов OutputStream или Writer, имеют методы с именами write() для записи одиночных байтов или массива байтов (отвечают за вывод данных).
Выходной поток OutputStream
Класс OutputStream — это абстрактный класс, определяющий байтовый поток вывода. Наследники данного класса определяют куда направлять данные: в массив байтов, в файл или канал. Из массива байт можно создать текстовую строку String.
Методы класса OutputStream :
- void write(int b) записывает один байт в выходной поток. Аргумент этого метода имеет тип int, что позволяет вызывать write, передавая ему выражение, при этом не нужно выполнять приведение его типа к byte.
- void write(byte b[]) записывает в выходной поток весь указанный массив байтов.
- void write(byte b[], int off, int len) записывает в поток len байтов массива, начиная с элемента b[off].
- void flush() очищает любые выходные буферы, завершая операцию вывода.
- void close() закрывает выходной поток. Последующие попытки записи в этот поток будут возбуждать IOException.
Класс ByteArrayOutputStream
Класс ByteArrayOutputStream представляет поток вывода, использующий массив байтов в качестве места вывода. Чтобы создать объект данного класса, можно использовать один из его конструкторов :
ByteArrayOutputStream() ByteArrayOutputStream(int size)
Первый конструктор создает массив данных для хранения байтов длиной в 32 байта, а второй конструктор создает массив длиной size.
Примеры использования класса ByteArrayOutputStream :
import java.io.ByteArrayOutputStream; public class TestBOS < public static void main(String[] args) < ByteArrayOutputStream bos; bos = new ByteArrayOutputStream(); String text = "Hello World!"; byte[] buffer = text.getBytes(); try< bos.write(buffer); >catch(Exception e) < System.out.println(e.getMessage()); >// Преобразование массива байтов в строку System.out.println(bos.toString()); // Вывод в консоль по символьно byte[] array = bos.toByteArray(); for (byte b: array) < System.out.print((char)b); >System.out.println(); > >
В классе ByteArrayOutputStream метод write записывает в поток некоторые данные (массив байтов). Этот массив байтов записывается в объекте ByteArrayOutputStream в защищенное поле buf, которое представляет также массив байтов (protected byte[] buf). Так как метод write может вызвать исключение, то вызов этого метода помещается в блок try..catch.
Используя методы toString() и toByteArray(), можно получить массив байтов buf в виде текста или непосредственно в виде массива байт.
С помощью метода writeTo можно перенаправить массив байт в другой поток. Данный метод в качестве параметра принимает объект OutputStream, в который производится запись массива байт :
Для ByteArrayOutputStream не надо явным образом закрывать поток с помощью метода close.
Класс FileOutputStream
Класс FileOutputStream создаёт объект класса OutputStream, который можно использовать для записи байтов в файл. Это основной класс для работы с файлами. Создание нового объекта не зависит от того, существует ли заданный файл или нет. Если файл отсутствует, то будет создан новый файл. В случае попытки открытия файла, доступного только для чтения, будет вызвано исключение.
FileOutputStream имеет следующий конструкторы:
public FileOutputStream(File file) throws FileNotFoundException; public FileOutputStream(String name) throws FileNotFoundException; public FileOutputStream(String name, boolean append) throws FileNotFoundException;
Смысл конструкторов последнего понятен из их описания. Но имеется несколько нюансов :
- При открытии файла на запись, если файл не существует, то он будет создан.
- Если файл существует, то он будет полностью обновлен. Т.е. если открыть и сразу закрыть файл, то содержимое файла будет уничтожено; реальный файл на диске станет нулевой длины.
- Исключением для предыдущего правила является последний из конструкторов. Если третьему параметру append присвоить значение true, то можно будет дописывать в конец файла.
Какой-либо дополнительной функциональности по сравнению с базовым классом FileOutputStream не добавляет.
ByteArrayOutputStream bos = new ByteArrayOutputStream(); String text = «Hello Wolrd!»; byte[] buffer = text.getBytes(); try < bos.write(buffer); >catch(Exception e) < System.out.println(e.getMessage()); >try < FileOutputStream fos = new FileOutputStream("hello.txt"); bos.writeTo(fos); >catch(IOException e)
Класс BufferedOutputStream
Класс BufferedOutputStream создает буфер для потоков вывода. Этот буфер накапливает выводимые байты без постоянного обращения к устройству. И когда буфер заполнен, производится запись данных.
import java.io.*; . String text = «Hello world!»; // строка для записи FileOutputStream fos = new FileOutputStream(«file.txt»); try < BufferedOutputStream bos = new BufferedOutputStream(fos); // Переводим текст в байты byte[] buffer = text.getBytes(); bos.write(buffer, 0, buffer.length); >catch(IOException e)
Класс BufferedOutputStream в конструкторе принимает в качестве параметра объект OutputStream — в примере это файловый поток вывода FileOutputStream.
BufferedOutputStream не добавляет много новой функциональности, он просто оптимизирует действие потока выводаи его следует использовать для организации более эффективного буферизованного вывода в поток.
Класс DataOutputStream
Класс DataOutputStream позволяет писать данные в поток через интерфейс DataOutput, который определяет методы, преобразующие элементарные значения в форму последовательности байтов. Такие потоки облегчают сохранение в файле двоичных данных.
Для записи каждого из примитивных типов предназначен свой метод класса DataOutputStream:
- writeByte(int value) — записывает в поток 1 байт
- writeChar(int value) — записывает 2х-байтовое значение char
- writeInt(int value) — записывает в поток целочисленное значение int
- writeShort(int v) — записывает в поток значение short
- writeFloat(float value) — записывает в поток 4-байтовое значение float
- writeDouble(double value) — записывает в поток 8-байтовое значение double
- writeBoolean(boolean value) — записывает в поток булевое однобайтовое значение
- writeLong(long value) — записывает в поток значение long
- writeUTF(String value) — записывает в поток строку в кодировке UTF-8
import java.io.*; . FileOutputStream fos = new FileOutputStream(«c://data.bin»); // запись в файл try (DataOutputStream dos = new DataOutputStream(fos)) < // записываем значения dos.writeUTF("Киса Воробьянинов"); dos.writeInt(30); dos.writeDouble(20.58); dos.writeBoolean(falss); System.out.println("Запись в файл выполнена"); >catch(IOException e)
Класс PrintStream
PrintStream является именно тем классом, который используется для вывода информации в консоль. Когда мы с помощью вызова System.out.println() пишем в консоль некоторую информацию, то тем самым используется PrintStream, так как переменная out класса System представляет объект класса PrintStream, а метод println() — это метод класса PrintStream.
Но PrintStream можно использовать для записи информации в поток вывода. Например, запишем информацию в файл:
import java.io.*; . String text = «Hello, World!»; // строка для записи FileOutputStream fos = new FileOutputStream(«C:/data.txt»); try < PrintStream printStream = new PrintStream(fos)); printStream.println(text); System.out.println("Запись в файл выполнена"); >catch(IOException e)
В данном примере используется конструктор PrintStream, который в качестве параметра принимает поток вывода FileOutputStream. Можно было бы также использовать конструктор с указанием названия файла для записи: PrintStream (String filename).
С помощью метода println() производится запись информации в выходной поток — то есть в объект FileOutputStream. В случае с выводом на консоль с помощью System.out.println() в качестве потока вывода выступает консоль.
Для вывода информации в выходной поток PrintStream использует следующие методы:
println(): вывод строковой информации с переводом строки print(): вывод строковой информации без перевода строки printf(): форматированный вывод
Следующий код показывает возможности использования форматированного вывода класса PrintStream :
int i = 15; printStream.printf("Квадрат числа %d равен %d \n", i, i*i);
Класс ObjectOutputStream
Класс ObjectOutputStream используется для сериализации объектов в поток. Сериализация представляет процесс записи состояния объекта в поток, соответственно процесс извлечения или восстановления состояния объекта из потока называется десериализацией. Сериализация очень удобна, когда идет работа со сложными объектами.
Для создания объекта ObjectOutputStream необходимо в конструктор передать поток, в который будет производится запись объектов.
Для записи данных ObjectOutputStream использует ряд методов, среди которых можно выделить следующие:
Метод | Описание |
---|---|
void close() | закрывает поток |
void flush() | сбрасывает содержимое буфера в выходной поток и очищает его |
void write(byte[] buf) | записывает в поток массив байтов |
void write(int val) | записывает в поток один младший байт из val |
void writeBoolean(boolean val) | записывает в поток значение boolean |
void writeByte(int val) | записывает в поток один младший байт из val |
void writeChar(int val) | записывает в поток значение типа char, представленное целочисленным значением |
void writeDouble(double val) | записывает в поток значение типа double |
void writeFloat(float val) | записывает в поток значение типа float |
void writeInt(int val) | записывает целочисленное значение |
void writeLong(long val) | записывает значение типа long |
void writeShort(int val) | записывает значение типа short |
void writeUTF(String str) | записывает в поток строку в кодировке UTF-8 |
void writeObject(Object obj) | записывает в поток отдельный объект |
Представленные методы охватывают весь спектр данных, которые можно сериализовать.
Пример использования класса ObjectOutputStream :
import java.io.*; class Person implements Serializable < private static final long serialVersionUID = 1L; public String name ; public int age ; public double height ; public boolean married; Person(String name,int age,double height,boolean married) < this.name = name; this.age = age; this.height = height; this.married = married; >> public class Example < public static void main(String[] args) < FileOutputStream fos; fos = new FileOutputStream("c:/data/persons.dat"); try < ObjectOutputStream oos; Person person; oos = new ObjectOutputStream(fos); person = new Person("Остап Бендер",35,175,false); oos.writeObject (person); >catch(Exception e) < System.out.println(e.getMessage()); >> >
Необходимо принимать во внимание, что сериализовать можно только те объекты, которые реализуют интерфейс Serializable.
Класс PipedOutputStream
Пакет java.io содержит класс PipedOutputStream, который может быть подключен к PipedInputStream, используемый для установления связи между двумя каналами. Данные в PipedOutputStream передаются в потоке Thread, который отправляет их в подключенный PipedInputStream, где данные также читаются, но в другом потоке.
То есть, класс PipedOutputStream предназначен для передачи информации между программами через каналы (pipes).
Наиболее часто используемые методы класса PipedOutputStream :
- void write(int b) — запись байта в канал
- void write(byte[] bytes, int off, int len) — запись определенного количества len байт начиная со смещения off массив bytes
- connect(PipedInputStream pis) — установление связи в каналом ввода pis
- close() — закрытие канала
- flush() — сброс данных в канал
Все методы класса могут вызвать исключение IOException.
Пример использования класса PipedOutputStream :
import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; public class Example < public static void main(String[] args) < PipedOutputStream pos = new PipedOutputStream(); PipedInputStream pis = new PipedInputStream(); byte[] bytes = "Hello, World!".getBytes(); try < // Установление связи между "трубами" pos.connect(pis); // Запись данных в PipedOutputStream for (int i = 0; i < bytes.length; i++) pos.write(bytes[i]); // Чтение данных из PipedInputStream int c; while((c = pis.read() ) != -1) < System.out.print((char) c); >> catch (IOException ioe) < System.out.println(ioe); >> >
Что делает метод reset в классе objectoutputstream
ObjectOutputStream пишет, что примитивные типы данных и графики Java возражают против OutputStream. Объекты могут быть считаны (воссозданное) использование ObjectInputStream. Персистентное хранение объектов может быть выполнено при использовании файла для потока. Если поток является сетевым потоком сокета, объекты могут быть воссозданы на другом узле или в другом процессе. Только объекты, которые поддерживают java.io. Сериализуемый интерфейс может быть записан потокам. Класс каждого сериализуемого объекта кодируется включая имя класса и подпись класса, значения полей объекта и массивов, и закрытия любых других объектов, на которые ссылаются от начальных объектов. Метод writeObject используется, чтобы записать объект в поток. Любой объект, включая Строки и массивы, пишется с writeObject. Многократные объекты или примитивы могут быть записаны потоку. Объекты должны быть считаны назад из соответствующего ObjectInputstream с теми же самыми типами и в том же самом порядке, как они были записаны. Примитивные типы данных могут также быть записаны потоку, используя соответствующие методы от DataOutput. Строки могут также быть записаны, используя writeUTF метод. Механизм сериализации по умолчанию для объекта пишет класс объекта, подписи класса, и значений всех непереходных и нестатических полей. Ссылки на другие объекты (кроме в переходных или статических полях) заставляют те объекты быть записанными также. Многократные ссылки на единственный объект кодируются, используя ссылочный механизм разделения доступа так, чтобы графики объектов могли быть восстановлены той же самой форме как тогда, когда оригинал был записан. Например, чтобы записать объект, который может быть считан примером в ObjectInputStream:
FileOutputStream fos = new FileOutputStream("t.tmp"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeInt(12345); oos.writeObject("Today"); oos.writeObject(new Date()); oos.close();
Классы, которые требуют специальной обработки во время сериализации и процесса десериализации, должны реализовать специальные методы с этими точными подписями:
private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException; private void writeObject(java.io.ObjectOutputStream stream) throws IOException private void readObjectNoData() throws ObjectStreamException;
writeObject метод ответственен за запись состояния объекта для его определенного класса так, чтобы соответствующий readObject метод мог восстановить это. Метод не должен интересоваться состоянием, принадлежащим суперклассам объекта или подклассам. Государство сохраняется при записи отдельных полей в ObjectOutputStream, используя writeObject метод или при использовании методов для примитивных типов данных, поддерживаемых DataOutput. Сериализация не выписывает поля любого объекта, который не реализует java.io. Сериализуемый интерфейс. Подклассы Объектов, которые не сериализуемы, могут быть сериализуемыми. В этом случае у несериализуемого класса должен быть конструктор без аргументов, чтобы позволить его полям быть инициализированными. В этом случае это — обязанность подкласса сохранить и восстановить состояние несериализуемого класса. Это часто имеет место, что поля того класса доступны (общественность, пакет, или защищенный) или что есть, получают и устанавливают методы, которые могут использоваться, чтобы восстановить состояние. Сериализация объекта может быть предотвращена, реализовывая writeObject и readObject методы, которые бросают NotSerializableException. Исключение будет поймано ObjectOutputStream и прервет процесс сериализации. Реализация интерфейса Externalizable позволяет объекту принять полный контроль над содержанием и форматом сериализированной формы объекта. Методы интерфейса Externalizable, writeExternal и readExternal, вызывают, чтобы сохранить и восстановить состояние объектов. Когда реализовано классом они могут записать и считать свое собственное состояние, используя все методы ObjectOutput и ObjectInput. Это — ответственность объектов обработать любое управление версиями, которое происходит. Перечислимые константы сериализируются по-другому чем обычный сериализуемый или объекты externalizable. Сериализированная форма перечислимой константы состоит исключительно из ее имени; значения полей константы не передаются. Чтобы сериализировать перечислимую константу, ObjectOutputStream пишет строку, возвращенную методом имени константы. Как другое сериализуемое или объекты externalizable, перечислимые константы могут функционировать как цели обратных ссылок, появляющихся впоследствии в потоке сериализации. Процесс, которым сериализируются перечислимые константы, не может быть настроен; любой специфичный для класса writeObject и writeReplace методы, определенные перечислимыми типами, игнорируются во время сериализации. Точно так же любой serialPersistentFields или serialVersionUID полевые объявления также игнорируются — у всех перечислимых типов есть фиксированный serialVersionUID 0L. Примитивные данные, исключая сериализуемые поля и externalizable данные, пишутся ObjectOutputStream в блочных записях данных. Блочная запись данных составляется из заголовка и данных. Блочный заголовок данных состоит из маркера и числа байтов, чтобы следовать за заголовком. Последовательные примитивные записи данных объединяются в одну блочную запись данных. Число записей в блоке, используемое для блочной записи данных, составит 1024 байта. Каждая блочная запись данных будет заполнена до 1024 байтов, или будет записана всякий раз, когда есть завершение блочного режима данных. Звонки в методы ObjectOutputStream writeObject, defaultWriteObject и writeFields первоначально завершают любую существующую блочную запись данных.
Вложенная Сводка Класса
Модификатор и Тип | Класс и Описание |
---|---|
static class | ObjectOutputStream. PutField |
Обеспечьте программируемый доступ к персистентным полям, которые будут записаны ObjectOutput.
Полевая Сводка
Поля, наследованные от интерфейса java.io. ObjectStreamConstants
Сводка конструктора
Модификатор | Конструктор и Описание |
---|---|
protected | ObjectOutputStream() |
Что делает метод reset в классе objectoutputstream
Java Serialization API используется множеством других Java API (например, RMI и JavaBeans) для сохранения объектов за пределами жизненного цикла виртуальной машины. Вы также можете самостоятельно использовать Java Serialization API для сохранения объектов в собственных целях. Несмотря на простоту основ сериализации Java, в использовании API существуют некоторые сложные моменты. В этой статье Тодд Гриньер (Todd Greanier) откроет вам секреты использования Java Serialization API.
Все мы знаем о том, что Java позволяет создавать в памяти объекты для многократного использования. Однако все эти объекты существуют лишь до тех пор, пока выполняется создавшая их виртуальная машина. Было бы неплохо, если бы создаваемые нами объекты могли бы существовать и вне пределов жизненного цикла виртуальной машины, не так ли? Что же, используя сериализацию объектов вы можете разложить свои объекты на байты и затем использовать их наиболее эффективным образом.
Сериализация объектов — это процесс сохранения состояния объектов в виде последовательности байтов, а также процесс восстановления в дальнейшем из этих байтов «живых» объектов. Java Serialization 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 является лишь маркерным интерфейсом — он позволяет механизму сериализации определить, может ли данный класс быть сохранен. Итак, мы можем сформулировать первое правило сериализации:
Правило №1: Сохраняемый объект должен реализовать интерфейс Serializable или унаследовать эту реализацию от вышестоящего по иерархии объекта.
Следующим шагом является, собственно, сохранение объекта. Оно выполняется при помощи класса java.io.ObjectOutputStream. Этот класс является фильтрующим потоком (filter stream) — он окружает низкоуровневый поток байтов (называемый узловым потоком (node stream)) и предоставляет нам поток сериализации. Узловые потоки могут быть использованы для записи в файловую систему, или даже в сокеты. Это означает, что мы с легкостью можем передавать разложенные на байты объекты по сети и затем восстанавливать их на других компьютерах!
Давайте взглянем на код, используемый для сохранения объекта 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("Время разложения: " + time.getTime()); 340 System.out.println(); 350 // распечатать текущее время 360 System.out.println("Текущее время: " + Calendar.getInstance().getTime()); 370 > 380>
В этом коде в 210-й строке происходит восстановление объекта при помощи вызова метода ObjectInputStream.readObject(). Метод считывает последовательность байтов, которую мы перед этим сохранили в файле, и создает «живой» объект, полностью повторяющий оригинал. Поскольку readObject() может считывать любой сериализуемый объект, необходимо его присвоение соответствующему типу. Таким образом, из системы, в которой происходит восстановление объекта, должен быть доступен файл класса. Другими словами, при сериализации не сохраняется ни файл класса объекта, ни его методы, сохраняется лишь состояние объекта.
Затем, в 360-й строке, мы просто вызываем метод getTime(), чтобы получить время у разложенного объекта. Время разложенного объекта сравнивается с текущим временем, дабы показать что механизм действительно работает так, как мы ожидаем.
Несериализуемые объекты
Основной механизм сериализации объектов Java прост для применения, но есть еще кое-что, что вам необходимо знать. Как упоминалось ранее, сохраняться могут лишь объекты, помеченные как Serializable. Класс java.lang.Object не реализует этот интерфейс, поэтому не все объекты Java могут быть автоматически сохранены. Хорошая новость заключается в том, что большая часть из них, включая AWT и компоненты Swing GUI, строки и массивы — сериализуемые.
В то же время, некоторые системные классы, такие как Thread, OutputStream и его подклассы, и Socket — не сериализуемые На самом деле даже если бы они были сериализуемыми, ничего бы не изменилось. К примеру, поток, запущенный в моей виртуальной машине, использует системную память. Его сохранение и последующее восстановление в вашей виртуальной машине ни к чему не приведет. Другой важный момент, вытекающий из того, что 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 Serialization API обещает разработчикам.
Однако, существует другое странное и хитрое решение. Используя встроенную возможность механизма сериализации, разработчики могут реализовать нормальный процесс поместив в свои файлы классов два метода:
- private void writeObject(ObjectOutputStream out) throws IOException;
- private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
Обратите внимание, что оба метода (совершенно справедливо), объявлены как 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 // наш "псевдо-конструктор" 260 in.defaultReadObject(); 270 // теперь мы вновь получили "живой" объект, поэтому давайте перестроим и запустим его 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 методы могут использоваться для внесения любого рода изменений в процесс сериализации. Например, для вывода объектов в поток может быть использована шифровка, а для ввода — дешифровка (при записи и чтении байтов данные записываются даже без применения технологии запутывания (obfuscation)). Методы могут использоваться также для сохранения в потоке дополнительных данных, например кода версии. Ваши возможности поистине не ограничены.
Остановите сериализацию!
О’кей, мы уже кое-что узнали о процессе сериализации, а теперь давайте двигаться дальше. Что если вы создали класс, чей суперкласс сериализуемый, но при этом вы не хотите чтобы ваш класс был сериализуемым? Вы не можете «разреализовать» интерфейс, поэтому если суперкласс реализует 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
Любая попытка записать или прочитать этот объект теперь приведет к возникновению исключительной ситуации. Запомните, если методы объявлены как private, никто не сможет модифицировать ваш код не изменяя исходный код класса. Java не позволяет переопределять такие методы.
Создание своего собственного протокола: интерфейс Externalizable
Наше обсуждение было бы неполным без упоминания третьей возможности сериализации: создания собственного протокола с интерфейсом Externalizable. Вместо реализации интерфейса Serializable, вы можете реализовать интерфейс Externalizable, который содержит два метода:
- public void writeExternal(ObjectOutput out) throws IOException;
- public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
Для создания собственного протокола нужно просто переопределить эти два метода. В отличие от двух рассмотренных ранее вариантов сериализации, здесь ничего не делается автоматически. Протокол полностью в ваших руках. Хотя это и наболее сложный способ, при этом он наиболее контролируемый. Возьмем, к примеру, ситуацию с альтернативным типом сериализации: запись и чтение PDF файлов Java приложением. Если вы знаете как читать и записывать PDF файлы (требуется определенная последовательность байт), вы можете создать протокол с учетом специфики PDF используя методы writeExternal и readExternal.
Так же, как и в рассмотренных случаях, нет никакой разницы в том, как используется класс, реализующий Externalizable. Вы просто вызываете методы writeObject() или readObject() и, вуаля, эти расширяемые методы будут вызываться автоматически.
Нюансы
Существует несколько моментов, имеющих отношение к протоколу сериализации, которые могут показаться очень странными для непосвященного разработчика. Разумеется, цель этой статьи посвятить вас в них! Поэтому давайте обсудим некоторые нюансы, попробуем понять почему они появились и как с ними обращаться.
Кэширование объектов в потоке
Во-первых, рассмотрим ситуацию, когда объект однажды уже записанный в поток, спустя какое-то время записывается в него снова. По умолчанию, 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, потому что всем классам, которые могут быть сохранены, присваивается уникальный идентификатор. Если идентификатор класса не совпадает с идентификатором разложенного объекта, возникает исключительная ситуация. Однако, если задуматься над этим, зачем нужны все эти исключительные ситуации, если вы всего лишь добавили новое поле? Разве нельзя установить в поле значение по умолчанию, а после сохранено?
Да, но это потребует легких манипуляций с кодом. Идентификатор, который является частью всех классов, хранится в поле, которое называется serialVersionUID. Если вы хотите контролировать версии, вы должны вручную задать поле serialVersionUID и убедиться в том, что оно такое же, и не зависит от изменений, внесенных вами в объект. Вы можете использовать утилиту, входящую в состав JDK, которая называется serialver, чтобы посмотреть какой код будет присвоен по умолчанию (это просто hash код объекта по умолчанию).
Вот пример использования serialver с классом Baz:
> serialver Baz > Baz: static final long serialVersionUID = 10275539472837495L;
Просто скопируйте возвращенную строку с идентификатором версии и поместите ее в ваш код. (В Windows вы можете запустить эту утилиту с опцией -show, чтобы упростить процедуру копирования и вставки). Теперь, если вы внесли какие-либо изменения в файл класса Baz, просто убедитесь что указан тот же идентификатор версии и все будет в порядке.
Контроль за версией прекрасно работает до тех пор, пока вносимые изменения совместимы. К совместимым изменениям относят добавление и удаление методов и полей. К несовместимым изменениям относят изменение иерархии объектов или прекращение реализации интерфейса Serializable. Полный перечень совместимых и несовместимых изменений приведен в Спецификации сериализации Java (см. Ссылки).
Обсуждение производительности
И третий момент. Механизм, используемый по умолчанию, несмотря на свою простоту, далеко не самый производительный. Я выполнил запись объект Data в файл 1’000 раз и повторил эту процедуру 100 раз. Среднее время записи объекта Data было 115 миллисекунд. Затем я вручную записал объект Data, используя стандартные инструменты ввода/вывода и повторил эту же операцию. Среднее время — 52 миллисекунды. Почти вдвое меньше! Это весьма обычный компромисс между простотой и производительностью, и сериализация — не исключение. Если скорость имеет для вашего приложения ключевое значение, вы можете выбрать метод создания собственного протокола.
Отдельного обсуждения заслуживает вышеупомянутый факт, что ссылки на объекты кэшируются в поток вывода. Согласно этому, система не может выполнять сбор мусора для записанных в поток объектов если поток не был закрыт. Лучшее решение (как всегда при помощи операций ввода/вывода) — это как можно скорее закрывать потоки после выполнения записи.
Заключение
Сериализация в Java проста для понимания и почти настолько же проста в реализации. Знание трех различных способов реализации сериализации должно помочь вам привязке API к вашей задаче. В этой статье вы познакомились с множеством механизмов сериализации и, я надеюсь, это пролило свет на эти вопросы, а не запутало вас еще больше. И, после прочтения, как это бывает в программировании, у вас появилось такое чувство, что вы давно знакомы с этим API. В статье изложены основы API Java сериализации, но перед использованием спецификации я рекомендую получше изучить ее во всех подробностях
Об авторе
Тодд Гриньер (Todd Greanier), технический директор ComTech Training приступил к обучению и разработке на языке Java сразу же, как только тот был представлен широкой общественности. Являясь экспертом по распределенным Java технологиям, он проводил обучение работе с классами для широчайшего спектра тематик, включая JDBC, RMI, CORBA, UML, Swing, сервлеты/JSP, безопасность, JavaBeans, Enterprise Java Beans и многопоточность. Он также организовывал специальные семинары для корпораций с учетом специфики их требований. Тодд живет на севере шатата Нью-Йорк вместе с женой Стэйси (Stacey) и кошкой Бин (Bean).
Ссылки
- Прочитайте Спецификацию сериализации Java объектов для того чтобы глубже овладеть секретами протокола: http://java.sun.com/products/jdk/1.2/docs/guide/serialization/spec/serialTOC.doc.html
- «Java ввод/вывод (Java I/O)», Элиота Расти Харольда (Elliotte Rusty Harold) (O’Reilly & Associates, 1999) также предлагает глубокое изучение Java Serialization API: http://www.amazon.com/exec/obidos/ASIN/1565924851/qid=962052367/104-9228407-2216730