serialVersionUID в Java: совместимость версий классов - Вопросы
Всего: 5 вопросов
1. Что такое serialVersionUID в Java и как JVM проверяет версию класса при десериализации?
Что такое serialVersionUID в Java и как JVM проверяет версию класса при десериализации?
serialVersionUID — номер версии сериализуемого класса: поле private static final long serialVersionUID. Обычно static-поля не сериализуются, но этот номер попадает в поток — не как состояние объекта, а как часть описания класса (class descriptor), которое ObjectOutputStream пишет перед данными объекта.
При десериализации ObjectInputStream читает из потока описание класса (имя, serialVersionUID, поля с типами), находит класс с тем же именем в classpath и сравнивает номера:
- номера не совпали —
InvalidClassException: local class incompatible, поля даже не сравниваются; - номера совпали — поля сопоставляются по имени: поле, которое есть и в потоке, и в классе, получает значение из потока; поле только в классе — значение по умолчанию (
null,0,false); поле только в потоке пропускается; - если у одноимённого поля изменился тип, чтение всё равно завершается ошибкой.
Совпадающий номер — это обещание разработчика: «новая версия класса умеет читать данные старой». Он не гарантирует правильных значений: если поменялся смысл поля (например, price хранил рубли, а теперь копейки), данные прочитаются без ошибок, но неверно. Номер нужно менять, когда меняется смысл данных, а не только их структура.
2. Что будет, если не объявить serialVersionUID, и какое значение ему задать: 1L или сгенерированное?
Что будет, если не объявить serialVersionUID, и какое значение ему задать: 1L или сгенерированное?
Код скомпилируется, но JVM вычислит номер по умолчанию — хеш SHA-1 от структуры класса. В расчёт входят имя и модификаторы класса, имена интерфейсов, все поля, кроме private static и private transient, наличие статического блока инициализации, все не-private конструкторы и методы. Поэтому номер меняется от правок, которые не затрагивают данные: добавили public-метод, сделали класс final, добавили интерфейс — и старый файл уже не читается (InvalidClassException: local class incompatible). Кроме того, из-за синтетических членов вложенных и анонимных классов javac и компилятор Eclipse могут вычислить разные номера для одного исходника. Поэтому номер объявляют явно.
Значение может быть любым long (в том числе отрицательным): JVM проверяет только совпадение.
- Новый класс, который ещё не сериализовался, —
1L: читается как «версия 1», при несовместимом изменении её легко увеличить до2L. - Старый класс без
serialVersionUID, данные которого уже лежат в файлах или кэше, — тот номер, который JVM вычисляла раньше. Его берут из сообщенияInvalidClassException(значениеstream classdesc) или утилитойserialver -classpath . Cat, запущенной на старой версии класса; IntelliJ IDEA тоже умеет сгенерировать его быстрым исправлением. Если объявить здесь1L, вы сами сломаете чтение старых данных.
3. Почему при десериализации возникает InvalidClassException и как исправить каждую причину?
Почему при десериализации возникает InvalidClassException и как исправить каждую причину?
java.io.InvalidClassException — наследник IOException, который бросает ObjectInputStream, когда класс в classpath не подходит к данным в потоке. Причину подсказывает текст после имени класса:
local class incompatible: stream classdesc serialVersionUID = X, local class serialVersionUID = Y— номера версий разные: поле не объявлено и класс изменился, или номер поменяли вручную. Если изменения совместимы, объявите в классе старый номер X из сообщения.incompatible types for field age— у поля с тем же именем изменился примитивный тип (int→long) или примитив заменён ссылочным типом. Верните тип или добавьте новое поле с другим именем и перенесите значение вreadObject().class invalid for deserialization— класс больше не реализуетSerializable; вернитеimplements Serializable.Serializable incompatible with Externalizable— класс переведён с одного интерфейса на другой; старые данные так не прочитать, нужна отдельная миграция.no valid constructor— у ближайшего несериализуемого родителя нет доступного конструктора без параметров; добавьте его.
Смена ссылочного типа поля (например, String → StringBuilder) даёт другое исключение: ClassCastException: cannot assign instance of java.lang.String to field Cat.name of type java.lang.StringBuilder. Причина и исправление те же, что для incompatible types for field. А после переименования класса или переноса его в другой пакет чтение падает с ClassNotFoundException: в потоке записано полное имя класса.
4. Какие изменения класса совместимы с сериализацией и как изменить класс, не потеряв уже сохранённые данные?
Какие изменения класса совместимы с сериализацией и как изменить класс, не потеряв уже сохранённые данные?
По спецификации Java Object Serialization (глава 5.6) совместимы: добавление поля (оно получит значение по умолчанию, например null), добавление и удаление методов и конструкторов, смена модификатора доступа поля, снятие static/transient с поля, добавление и удаление writeObject()/readObject(), добавление класса в иерархию или удаление из неё. Всё это работает, только если serialVersionUID объявлен явно и не менялся.
Несовместимы: смена типа поля (int → long даёт InvalidClassException: incompatible types for field age даже при совпадающем номере), пометка поля static/transient, переименование поля, перенос класса в другой пакет, перемещение по иерархии, отказ от Serializable, переход на Externalizable. Удаление поля формально тоже несовместимо: новая версия читает старые данные, но старая версия, читающая новые данные, получит в этом поле значение по умолчанию.
Если новому полю не подходит значение по умолчанию, задайте его в readObject():
@Serial
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // стандартное чтение полей
if (color == null) {
color = "не указан";
}
}
Для несовместимых изменений: оставьте старое поле и добавьте новое (long ageInDays рядом с int age, заполнение в readObject()), переведите старые файлы программой со старой версией класса или сознательно увеличьте номер до 2L, если старые данные не нужны. Для долгоживущих данных надёжнее JSON (Jackson, Gson) или Protobuf.
5. Как работает serialVersionUID в иерархии классов, enum и record и что делать с предупреждением IDE «does not declare a static final serialVersionUID field»?
Как работает serialVersionUID в иерархии классов, enum и record и что делать с предупреждением IDE «does not declare a static final serialVersionUID field»?
- Иерархия.
serialVersionUIDне наследуется: JVM ищет поле только в самом классе. В поток пишутся описания класса объекта и всех его сериализуемых родителей, и каждый проверяется отдельно. ЕслиCat extends Animalи обаSerializable, объявите номер в каждом; смена номера вAnimalсломает чтение всех сохранённыхCat. - enum сериализуется по имени константы, его
serialVersionUIDвсегда0L; объявление JVM игнорирует. - record (Java 16+) десериализуется через канонический конструктор, проверка совпадения
serialVersionUIDдля него отключена — номер можно не объявлять.
Предупреждение чаще всего появляется на собственном исключении или классе Swing: Throwable и Component реализуют Serializable, значит, сериализуемы и все наследники. Eclipse пишет The serializable class MyException does not declare a static final serialVersionUID field of type long, javac с -Xlint:serial — serializable class MyException has no definition of serialVersionUID, в IntelliJ IDEA есть инспекция Serializable class without 'serialVersionUID' (по умолчанию выключена).
Если объекты класса никогда не сериализуются, предупреждение можно подавить через @SuppressWarnings("serial"). Но объявить @Serial private static final long serialVersionUID = 1L; почти так же коротко, и это защитит класс, если его позже начнут сериализовать (исключение передадут через RMI или сохранят в HTTP-сессии).