什么是 serialVersionUID,为什么要使用它?

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

可序列化的类 Foo 没有声明类型为 long 的静态最终 serialVersionUID 字段

什么是serialVersionUID ,为什么重要?请显示一个示例,其中缺少serialVersionUID会导致问题。

答案

您将得到有关java.io.Serializable的文档最佳解释:

序列化运行时与每个可序列化的类关联一个版本号,称为serialVersionUID ,在反序列化期间使用该版本号来验证序列化对象的发送者和接收者是否已加载了该对象的与序列化兼容的类。如果接收者已为该对象加载了一个与相应发送者类具有不同的serialVersionUID的类,则反序列化将导致InvalidClassException 。可序列化的类可以通过声明一个名为serialVersionUID的字段来显式声明其自己的serialVersionUID ,该字段必须是静态的,最终的且类型为long

ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;

如果可序列化的类未明确声明serialVersionUID ,则序列化运行时将根据该类的各个方面,为该类计算默认的serialVersionUID值,如 Java(TM)对象序列化规范中所述。但是, 强烈建议所有可序列化的类显式声明serialVersionUID值,因为默认的serialVersionUID计算对类详细信息高度敏感,而类详细信息可能会根据编译器的实现而有所不同,因此可能在反序列化期间导致意外的InvalidClassExceptions 。因此,为了保证不同 Java 编译器实现之间的serialVersionUID值一致,可序列化的类必须声明一个显式的serialVersionUID值。强烈建议显式serialVersionUID声明在可能的情况下使用 private 修饰符,因为此类声明仅适用于立即声明的类serialVersionUID字段作为继承成员不起作用。

如果您只是为了实现的需要而进行序列化(例如,谁在乎是否为HTTPSession进行序列化…… 例如,是否进行序列化…… 是否存储,则可能不关心对de-serializing对象进行de-serializing ) ,那么您可以忽略此。

如果您实际上正在使用序列化,则仅在计划直接使用序列化存储和检索对象时才重要。 serialVersionUID表示您的类版本,如果您的类的当前版本与它的先前版本不向后兼容,则应该递增它。

大多数时候,您可能不会直接使用序列化。如果是这种情况, SerialVersionUID通过单击快速修复选项来生成默认的SerialVersionUID ,不用担心。

我不能错过这个机会来插上乔什 · 布洛赫(Josh Bloch)的书《 有效的 Java》 (第二版)。第 11 章是有关 Java 序列化的必不可少的资源。

Per Josh 会根据类名称,已实现的接口以及所有公共成员和受保护成员来生成自动生成的 UID。以任何方式更改其中任何一个都将更改serialVersionUID 。因此,仅在确定不会对一个以上版本的类进行序列化(跨进程或在以后的时间从存储中检索)时,您才无需弄乱它们。

如果现在暂时忽略它们,然后以后发现需要以某种方式更改类但要保持与旧版本的兼容性,则可以使用 JDK 工具serialver类上生成serialVersionUID ,并进行显式设置在新班上。 (根据您的更改,您可能还需要通过添加writeObjectreadObject方法来实现自定义序列化 - 请参见Serializable javadoc 或前面提到的第 11 章。)

您可以告诉 Eclipse 忽略以下 serialVersionUID 警告:

窗口 > 首选项 > Java > 编译器 > 错误 / 警告 > 潜在的编程问题

如果您不知道,您可以在本节中启用很多其他警告(甚至有一些警告报告为错误),其中许多警告非常有用:

  • 潜在的编程问题:可能的意外布尔分配
  • 潜在的编程问题:空指针访问
  • 不必要的代码:永远不会读取局部变量
  • 不必要的代码:冗余 null 检查
  • 不必要的代码:不必要的强制转换或'instanceof'

还有很多。

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

如果要对数据进行版本控制,通常以serialVersionUID 0 开头,并在每次对类进行结构更改时更改它,从而更改序列化的数据(添加或删除非瞬态字段)。

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

但是存储序列化数据这么长的时间并不是很常见。使用序列化机制将数据临时写入例如高速缓存或通过网络将其发送到具有相同版本代码库相关部分的另一个程序,这是更为常见的做法。

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

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

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

但是自动生成的 id 策略有一个缺点。也就是说,同一类的生成 ID 可能在编译器之间有所不同(如上文 Jon Skeet 所述)。因此,如果在使用不同编译器编译的代码之间传递序列化数据,则建议仍然手动维护 ID。

