Сериализация Java позволяет записывать объекты Java в файловые системы для постоянного хранения или в сеть для передачи в другие приложения. Сериализация в Java достигается с помощью интерфейса Serializable. Интерфейс Java Serializable гарантирует возможность сериализации объектов. Этот интерфейс рекомендует также использовать serialVersionUID.
Теперь, даже если вы используете оба в классах приложений, знаете ли вы, что может сломать ваш дизайн даже сейчас?? Давайте определим будущие изменения в классе, которые будут совместимыми изменениями, и другие, которые окажутся несовместимыми изменениями.
1. Несовместимые изменения
Несовместимые изменения классов — это те изменения, для которых гарантия взаимодействия не может быть сохранена. Несовместимые изменения, которые могут возникнуть при развитии класса, приведены ниже(с учетом сериализации или десериализации по умолчанию):
- Удаление полей – Если поле удалено в классе, записанный поток не будет содержать его значения. Когда поток считывается более ранним классом, значение поля будет установлено на значение по умолчанию, поскольку в потоке нет доступного значения. Однако это значение по умолчанию может отрицательно сказаться на способности более ранней версии выполнять свой контракт.
- Перемещение классов вверх или вниз по иерархии — это недопустимо, поскольку данные в потоке появляются в неправильной последовательности.
- Изменение нестатического поля на статическое или непереходного поля на переходное – При использовании сериализации по умолчанию это изменение эквивалентно удалению поля из класса. Эта версия класса не будет записывать эти данные в поток, поэтому они не будут доступны для чтения более ранними версиями класса. Как и при удалении поля, поле более ранней версии будет инициализировано значением по умолчанию, что может привести к сбою класса неожиданным образом.
- Изменение объявленного типа примитивного поля – Каждая версия класса записывает данные с объявленным типом. Более ранние версии класса, пытающиеся прочитать поле, потерпят неудачу, поскольку тип данных в потоке не соответствует типу поля.
- Изменение метода writeObject или readObject таким образом, чтобы он больше не писал и не читал данные поля по умолчанию или изменение его таким образом, чтобы он пытался записать или прочитать его, когда предыдущая версия этого не делала. Данные поля по умолчанию должны последовательно либо появляться, либо не появляться в потоке.
- Изменение класса с Serializable на Externalizable или наоборот является несовместимым изменением, поскольку поток будет содержать данные, несовместимые с реализацией доступного класса.
- Изменение класса с неперечисляемого типа на перечисляемый тип или наоборот, поскольку поток будет содержать данные, несовместимые с реализацией доступного класса.
- Удаление Serializable или Externalizable является несовместимым изменением, поскольку после написания оно больше не будет предоставлять поля, необходимые более старым версиям класса.
- Добавление метода writeReplace или readResolve к классу несовместимо, если такое поведение приведет к созданию объекта, несовместимого с любой более старой версией класса.
2. Совместимые изменения
- Добавление полей – Когда восстанавливаемый класс имеет поле, которое не встречается в потоке, это поле в объекте будет инициализировано значением по умолчанию для его типа. Если требуется инициализация, специфичная для класса, класс может предоставить метод readObject() для инициализации поля нестандартными значениями.
- Добавление классов – Поток будет содержать иерархию типов каждого объекта в потоке. Сравнение этой иерархии в потоке с текущим классом может обнаружить дополнительные классы. Поскольку в потоке нет информации, из которой можно инициализировать объект, поля класса будут инициализированы значениями по умолчанию.
- Удаление классов – сравнение иерархии классов в потоке с иерархией текущего класса может обнаружить, что класс был удален. В этом случае поля и объекты, соответствующие этому классу, считываются из потока. Примитивные поля отбрасываются, но объекты, на которые ссылается удаленный класс, создаются, поскольку они могут быть упомянуты позже в потоке. Они будут удалены сборщиком мусора, когда поток будет удален или сброшен.
- Добавление методов writeObject/readObject – Если версия, считывающая поток, имеет эти методы, то readObject, как обычно, должен прочитать требуемые данные, записанные в поток сериализацией по умолчанию. Он должен сначала вызвать defaultReadObject перед чтением любых дополнительных данных. Ожидается, что метод writeObject, как обычно, вызовет defaultWriteObject для записи требуемых данных, а затем может записать дополнительные данные.
- Удаление методов writeObject/readObject. Если класс, считывающий поток, не имеет этих методов, требуемые данные будут считаны с помощью сериализации по умолчанию, а необязательные данные будут отброшены.
- Добавление java.io.Serializable – это эквивалентно добавлению типов. В потоке не будет значений для этого класса, поэтому его поля будут инициализированы значениями по умолчанию. Поддержка подклассов несериализуемых классов требует, чтобы супертип класса имел конструктор без аргументов, а сам класс был инициализирован значениями по умолчанию. Если конструктор без аргументов недоступен, выдается исключение InvalidClassException.
- Изменение доступа к полю. Модификаторы доступа public, package, protected и private не влияют на способность сериализации присваивать значения полям.
- Изменение поля со статического на нестатическое или из переходного в непереходное – При использовании сериализации по умолчанию для вычисления сериализуемых полей это изменение эквивалентно добавлению поля в класс. Новое поле будет записано в поток, но более ранние классы проигнорируют значение, поскольку сериализация не будет назначать значения статическим илипереходным полям.
3. SerialVersionUID
serialVersionUID — это универсальный идентификатор версии для класса Serializable. Десериализация использует этот номер, чтобы гарантировать, что загруженный класс точно соответствует сериализованному объекту. Если совпадений не найдено, выдается исключение InvalidClassException.
- Всегда включайте его как поле, например: «private static final long serialVersionUID = 7526472295622776147L;» включайте это поле даже в первую версию класса, как напоминание о его важности.
- Не изменяйте значение этого поля в будущих версиях, если только вы сознательно не вносите изменения в класс, которые сделают его несовместимым со старыми сериализованными объектами. При необходимости следуйте приведенным выше рекомендациям.
4. Методы readObject() и writeObject()
- Десериализацию следует рассматривать как любой конструктор: проверять состояние объекта в конце десериализации — это подразумевает, что readObject() почти всегда должен быть реализован в классах Serializable, чтобы выполнялась эта проверка.
- Если конструкторы создают защитные копии для изменяемых полей объекта, то же самое должен делать и readObject().
5. Лучшие практики
- Используйте тег @serial из javadoc для обозначения сериализуемых полей.
- Расширение .ser традиционно используется для файлов, представляющих сериализованные объекты.
- Никакие статические или временные поля не подвергаются сериализации по умолчанию.
- Расширяемые классы не должны быть сериализуемыми, если в этом нет необходимости.
- Внутренние классы редко, если вообще когда-либо, должны реализовывать Serializable.
- Классы контейнеров обычно должны следовать стилю Hashtable, который реализует Serializable путем хранения ключей и значений, в отличие от большой структуры данных хэш-таблицы.
6. Пример реализации
import java.io.Serializable;import java.text.StringCharacterIterator;import java.util.*;import java.io.*;public final class UserDetails implements Serializable {/*** This constructor requires all fields** @param aFirstName* contains only letters, spaces, and apostrophes.* @param aLastName* contains only letters, spaces, and apostrophes.* @param aAccountNumber* is non-negative.* @param aDateOpened* has a non-negative number of milliseconds.*/public UserDetails(String aFirstName, String aLastName, int aAccountNumber,Date aDateOpened){super();setFirstName(aFirstName);setLastName(aLastName);setAccountNumber(aAccountNumber);setDateOpened(aDateOpened);// there is no need here to call verifyUserDetails.}// The default constructorpublic UserDetails() {this("FirstName", "LastName", 0, new Date(System.currentTimeMillis()));}public final String getFirstName() {return fFirstName;}public final String getLastName() {return fLastName;}public final int getAccountNumber() {return fAccountNumber;}/*** Returns a defensive copy of the field so that no one can change this* field.*/public final Date getDateOpened() {return new Date(fDateOpened.getTime());}/*** Names must contain only letters, spaces, and apostrophes. Validate before* setting field to new value.** @throws IllegalArgumentException* if the new value is not acceptable.*/public final void setFirstName(String aNewFirstName) {verifyNameProperty(aNewFirstName);fFirstName = aNewFirstName;}/*** Names must contain only letters, spaces, and apostrophes. Validate before* setting field to new value.** @throws IllegalArgumentException* if the new value is not acceptable.*/public final void setLastName(String aNewLastName) {verifyNameProperty(aNewLastName);fLastName = aNewLastName;}/*** Validate before setting field to new value.** @throws IllegalArgumentException* if the new value is not acceptable.*/public final void setAccountNumber(int aNewAccountNumber) {validateAccountNumber(aNewAccountNumber);fAccountNumber = aNewAccountNumber;}public final void setDateOpened(Date aNewDate) {// make a defensive copy of the mutable date objectDate newDate = new Date(aNewDate.getTime());validateAccountOpenDate(newDate);fDateOpened = newDate;}/*** The client's first name.** @serial*/private String fFirstName;/*** The client's last name.** @serial*/private String fLastName;/*** The client's account number.** @serial*/private int fAccountNumber;/*** The date the account was opened.** @serial*/private Date fDateOpened;/*** Determines if a de-serialized file is compatible with this class.* Included here as a reminder of its importance.*/private static final long serialVersionUID = 7526471155622776147L;/*** Verify that all fields of this object take permissible values** @throws IllegalArgumentException* if any field takes an unpermitted value.*/private void verifyUserDetails() {validateAccountNumber(fAccountNumber);verifyNameProperty(fFirstName);verifyNameProperty(fLastName);validateAccountOpenDate(fDateOpened);}/*** Ensure names contain only letters, spaces, and apostrophes.** @throws IllegalArgumentException* if field takes an unpermitted value.*/private void verifyNameProperty(String aName) {boolean nameHasContent =(aName != null) &&(!aName.equals(""));if(!nameHasContent) {throw new IllegalArgumentException("Names must be non-null and non-empty.");}StringCharacterIterator iterator = new StringCharacterIterator(aName);char character = iterator.current();while(character != StringCharacterIterator.DONE) {boolean isValidChar =(Character.isLetter(character)|| Character.isSpaceChar(character) || character == ''');if(isValidChar) {// do nothing} else {String message = "Names can contain only letters, spaces, and apostrophes.";throw new IllegalArgumentException(message);}character = iterator.next();}}/*** AccountNumber must be non-negative.** @throws IllegalArgumentException* if field takes an unpermitted value.*/private void validateAccountNumber(int aAccountNumber) {if(aAccountNumber < 0) {String message = "Account Number must be greater than or equal to 0.";throw new IllegalArgumentException(message);}}/*** DateOpened must be after 1970.** @throws IllegalArgumentException* if field takes an unpermitted value.*/private void validateAccountOpenDate(Date aDateOpened) {if(aDateOpened.getTime() < 0) {throw new IllegalArgumentException("Date Opened must be after 1970.");}}/*** Always treat deserialization as a full-blown constructor, by validating* the final state of the de-serialized object.*/private void readObject(ObjectInputStream aInputStream)throws ClassNotFoundException, IOException {// always perform the default deserialization firstaInputStream.defaultReadObject();// make defensive copy of the mutable Date fieldfDateOpened = new Date(fDateOpened.getTime());// ensure that object state has not been corrupted or tampered with// malicious codeverifyUserDetails();}/*** This is the default implementation of writeObject. Customise if* necessary.*/private void writeObject(ObjectOutputStream aOutputStream)throws IOException {// perform the default serialization for all non-transient, non-static// fieldsaOutputStream.defaultWriteObject();}}
Давайте теперь посмотрим, как выполнить сериализацию и десериализацию в Java.
7. Демонстрация
import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.util.Calendar;import java.util.Date;public class TestUserDetails {public static void main(String[] args) {// Create new UserDetails objectUserDetails myDetails = new UserDetails("Lokesh", "Gupta", 102825,new Date(Calendar.getInstance().getTimeInMillis()));// Serialization codetry {FileOutputStream fileOut = new FileOutputStream("userDetails.ser");ObjectOutputStream out = new ObjectOutputStream(fileOut);out.writeObject(myDetails);out.close();fileOut.close();} catch(IOException i) {i.printStackTrace();}// deserialization code@SuppressWarnings("unused")UserDetails deserializedUserDetails = null;try {FileInputStream fileIn = new FileInputStream("userDetails.ser");ObjectInputStream in = new ObjectInputStream(fileIn);deserializedUserDetails =(UserDetails) in.readObject();in.close();fileIn.close();// verify the object stateSystem.out.println(deserializedUserDetails.getFirstName());System.out.println(deserializedUserDetails.getLastName());System.out.println(deserializedUserDetails.getAccountNumber());System.out.println(deserializedUserDetails.getDateOpened());} catch(IOException ioe) {ioe.printStackTrace();} catch(ClassNotFoundException cnfe) {cnfe.printStackTrace();}}}
Вывод программы:
LokeshGupta102825Wed Nov 21 15:06:34 GMT+05:30 2012
Ссылки: http://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html