当缺少serialVersionUID时,Eclipse会发出警告。

可序列化类Foo未声明静态finallong类型的serialVersionUID字段

什么是serialVersionUID,为什么它很重要?请显示缺少serialVersionUID将导致问题的示例。


当前回答

每次序列化对象时,对象都会标记对象类的版本ID号。此ID称为serialVersionUID,它是根据有关类结构的信息计算的。假设您创建了一个Employee类,它的版本id为#333(由JVM分配),现在当您将序列化该类的对象(假设Employees对象)时,JVM将为其分配UID为#333。

考虑一种情况——将来您需要编辑或更改类,在这种情况下,当您修改它时,JVM将为它分配一个新的UID(假设#444)。现在,当您尝试反序列化雇员对象时,JVM会将序列化对象(雇员对象)的版本ID(#333)与类的版本ID进行比较,即#444(自更改以来)。相比之下,JVM将发现两个版本UID不同,因此反序列化将失败。因此,如果每个类的serialVersionID由程序员自己定义。即使类在未来演变,它也将是相同的,因此JVM将始终发现类与序列化对象兼容,即使类已更改。有关更多信息,请参阅HEAD FIRST JAVA的第14章。

其他回答

首先我需要解释什么是序列化。

序列化允许将对象转换为流,以便通过网络发送该对象或将其保存到文件或保存到DB中以供字母使用。

序列化有一些规则。

只有当对象的类或其超类实现serializable接口时,对象才可串行化对象是可序列化的(它本身实现了可序列化接口),即使它的超类不是。但是,可序列化类层次结构中的第一个超类(未实现可序列化接口)必须具有无参数构造函数。如果违反了这一点,readObject()将在运行时生成java.io.InvalidClassException所有基元类型都是可序列化的。瞬态字段(带有瞬态修饰符)未序列化(即未保存或还原)。实现Serializable的类必须标记不支持序列化的类的临时字段(例如,文件流)。静态字段(带有静态修饰符)未序列化。

当Object被序列化时,Java Runtime将序列版本号(即serialVersionID)关联起来。

我们需要serialVersionID的地方:

在反序列化期间,验证发送方和接收方在序列化方面是否兼容。如果接收方使用不同的serialVersionID加载了类,则反序列化将以InvalidClassCastException结束。可序列化类可以通过声明一个名为serialVersionUID的字段来显式声明自己的serialVersionID,该字段必须是静态、final和long类型。

让我们举个例子来试试。

import java.io.Serializable;

public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    private String empname;
    private byte empage;

    public String getEmpName() {
        return name;
    }

    public void setEmpName(String empname) {
        this.empname = empname;
    }

    public byte getEmpAge() {
        return empage;
    }

    public void setEmpAge(byte empage) {
        this.empage = empage;
    }

    public String whoIsThis() {
        return getEmpName() + " is " + getEmpAge() + "years old";
    }
}

创建序列化对象

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Writer {
    public static void main(String[] args) throws IOException {
        Employee employee = new Employee();
        employee.setEmpName("Jagdish");
        employee.setEmpAge((byte) 30);

        FileOutputStream fout = new
                FileOutputStream("/users/Jagdish.vala/employee.obj");
        ObjectOutputStream oos = new ObjectOutputStream(fout);
        oos.writeObject(employee);
        oos.close();
        System.out.println("Process complete");
    }
}

反序列化对象

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class Reader {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        Employee employee = new Employee();
        FileInputStream fin = new FileInputStream("/users/Jagdish.vala/employee.obj");
        ObjectInputStream ois = new ObjectInputStream(fin);
        employee = (Employee) ois.readObject();
        ois.close();
        System.out.println(employee.whoIsThis());
    }
}

注意:现在更改Employee类的serialVersionUID并保存:

private static final long serialVersionUID = 4L;

并执行Reader类。不执行Writer类,您将得到异常。