并且,如果您像提到的第一个用例那样向后兼容数据,则您可能还想自己维护 ID。为了获得可识别的 ID,并更好地控制它们的更改时间和方式。

什么是serialVersionUID ,为什么要使用它?

SerialVersionUID是每个类的唯一标识符, JVM使用它来比较该类的版本,以确保在反序列化期间加载序列化期间使用的同一类。

指定一个可以提供更多控制权,但是如果您未指定的话,JVM 会生成一个控制权。生成的值在不同的编译器之间可能有所不同。此外,有时您出于某种原因只想禁止对旧的序列化对象进行反序列化 [ backward incompatibility ],在这种情况下,您只需要更改 serialVersionUID。

用于Serializablejavadocs

默认的 serialVersionUID 计算对类详细信息高度敏感,类详细信息可能会根据编译器的实现而有所不同,因此可能会在反序列化期间导致意外的InvalidClassException

因此,您必须声明 serialVersionUID,因为它可以提供更多控制权

本文对此主题有一些好处。

最初的问题要求使用 “ Serial Version ID为什么有用” 的 “为什么重要” 和 “示例”。好吧,我找到了一个。

假设您创建一个Car类,实例化它,并将其写到对象流中。展平的汽车对象在文件系统中放置了一段时间。同时,如果通过添加新字段来修改Car类。稍后,当您尝试读取(即反序列化)展平的Car对象时,会得到java.io.InvalidClassException因为所有可序列化的类都会自动获得唯一的标识符。当类的标识符与展平对象的标识符不相等时,抛出此异常。如果您真的考虑过,则会由于添加了新字段而引发异常。您可以通过声明显式的 serialVersionUID 来控制版本,从而避免抛出此异常。显式声明您的serialVersionUID也有一个小的性能好处(因为不必计算)。因此,最佳实践是在创建它们后立即将自己的 serialVersionUID 添加到 Serializable 类,如下所示:

public class Car {
    static final long serialVersionUID = 1L; //assign a long value
}

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

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

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

有一些序列化规则

  • 仅当对象的类或其超类实现 Serializable 接口时,该对象才可序列化

  • 一个对象是可序列化的(本身实现了 Serializable 接口),即使其超类不是。但是,可序列化类的层次结构中的第一个超类(不实现 Serializable 接口)必须具有无参数构造函数。如果违反此规定,则 readObject()将在运行时生成 java.io.InvalidClassException

  • 所有原始类型都是可序列化的。

  • 暂态字段(带有暂态修饰符)未序列化(即,未保存或恢复)。实现 Serializable 的类必须标记不支持序列化的类(例如文件流)的瞬态字段。

  • 静态字段(带有 static 修饰符)未序列化。

序列化Object时,Java Runtime 会将序列号也称为serialVersionID关联。

我们需要 serialVersionID 的位置:在反序列化期间,验证发送者和接收者在序列化方面是否兼容。如果接收方使用不同的serialVersionID加载了该类,则反序列化将以InvalidClassCastException结尾。
可序列化的类可以通过声明一个名为serialVersionUID的字段来显式声明其自己的serialVersionUID ,该字段必须是静态的,最终的且类型为 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() {
    StringBuffer employee = new StringBuffer();
    employee.append(getEmpName()).append(" is ).append(getEmpAge()).append("
years old  "));
    return employee.toString();
}
}

创建序列化对象

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)

如果在类上收到此警告,则永远不要考虑进行序列化,并且也没有声明自己implements Serializable ,这通常是因为您从实现了 Serializable 的超类继承而来。通常,最好委托给这样的对象,而不要使用继承。

所以,代替

public class MyExample extends ArrayList<String> {

    public MyExample() {
        super();
    }
    ...
}

public class MyExample {
    private List<String> myList;

    public MyExample() {
         this.myList = new ArrayList<String>();
    }
    ...
}

并在相关方法中调用myList.foo()而不是this.foo() (或super.foo() )。 (这并不适合所有情况,但仍然很常见。)

我经常看到人们扩展 JFrame 之类的东西,而实际上他们只需要委托给它即可。 (这也有助于在 IDE 中自动完成,因为 JFrame 具有数百种方法,当您要在类上调用自定义方法时就不需要这些方法。)

警告(或 serialVersionUID)不可避免的一种情况是,当您从 AbstractAction 扩展时,通常是在一个匿名类中,仅添加 actionPerformed 方法。我认为在这种情况下不应该发出警告(因为您通常无法跨类的不同版本可靠地序列化和反序列化此类匿名类),但是我不确定编译器如何识别这一点。