Все мы знаем, что интерфейс Serializable нужен для сериализации классов. Также этот интерфейс рекомендует вам использовать поле serialVersionUID. Но знаете ли вы, что даже если вы все сделаете согласно рекомендациям, ваш код все равно может не работать? Давайте определим дальнейшие изменения в ваших классах которые не сломают совместимость с предыдущими версиями, и те которые ее точно сломают.
Изменения ломающие совместимость
Это изменения не дающие сохранить взаимную совместимость разных версий классов. Ниже мы приведем список таких изменений, возникающих в процессе работы над кодом класса (предполагается реализация сериализации и десериализации по умолчанию):
  1. Удаление полей класса. Если поле класса удалено, его значение не будет содержаться в записываемом потоке. В случае когда эти данные будут прочитаны в более раннюю версию класса (где эти поля еще существуют), значение поля будет установлено по умолчанию. Вполне возможно что это вызовет проблемы в работе программы.
  2. Перемещение классов вверх или вниз в иерархии наследования. Это недопустимо, потому что данные в потоке будут идти в неверной последовательности.
  3. Назначить нестатическое поле статическим, или сериализуемое несериализуемым. Если мы полагаемся на сериализацию по умолчанию, это аналогично удалению поля из класса. Текущая версия класса не запишет значение поля в поток, следовательно если этот поток будет прочтен предыдущей версией класса, см. пункт 1.
  4. Изменение типа поля класса. Каждая версия класса записывает данные вместе с их типом. Попытка прочесть эти данные более ранней версией класса провалится потому что тип данных в потоке не соответствует текущему типу поля класса.
  5. Переопределение методов writeObject() или readObject(), с изменением способа чтения и записи полей класса. Предыдущая версия просто не сможет прочесть эти данные.
  6. Изменение того, какой интерфейс класс реализует, с Serializable на Externalizable и наоборот. Поток данных будет несовместим с более ранними версиями класса.
  7. Преобразование класса в перечисление и обратно. Поток будет содержать данные, несовместимые с предыдущей версией класса.
  8. Удаление Serializable или Externalizable из списка реализуемых интерфейсов. Потому что данный класс уже не будет иметь всех полей, существующих в более ранних версиях класса.
  9. Переопределение методов writeReplace() или readResolve(). Опять-таки, полученные в результате данные будут несовместимы с предыдущими версиями класса.
Изменения не ломающие совместимость
  1. Добавление новых полей класса. Если десериализуемый класс имеет поле отсутствующее в потоке данных, данное поле будет инициализировано значением по умолчанию. Если это вас по каким-то причинам не устраивает, можете переопределить метод readObject() в котором инициализировать любые поля как угодно.
  2. Добавление новых классов. Поток данных содержит информацию о иерархии наследования всех объектов в потоке. Это позволяет легко выявить классы которых там нет. Поскольку информация о том как инициализировать эти классы отсутствует, они будут инициализированы значениями по умолчанию.
  3. Удаление классов. Также несложно выявить какой класс был удален. Значения полей объектов этого класса все равно будут прочитаны из потока, нессылочные типы данных будут проигнорированы, а все объекты на которые ссылался более несуществующий класс все равно будут созданы, потому что далее в потоке будут идти соответствующие им данные. Сборщик мусора удалит эти объекты вместе с потоком.
  4. Добавление методов readObject()/writeObject(). Если версия класса читающая поток содержит эти методы, то предполагается что readObject() прочтет данные из потока сериализованные по умолчанию. Если вы планируете записывать/считывать некую дополнительную информацию, первым делом необходимо вызвать defaultReadObject() при чтении и defaultWriteObject() при записи чтобы считать/записать обязательный набор данных, а уже потом считывать/записывать то что вы хотите дополнительно.
  5. Удаление методов readObject()/writeObject(). Если класс читающий поток не содержит этих методов, то данные будут десериализованы способом по умолчанию, любые дополнительные данные будут проигнорированы.
  6. Добавление Serializable в список реализуемых интерфейсов. Это то же самое что и добавление классов. В потоке данных нет ничего для инициализации этого класса, так что все его поля будут инициализированы значениями по умолчанию. Чтобы наследовать наш класс от несериализуемого предка, этот предок должен иметь конструктор без аргументов, тогда поля нашего класса будут инициализированы значениями по умолчанию. Если конструктора без аргументов нет, будет вызвано исключение InvalidClassException.
  7. Изменение модификаторов доступа поля. Модификаторы доступа поля никак не влияют на сериализацию/десериализацию данного поля.
  8. Назначение статического поля нестатическим, или несереализуемого сериализуемым. Если мы полагаемся на сериализацию по умолчанию, данное изменение аналогично добавлению нового поля в класс. Новое поле будет записано в поток, но предыдущие версии класса при чтении его проигнорируют потому что сериализация не присваивает значения статическим и несериализуемым полям.
Рекоммендации для использования serialVersionUID
Поле serialVersionUID - универсальный идентификатор версии класса, реализующего интерфейс Serializable. Используется при десериализации, чтобы убедиться что загружаемые данные соответствуют объекту в который они загружаются. Если версии не совпадают, вызывается исключение InvalidClassException.
  1. Всегда создавайте это поле, к примеру - private static final long serialVersionUID = 7526472295622776147L; даже в первой версии класса, как напоминание о его важности.
  2. Не меняйте значение поля в следующих версиях, пока они совместимы с предыдущими. Для проверки совместимости смотрите вышеизложенное.
Рекомендации для использования readObject() и writeObject()
  1. При десериализации, как и в конструкторе, необходимо проверять состояние объекта при завершении десериализации. Это значит, что метод readObject() должен быть реализован почти всегда, как минимум для проведения этих проверок.
  2. Если конструктор создает новые объекты полей класса инициализируя их нужными значениями, также должен поступать и readObject().