Вступление
В этой статье мы обсудим концепцию, связанную с сериализацией и
десериализацией в Java. В этой статье мы увидим, что serialVersionUID
на самом деле довольно прямолинейен и прост, хотя иногда это
рассматривается как « часть черной магии API сериализации
Java».
Во-первых, мы упростим сериализацию и десериализацию, чтобы вспомнить
некоторые важные идеи, которые нам понадобятся позже. После этого мы
углубимся в serialVersionUID
и покажем, что это такое и как работает.
В заключение мы покажем пример, который должен связать все вместе.
Сериализация и десериализация
Сериализация - это процесс сохранения состояния объекта, чтобы его можно было сохранить в базе данных, передать по сети, записать в файл и т. Д. Как именно работает сериализация, выходит за рамки этой статьи, но в целом - он работает путем преобразования объекта в поток байтов, который затем можно использовать как любой другой поток информации, например, передаваемый через сетевой сокет.
Десериализация - это процесс, противоположный сериализации. Он принимает представление объекта (например, из файла или сокета) в потоке байтов и преобразует его обратно в объект Java, который находится внутри JVM.
Прежде чем сериализацию или десериализацию можно будет выполнить для
объекта, необходимо, чтобы этот объект (то есть его класс) реализовал
интерфейс Serializable
Интерфейс Serializable
используется для
«пометки» классов, которые можно (де) сериализовать.
Без класса, реализующего этот интерфейс, невозможно ни сериализовать, ни десериализовать объекты из этого класса. По словам сериализуемого Javadoc :
«Сериализуемость класса обеспечивается классом, реализующим интерфейс java.io.Serializable *.
Что такое serialVersionUID?
Для правильной работы сериализации и десериализации каждый сериализуемый класс должен иметь связанный с ним номер версии - serialVersionUID . Цель этого значения - убедиться, что классы, используемые как отправителем (тот, который сериализует), так и получателем (тот, который десериализует) сериализованного объекта, совместимы друг с другом.
Если подумать, в этом есть большой смысл. Должен быть какой-то механизм определения, совпадает ли отправленный объект с полученным. В противном случае может случиться, например, что в класс объекта до его сериализации было внесено изменение, о котором получатель не знает.
После чтения объекта (т.е. десериализации) читатель может загрузить «новый» объект в «старое» представление. В лучшем случае это могло иметь неприятные последствия, а в худшем - полное нарушение бизнес-логики.
Именно по этой причине serialVersionUID
который обычно используется со
всеми сериализуемыми объектами. Он используется для проверки того, что
обе «версии» объекта (на стороне отправителя и получателя) совместимы,
то есть идентичны.
Если действительно необходимо обновить класс, это можно обозначить,
увеличив значение serialVersionUID
. Таким образом, сериализованная
версия будет иметь обновленный UID, который будет храниться вместе с
объектом и доставлен читателю.
Если у читателя нет последней версии класса, будет выброшено
InvalidClassException
Как сгенерировать serialVersionUID?
Согласно документации, каждое поле serialVersionUID должно быть static
, final
и long
. Модификатор доступа может быть произвольным, но
настоятельно рекомендуется, чтобы все объявления использовали
модификатор private
В этом случае модификатор будет применяться только к текущему классу, а
не к его подклассам, что является ожидаемым поведением; мы не хотим,
чтобы на класс влияло что-либо еще, кроме него самого. С учетом всего
сказанного, вот как может выглядеть serialVersionUID
private static final long serialVersionUID = 42L;
Ранее мы упоминали, что все сериализуемые классы должны реализовывать
интерфейс Serializable
Этот интерфейс предполагает, что все сериализуемые классы могут
объявлять serialVersionUID
, но не обязаны это делать. Если у класса
нет явно объявленного значения serialVersionUID, оно будет сгенерировано
средой выполнения сериализации.
Однако настоятельно рекомендуется, чтобы все сериализуемые классы явно объявляли значение
serialVersionUID
Это связано с тем, что по умолчанию serialVersionUID
является сложным
и, следовательно, чувствительным к очень незначительным различиям в
средах. Если в процессе сериализации-десериализации используются два
разных компилятора, во время десериализации InvalidClassException
поскольку классы, по-видимому, не будут совпадать, даже если они
содержат одно и то же содержимое, дословно.
Наконец, если в классе присутствуют какие-либо transient
или static
поля, они будут проигнорированы во время процесса сериализации и станут
null
после десериализации.
serialVersionUID Пример
Давайте определим класс, который мы будем использовать для сериализации
и десериализации. Конечно, он реализует Serializable
и мы начнем с
serialVersionUID
1
:
public class Spaceship implements Serializable {
private static final long serialVersionUID = 1L;
private Pilot pilot;
private Engine engine;
private Hyperdrive hyperdrive;
public void fly() {
System.out.println("We're about to fly high among the stars!");
}
// Constructor, Getters, Setters
}
Затем мы реализуем serializeObject()
который будет отвечать за
сериализацию объекта и запись его в файл .ser
public void serializeObject(Spaceship spaceship) {
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("./spaceship.ser")
);
out.writeObject(spaceship);
out.close();
}
Наш метод сериализует объект spaceship
в файл .ser
FileOutputStream
. Теперь этот файл содержит сериализованное
содержимое нашего объекта.
Теперь давайте реализуем .ser
deserializeObject()
, который
принимает этот файл .ser и создает из него объект:
public void deserializeObject(String filepath) {
Spaceship ship;
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(filepath)
);
ship = (Spaceship) in.readObject();
in.close();
ship.fly();
}
Назовем эти два и посмотрим на результат:
public class Main {
public static void main(String[] args) {
Spaceship spaceship = new Spaceship();
serializeObject(spaceship);
deserializeObject("./spaceship.ser");
}
}
Это приведет к:
We're about to fly high among the stars!
Наш deserializeObject()
загрузил сериализованный файл в JVM и успешно
преобразовал его в объект Spaceship
Чтобы продемонстрировать проблему, упомянутую ранее в отношении
управления версиями, давайте изменим значение serialVersionUID
с 1L
на 2L
в нашем классе Spaceship
После этого давайте изменим наш main()
чтобы снова прочитать файл, не
записывая его с измененным serialVersionUID
:
public class Main {
public static void main(String[] args) {
deserializeObject("./spaceship.ser");
}
}
Конечно, это приведет к:
Exception in thread "main" java.io.InvalidClassException ...
Как и ожидалось, причина исключения кроется в serialVersionUID
.
Поскольку мы не записали новые данные после обновления значения
serialVersionUID
2L
, сериализованный объект по-прежнему содержит
1L
качестве своего serialVersionUID
.
Однако метод deserializeObject()
ожидал, что это значение будет 2L
потому что это фактическое новое значение из экземпляра Spaceship
Из-за этого несоответствия между сохраненным и восстановленным
состоянием объекта " Spaceship
" было выброшено исключение.
Заключение
Сериализация и десериализация - мощные и распространенные методы,
используемые для хранения или передачи структур данных и объектов.
Иногда легко упустить из виду некоторые важные детали, такие как
serialVersionUID
, особенно с учетом того факта, что IDE обычно
генерируют его автоматически.
Надеюсь, теперь станет немного яснее, какова его цель и как ее правильно использовать в будущих проектах.