Exception in thread "main" java.io.InvalidClassException: 
com.jagdish.vala.java.serialVersion.Employee; local class incompatible: 
stream classdesc serialVersionUID = 1, local class serialVersionUID = 4
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
at com.krishantha.sample.java.serialVersion.Reader.main(Reader.java:14)

如果您永远不需要将对象序列化到字节数组并发送/存储它们,那么您就不必担心。如果需要,那么您必须考虑serialVersionUID,因为对象的反序列化程序会将其与其类加载器所具有的对象版本相匹配。在Java语言规范中了解更多有关它的信息。

serialVersionUID有助于序列化数据的版本控制。序列化时,其值与数据一起存储。反序列化时,将检查同一版本,以查看序列化数据与当前代码的匹配情况。

如果要对数据进行版本化,通常从serialVersionUID 0开始,并将其与更改序列化数据(添加或删除非瞬时字段)的类的每一个结构更改一起转储。

内置的反序列化机制(在.defaultReadObject()中)将拒绝从旧版本的数据进行反序列化。但如果您愿意,您可以定义自己的readObject()函数,该函数可以读取旧数据。然后,此自定义代码可以检查serialVersionUID,以了解数据的版本,并决定如何对其进行反序列化。如果存储的序列化数据在代码的几个版本中都存在,则此版本控制技术非常有用。

但将序列化数据存储如此长的时间跨度并不常见。更常见的是使用串行化机制将数据临时写入例如缓存,或通过网络将数据发送到具有相同版本的代码库相关部分的另一个程序。

在这种情况下,您对保持向后兼容性不感兴趣。您只关心确保正在通信的代码库确实具有相同版本的相关类。为了方便这种检查,您必须像以前一样维护serialVersionUID,并且在对类进行更改时不要忘记更新它。

如果忘记更新字段,则可能会导致一个类的两个不同版本具有不同的结构,但具有相同的serialVersionUID。如果发生这种情况,默认机制(在.defaultReadObject()中)将检测不到任何差异,并尝试对不兼容的数据进行反序列化。现在,您可能会遇到一个神秘的运行时错误或静默失败(空字段)。这些类型的错误可能很难找到。

因此,为了帮助这个用例,Java平台为您提供了不手动设置serialVersionUID的选择。相反,类结构的哈希将在编译时生成并用作id。该机制将确保您永远不会有具有相同id的不同类结构,因此您不会得到上述难以跟踪的运行时序列化失败。

但自动生成id策略也有其背后的原因。也就是说,为同一类生成的id在编译器之间可能会有所不同(正如Jon Skeet所提到的)。因此,如果在使用不同编译器编译的代码之间传递序列化数据,建议无论如何都手动维护id。

如果您像前面提到的第一个用例那样与数据向后兼容,那么您可能也希望自己维护id。这是为了获得可读的id,并更好地控制它们的更改时间和方式。

首先回答您的问题,当我们不在类中声明SerialVersionUID时,Java运行时会为我们生成它,但该过程对许多类元数据敏感,包括字段数、字段类型、字段的访问修饰符、类实现的接口等。因此,建议我们自己声明它,Eclipse也会警告您。

序列化:我们经常处理状态(对象变量中的数据)非常重要的重要对象,因此在将对象状态发送到其他机器时,我们不会因电源/系统故障(或网络故障)而丢失它。这个问题的解决方案被命名为“持久性”,这意味着持久化(保存/保存)数据。串行化是实现持久性的许多其他方法之一(通过将数据保存到磁盘/内存)。保存对象的状态时,为对象创建标识,以便能够正确地将其读回(反序列化),这一点非常重要。此唯一标识ID为SerialVersionUID。

如果CheckStyle能够验证实现Serializable的类上的serialVersionUID是否具有良好的值,即它与串行版本id生成器将生成的值相匹配,这将是一件好事。例如,如果您有一个包含大量可序列化DTO的项目,那么记住删除现有的serialVersionUID并重新生成它是一件痛苦的事,而目前验证这一点的唯一方法(据我所知)是为每个类重新生成并与旧类进行比较。这是非常痛苦的。