Сериализация (Serialization) — это процесс, который переводит объект в последовательность байтов, по которой затем его можно полностью восстановить. Зачем это нужно? Дело в том, при обычном выполнении программы максимальный срок жизни любого объекта известен — от запуска программы до ее окончания. Сериализация позволяет расширить эти рамки и «дать жизнь» объекту так же между запусками программы.
Дополнительным бонусом ко всему является сохранение кроссплатформенности. Не важно какая у вас операционная система, сериализация переводит объект в поток байтов, который может быть восстановлен на любой ОС. Если вам необходимо передать объект по сети, вы можете сериализовать объект, сохранить его в файл и передать по сети получателю. Он сможет восстановить полученный объект. Так же сериализация позволяет осуществлять удаленный вызов методов (Java RMI), которые находятся на разных машинах с, возможно, разными операционными системами, и работать с ними так, словно они находятся на машине вызывающего java-процесса.
Реализовать механизм сериализации довольно просто. Необходимо, чтобы ваш класс реализовывал интерфейс Serializable. Это интерфейс — идентификатор, который не имеет методов, но он указывает jvm, что объекты этого класса могут быть сериализованы. Так как механизм сериализации связан с базовой системой ввода/вывода и переводит объект в поток байтов, для его выполнения необходимо создать выходной поток OutputStream, упаковать его в ObjectOutputStream и вызвать метод writeObject(). Для восстановления объекта нужно упаковать InputStream в ObjectInputStream и вызвать метод readObject().
В процессе сериализации вместе с сериализуемым объектом сохраняется его граф объектов. Т.е. все связанные с этим объекто, объекты других классов так же будут сериализованы вместе с ним.
Рассмотри пример сериализации объекта класса Person.
import java.io.*;
class Home implements Serializable {
private String home;
public Home(String home) {
this.home = home;
}
public String getHome() {
return home;
}
}
public class Person implements Serializable {
private String name;
private int countOfNiva;
private String fatherName;
private Home home;
public Person(String name, int countOfNiva, String fatherName, Home home) {
this.name = name;
this.countOfNiva = countOfNiva;
this.fatherName = fatherName;
this.home = home;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", countOfNiva=" + countOfNiva +
", fatherName='" + fatherName + '\'' +
", home=" + home +
'}';
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Home home = new Home("Vishnevaia 1");
Person igor = new Person("Igor", 2, "Raphael", home);
Person renat = new Person("Renat", 2, "Raphael", home);
//Сериализация в файл с помощью класса ObjectOutputStream
ObjectOutputStream objectOutputStream = new ObjectOutputStream(
new FileOutputStream("person.out"));
objectOutputStream.writeObject(igor);
objectOutputStream.writeObject(renat);
objectOutputStream.close();
// Востановление из файла с помощью класса ObjectInputStream
ObjectInputStream objectInputStream = new ObjectInputStream(
new FileInputStream("person.out"));
Person igorRestored = (Person) objectInputStream.readObject();
Person renatRestored = (Person) objectInputStream.readObject();
objectInputStream.close();
//Сериализация с помощью класса ByteArrayOutputStream
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream2 = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream2.writeObject(igor);
objectOutputStream2.writeObject(renat);
objectOutputStream2.flush();
//Восстановление с помощью класса ByteArrayInputStream
ObjectInputStream objectInputStream2 = new ObjectInputStream(
new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
Person igorRestoredFromByte = (Person) objectInputStream2.readObject();
Person renatRestoredFromByte = (Person) objectInputStream2.readObject();
objectInputStream2.close();
System.out.println("Before Serialize: " + "\n" + igor + "\n" + renat);
System.out.println("After Restored From Byte: " + "\n" + igorRestoredFromByte + "\n" + renatRestoredFromByte);
System.out.println("After Restored: " + "\n" + igorRestored + "\n" + renatRestored);
}
}
Вывод:
Before Serialize:
Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@355da254}
Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@355da254}
After Restored From Byte:
Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b}
Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b}
After Restored:
Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae}
Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae}
В данном примере класс Home создан для того чтобы продемонстрировать, что при сериализации объекта Person, с ним сериализуется и граф его объектов. Класс Home так же должен реализовывать интерфейс Serializable, иначе случится исключение java.io.NotSerializableException. Так же в примере описана сериализация с помощью класса ByteArrayOutputStream.
Из результатов выполнения программы можно сделать интересный вывод: при восстановлении объектов, у которых до сериализации была ссылка на один и тот же объект, этот объект будет восстановлен только один раз. Это видно по одинаковым ссылкам в объектах после восстановления:
After Restored From Byte:
Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b}
Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b}
After Restored:
Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae}
Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae}
Однако, так же видно, что при выполнении записи двумя потоками вывода (у нас это ObjectInputStream и ByteArrayOutputStream), объект home будет создан заново, несмотря на то, что он уже был создан до этого в одном из потоков. Мы видим это по разным адресам объектов home, полученных в двух потоках. Получается, что если выполнить сериализацию одним выходным поток, затем восстановить объект, то у нас есть гарантия восстановления полной сети объектов без лишних дубликатов. Конечно, в ходе выполнения программы состояние объектов может измениться, но это на совести программиста.
Проблема
Из примера так же видно, что при восстановлении объекта может возникнуть исключение ClassNotFoundException. С чем это связано? Дело в том, что мы легко можем сериализовать объект класса Person в файл, передать его по сети нашему товарищу, который может восстановить объект другим приложением, в котором класса Person попросту нет.
Своя сериализация. Как сделать?
Что делать, если вы хотите управлять сериализацией сами? Например, ваш объект хранит в себе логин и пароль пользователей. Вам необходимо сериализовать его для дальнейшей передачи его по сети. Передавать пароль в таком случае крайне ненадежно. Как решить эту задачу? Существует два способа. Первый, использовать ключевое слово transient. Второй, вместо реализации интереса Serializable использовать его расширение — интерфейс Externalizable. Рассмотрим примеры работы первого и второго способа для их сравнения.
Первый способ — Сериализация с использованием transient
import java.io.*;
public class Logon implements Serializable {
private String login;
private transient String password;
public Logon(String login, String password) {
this.login = login;
this.password = password;
}
@Override
public String toString() {
return "Logon{" +
"login='" + login + '\'' +
", password='" + password + '\'' +
'}';
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Logon igor = new Logon("IgorIvanovich", "Khoziain");
Logon renat = new Logon("Renat", "2500RUB");
System.out.println("Before: \n" + igor);
System.out.println(renat);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out"));
out.writeObject(igor);
out.writeObject(renat);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out"));
igor = (Logon) in.readObject();
renat = (Logon) in.readObject();
System.out.println("After: \n" + igor);
System.out.println(renat);
}
}
Вывод:
Before:
Logon{login='IgorIvanovich', password='Khoziain'}
Logon{login='Renat', password='2500RUB'}
After:
Logon{login='IgorIvanovich', password='null'}
Logon{login='Renat', password='null'}
Второй способ — Сериализация с реализацией интерфейса Externalizable
import java.io.*;
public class Logon implements Externalizable {
private String login;
private String password;
public Logon() {
}
public Logon(String login, String password) {
this.login = login;
this.password = password;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(login);
}
@Override
public String toString() {
return "Logon{" +
"login='" + login + '\'' +
", password='" + password + '\'' +
'}';
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
login = (String) in.readObject();
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Logon igor = new Logon("IgorIvanovich", "Khoziain");
Logon renat = new Logon("Renat", "2500RUB");
System.out.println("Before: \n" + igor);
System.out.println(renat);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out"));
out.writeObject(igor);
out.writeObject(renat);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out"));
igor = (Logon) in.readObject();
renat = (Logon) in.readObject();
System.out.println("After: \n" + igor);
System.out.println(renat);
}
}
Вывод:
Before:
Logon{login='IgorIvanovich', password='Khoziain'}
Logon{login='Renat', password='2500RUB'}
After:
Logon{login='IgorIvanovich', password='null'}
Logon{login='Renat', password='null'}
Первое отличие двух вариантов, которое бросается в глаза это размер кода. При реализации интерфейса Externalizable нам необходимо переопределить два метода: writeExternal() и readExternal(). В методе writeExternal() мы указываем какие поля будут сериализованы и как, в readExternal() как их прочитать. При использовании слова transient мы явно указываем, какое поле или поля не нужно сериализовывать. Так же заметим, что во втором способе мы явно создали конструктор по умолчанию, причем публичный. Зачем это сделано? Давайте попробуем запустить код без этого конструктора. И посмотрим на вывод:
Before:
Logon{login='IgorIvanovich', password='Khoziain'}
Logon{login='Renat', password='2500RUB'}
Exception in thread "main" java.io.InvalidClassException: Logon; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169)
at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2043)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at Logon.main(Logon.java:45)
Мы получили исключение java.io.InvalidClassException. С чем это связано? Если пройти по стек-трейсу можно выяснить, что в конструкторе класса ObjectStreamClass есть строчки:
if (externalizable) {
cons = getExternalizableConstructor(cl);
} else {
cons = getSerializableConstructor(cl);
Для интерфейса Externalizable будет вызван метод получения конструктора getExternalizableConstructor(), внутри которого мы через Reflection попробуем получить конструктор по умолчанию класса, для которого мы восстанавливаем объект. Если нам не удается его найти, или он не public, то мы получаем исключение. Обойти эту ситуацию можно следующим образом: не создавать явно никакого конструктора в классе и заполнять поля с помощью сеттеров и получать значение геттерами. Тогда при компиляции класса будет создан конструктор по умолчанию, который будет доступен для getExternalizableConstructor(). Для Serializable метод getSerializableConstructor() получает конструктор класса Object и от него ищет нужный класс, если не найдет, то получим исключение ClassNotFoundException. Выходит, что ключевое различие между Serializable и Externalizable в том, что первому не нужен конструктор для создания восстановления объекта. Он просто полностью восстановится из байтов. Для второго при восстановлении сначала будет создан объект с помощью конструктора в точке объявления, а затем в него будут записаны значения его полей из байтов, полученных при сериализации. Лично мне больше нравится первый способ, он гораздо проще. Причем, даже если нам нужно все таки задать поведение сериализации, мы можем не использовать Externalizable, а так же реализовать Serializable, добавив (не переопределив) в него методы writeObject() и readObject(). Но для того, чтобы они «работали» нужно точно соблюсти их сигнатуру.
import java.io.*;
public class Talda implements Serializable {
private String name;
private String description;
public Talda(String name, String description) {
this.name = name;
this.description = description;
}
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
System.out.println("Our writeObject");
}
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
System.out.println("Our readObject");
}
@Override
public String toString() {
return "Talda{" +
"name='" + name + '\'' +
", description='" + description + '\'' +
'}';
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Talda partizanka = new Talda("Partizanka", "Viiiski");
System.out.println("Before: \n" + partizanka);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Talda.out"));
out.writeObject(partizanka);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("Talda.out"));
partizanka = (Talda) in.readObject();
System.out.println("After: \n" + partizanka);
}
}
Вывод:
Before:
Talda{name='Partizanka', description='Viiiski'}
Our writeObject
Our readObject
After:
Talda{name='Partizanka', description='Viiiski'}
Внутри наших добавленных методов вызываются defaultWriteObject() и defaultReadObject(). Они отвечают за сериализацию по умолчанию, как если бы она работала без добавленных нами методов.
На самом деле это только верхушка айсберга, если продолжить углубляться в механизм сериализации, то с высокой доли вероятности, можно отыскать еще нюансы, найдя которые мы скажем: «Сериализация… не все так просто».
Комментарии (16)
shpi0
29.11.2018 20:05+1Сериализация через Serializable использует рефлексию, что не всегда хорошо, например под Андроид. Parcelable в статье не хватает для сравнения :)
c0f04
29.11.2018 22:07А можете подробнее объяснить, чем рефлексия под Андроидом будет плоха?
shpi0
30.11.2018 00:25Как минимум — рефлексия работает медленно, для андроид устройств, как известно, производительность критична.
Ну и второй момент, в случае обфускации кода, весь reflection, а значит и Serializable перестает работать.
Что касается Externalizable, он работает не через рефлексию, поэтому лишен этих недостатков, но все-таки проигрывает по производительности Parcelable.BigDflz
30.11.2018 07:30Как минимум — рефлексия работает медленно
это было справедливо лет 10++ назад.Ну и второй момент, в случае обфускации кода, весь reflection, а значит и Serializable перестает работать.
это надо знать, чтоб не наступить на грабли.
aleksandy
29.11.2018 23:51Externalizable нужен, если предполагается ручной вызов методов сериализациии. А чтобы иметь возможность вручную управлять сериализацией/десериализацией достаточно создать приватные методы readObject(ObjectInputStream)/writeObject(ObjectOutputStream) в Serializable-объекте.
BigDflz
как бы такое не красиво использовать, когда есть String.format
aleksandy
Есть подозрение, что это сгенерировано IDE.
LexB
Почему? Вроде такое читается легче, работает быстрее и вероятность ошибки ниже.
BigDflz
Если только компилятор преобразует в .append (строковые операции самые медленные в java) ваш код:
вывел
Logon{login='login', password='password'}
хотя у вас есть ошибки…
грамотнее написать так
ну и
LexB
Почему этот код стал моим?
Какая ошибка, какая грамота? Мы все еще о программировании говорим?Вы понимаете что если в паттерне формата ошибетесь, или тип переменной не тот будет, то ошибку получите не на момент компиляции а на момент исполнения? При определенной удаче вы не отловите ее на момент отладки и тестирования…
BigDflz
посмотрите внимательно — использование ограничителей строк = и двойные и одинарные кавычки.
пользуйтесь нормальным ide и ошибка будет на момент написания кода.
LexB
Так в чем ошибка-то? Кавычку не так поставили?
Idea уже ненормальная ide? Чем она мне поможет в кейсах ниже? Срочно на эклипс переходить?
BigDflz
смотри внимательно.
нравится Idea — используй, но и читай что в ссылках указывают.