Должен ли абстрактный класс иметь serialVersionUID
В Java, если класс реализует Serializable, но является абстрактным, должен ли он иметь serialVersionUID, давно объявленный, или подклассам требуется только это?
В этом случае действительно подразумевается, что все подклассы имеют дело с сериализацией, поскольку цель типа должна использоваться в вызовах RMI.
4 ответа
SerialVersionUID предоставляется для определения совместимости между десерализованным объектом и текущей версией класса. Как таковой, он не является действительно необходимым в первой версии класса или, в данном случае, в абстрактном базовом классе.У вас никогда не будет экземпляра этого абстрактного класса для сериализации / десериализации, поэтому для него не нужен serialVersionUID.
(Конечно, он генерирует предупреждение компилятора, от которого вы хотите избавиться, верно?)
Оказывается, комментарий Джеймса правильный. SerialVersionUID абстрактного базового класса действительно распространяется на подклассы. В свете этого вам нужен serialVersionUID в вашем базовом классе.
Код для тестирования:
import java.io.Serializable;
public abstract class Base implements Serializable {
private int x = 0;
private int y = 0;
private static final long serialVersionUID = 1L;
public String toString()
{
return "Base X: " + x + ", Base Y: " + y;
}
}
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Sub extends Base {
private int z = 0;
private static final long serialVersionUID = 1000L;
public String toString()
{
return super.toString() + ", Sub Z: " + z;
}
public static void main(String[] args)
{
Sub s1 = new Sub();
System.out.println( s1.toString() );
// Serialize the object and save it to a file
try {
FileOutputStream fout = new FileOutputStream("object.dat");
ObjectOutputStream oos = new ObjectOutputStream(fout);
oos.writeObject( s1 );
oos.close();
} catch (Exception e) {
e.printStackTrace();
}
Sub s2 = null;
// Load the file and deserialize the object
try {
FileInputStream fin = new FileInputStream("object.dat");
ObjectInputStream ois = new ObjectInputStream(fin);
s2 = (Sub) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println( s2.toString() );
}
}
Запустите main в Sub один раз, чтобы он создал и сохранил объект. Затем измените serialVersionUID в базовом классе, закомментируйте строки в main, которые сохраняют объект (чтобы он больше не сохранялся, вы просто хотите загрузить старый), и запустите его снова. Это приведет к исключению
java.io.InvalidClassException: Base; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
Да, в общем, по той же причине, по которой любой другой класс нуждается в серийном идентификаторе - чтобы избежать его создания. По сути, любой класс (не интерфейс), который реализует сериализуемый, должен определять идентификатор последовательной версии, иначе вы рискуете ошибками десериализации, если одна и та же компиляция.class отсутствует в JVM сервера и клиента.
Есть и другие варианты, если вы пытаетесь сделать что-то необычное. Я не уверен, что вы подразумеваете под "намерением подклассов...". Собираетесь ли вы написать собственные методы сериализации (например, writeObject, readObject)? Если так, то есть другие варианты для работы с суперклассом.
см.: http://java.sun.com/javase/6/docs/api/java/io/Serializable.html
HTH Том
Концептуально сериализованные данные выглядят так:
subClassData(className + version + fieldNames + fieldValues)
parentClassData(className + version + fieldNames + fieldValues)
... (up to the first parent, that implements Serializable)
Таким образом, при десериализации несоответствие версий любого из классов в иерархии приводит к сбою десериализации. Для интерфейсов ничего не сохраняется, поэтому нет необходимости указывать версию для них.
Ответ: да, вам нужно предоставить serialVersionUID
в базовом абстрактном классе. Даже если у него нет полей (className + version
хранятся, даже если нет полей).
Также обратите внимание на следующее:
- Если в классе нет поля, обнаруженного в сериализованных данных (удаленное поле), оно игнорируется.
- Если в классе есть поле, которого нет в сериализованных данных (новое поле), для него устанавливается значение 0/false/null (не по умолчанию, как можно было бы ожидать).
- если поле изменяет тип данных, десериализованное значение должно быть присвоено новому типу. Например, если у вас был
Object
поле сString
значение, изменяя тип поля наString
будет успешным, но меняется наInteger
не будет. Тем не менее, изменение поля изint
вlong
не будет работать, даже если вы можете назначитьint
значение дляlong
переменная. - Если подкласс больше не расширяет родительский класс, который он расширяет в сериализованных данных, он игнорируется (как в случае 1).
- Если подкласс теперь расширяет класс, которого нет в сериализованных данных, поля родительского класса восстанавливаются со значением 0/false/null (как в случае 2).
Простыми словами: вы можете изменить порядок полей, добавлять и удалять их, даже изменять иерархию классов. Вы не должны переименовывать поля или классы (это не сработает, но значения не будут десериализованы). Вы не можете изменить тип полей с примитивным типом, и вы можете изменить поля ссылочного типа, если новый тип может быть назначен из всех значений.
Примечание: если базовый класс не реализует Serializable и только подкласс делает, то поля из базового класса будут вести себя как transient
,
На самом деле, указывая на ссылку Тома, если отсутствует serialVersionID
фактически рассчитывается во время выполнения сериализации, т.е. не во время компиляции
Если сериализуемый класс явно не объявляет serialVersionUID, тогда среда выполнения сериализации вычислит значение serialVersionUID по умолчанию для этого класса на основе различных аспектов класса...
Это еще более усложняет использование разных версий JRE.