一个抽象类是否应该具有serialVersionUID?

77
在Java中,如果一个类实现了Serializable接口但是是抽象类,它是否需要声明一个serialVersionUID长整型呢?还是只有子类需要声明?
在这种情况下,的确打算让所有子类处理序列化,因为该类型的目的是用于RMI调用。

3
我开始写回答,但后来意识到我并不完全知道,虽然我有点儿直觉。对于这个我无法回答的问题,给它点一个赞。 - Michael Myers
4个回答

56

serialVersionUID用于确定反序列化对象与当前类的版本之间的兼容性。在一个类的第一个版本或者在这种情况下,抽象基类中实际上并不是必需的。因为您永远不会拥有该抽象类的实例来进行序列化/反序列化,所以它不需要serialVersionUID。

(当然,它会生成一个编译器警告,你想要消除它,对吧?)

事实证明,james的评论是正确的。抽象基类的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() );
    }
}

运行 Sub 类中的 main 方法一次,以便它创建并保存一个对象。然后更改 Base 类中的 serialVersionUID,在 main 中注释掉保存对象的行(这样它就不会再次保存,你只是想加载旧的对象),然后再次运行。这将导致一个异常。

java.io.InvalidClassException: Base; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

1
@Ryan:谢谢,但我通常把警告当作错误来处理并直接解决它们。 - Bill the Lizard
1
但我明白并不是每个人都像我一样固执,所以感谢你的评论。 - Bill the Lizard
34
实际上,这是不正确的。在反序列化过程中,考虑了继承链中所有类的serialversionuid,因此抽象类缺少serialversionuid可能会存在问题。我曾经遇到过这个问题。 - james
3
它还应该存在于类的第一个版本中,因为使用不同的编译器重新编译它可能会产生不同的默认serialVersionUID。这样,即使没有代码更改,新编译的类的版本与旧版本不兼容。请查看注释http://java.sun.com/j2se/1.5.0/docs/guide/serialization/spec/class.html#4100 - Robin
我投票支持这个答案,我们有子类的具体例子。 抽象基类的serialVersionUID确实会传播到子类中, 但是从Java文档中可以看出, http://download.java.net/jdk8/docs/api/java/io/Serializable.html 您必须明确声明serialVersionUID。“还强烈建议显式serialVersionUID声明在可能的情况下使用private修饰符, 因为这样的声明仅适用于立即声明的类 - serialVersionUID字段作为继承成员没有用。” - vsingh
显示剩余4条评论

7

通常情况下,与其他类一样,任何其他类都需要一个序列化标识符 - 以避免为其生成标识符。基本上,任何实现可序列化的类(不是接口)都应该定义序列化版本 id,否则当相同的 .class 编译器不在服务器和客户端 JVM 中时,会出现反序列化错误。

如果您正在尝试做一些花哨的事情,还有其他选项。我不确定您所说的“子类的意图是什么”。您是否将编写自定义序列化方法(例如,writeObject、readObject)?如果是这样,处理超类的其他选项。

see: http://java.sun.com/javase/6/docs/api/java/io/Serializable.html

HTH Tom


5

在概念上,序列化的数据看起来像这样:

subClassData(className + version + fieldNames + fieldValues)
parentClassData(className + version + fieldNames + fieldValues)
... (up to the first parent, that implements Serializable)

当进行反序列化时,如果类层次结构中的任何一个类的版本不匹配,则反序列化操作将失败。接口没有存储,因此无需为它们指定版本。
所以答案是:是的,即使基础抽象类没有字段,您仍需要在其中提供serialVersionUID:className + version仍会被存储。
还请注意以下内容: 1. 如果某个类没有遇到在序列化数据中出现的字段(已删除的字段),则该字段将被忽略。 2. 如果某个类具有在序列化数据中不存在的字段(新字段),则它将被设置为0/false/null。它不会像人们期望的那样设置为默认值。 3. 如果字段类型发生更改,则反序列化值必须可分配给新类型。例如,如果您有一个带有String值的Object字段,将字段类型更改为String将成功,但将其更改为Integer则不行。但是,即使您可以将int值分配给long变量,将字段从int更改为long也行不通。 4. 如果子类不再扩展在序列化数据中扩展的父类,则该子类将被忽略(与情况1相同)。 5. 如果子类现在继承找不到的类,则将恢复父类字段并将其设置为0/false/null值(如情况2所示)。
简单来说,您可以重新排序字段,添加和删除它们,甚至更改类层次结构。您不应重命名字段或类(它不会失败,但将被处理为已删除并添加的字段)。您无法更改具有原始类型的字段的类型,可以更改引用类型字段,但提供的新类型必须可分配给所有序列化值。
注意:如果基类不实现Serializable,只有子类实现,则来自基类的字段将表现为transient。

3

实际上,如果Tom的链接缺少serialVersionID,则实际上是由序列化运行时计算的,即在编译期间不进行计算。

如果可序列化类没有显式声明serialVersionUID,则序列化运行时将根据类的各个方面计算该类的默认serialVersionUID值...

这使得事情变得更加复杂,因为JRE有不同的版本。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接