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

3413

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

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

serialVersionUID 是什么以及为什么它很重要?请给出一个示例,其中缺少serialVersionUID 将导致问题。


7
找到有关serialversionUID的良好实践;https://dzone.com/articles/what-is-serialversionuid - ceyun
另外两篇文章。mkyong.com/java-best-practices/understand-the-serialversionuid/,https://www.baeldung.com/java-serial-version-uid - Nick Dong
如果在序列化和反序列化时不使用serialVersionUID,对类的任何更改都可能导致先前序列化的版本无法读取或在反序列化时出现意外行为。 - undefined
我创建了一个 JUnit 测试,用于在我的代码中查找所有可序列化的类,如果有任何类缺少 serialVersionUID,则测试失败,并生成一个测试和一些代码,用于在旧值处插入 serialVersionUID。对他人可能有用。再也不用担心没有 linter 或 JUnit 失败而忘记 serialVersionUID 了!https://gist.github.com/arberg/d867185288b0f36aa908f45d6225c40c - undefined
25个回答

2571

关于 java.io.Serializable 的文档可能是你能找到的最好的解释:

序列化运行时会为每个可序列化的类关联一个版本号,称为 serialVersionUID。在反序列化过程中,该版本号用于验证序列化对象的发送方和接收方是否已加载与序列化兼容的该对象的类。如果接收方已经加载了与对应发送方的类不同 serialVersionUID 的对象类,则反序列化将导致一个 InvalidClassException 异常。可序列化的类可以通过声明一个名为 serialVersionUID 的静态、常量、类型为 long 的字段来显式地声明其自己的 serialVersionUID

ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
如果一个可序列化的类没有显式声明 serialVersionUID,那么序列化运行时会根据该类的各个方面计算出一个默认的 serialVersionUID 值,具体描述详见 Java(TM) Object Serialization Specification。然而,强烈建议所有可序列化的类都要显式声明 serialVersionUID 值,因为默认的 serialVersionUID 计算过程非常敏感,可能会因编译器实现不同而导致意料之外的反序列化异常 InvalidClassExceptions。因此,为了保证在不同的Java编译器实现之间具有一致的 serialVersionUID 值,可序列化的类必须声明一个显式的 serialVersionUID 值。同时,强烈建议使用 private 修饰符来声明 serialVersionUID,因为这样的声明仅适用于直接声明的类 —— serialVersionUID 字段作为继承成员并不实用。

382
因此,您的意思基本上是,如果用户没有理解上述所有材料,那么该用户不必担心序列化?我相信您回答了“如何?”而不是解释“为什么?”。就我个人而言,我不明白为什么我需要关注SerializableVersionUID。 - Ziggy
407
为什么在第二段中解释:如果您没有明确指定serialVersionUID,则会自动生成一个值 - 但这很脆弱,因为它取决于编译器的实现。 - Jon Skeet
19
当我继承异常类时,为什么Eclipse会提示我需要添加“private static final long serialVersionUID = 1L;”? - JohnMerlino
25
@JohnMerlino:嗯,我不会期望它说你需要一个 - 但是它可能会建议你使用一个来帮助你正确地序列化异常。如果你不打算对它们进行序列化,那么你真的不需要这个常量。 - Jon Skeet
19
@JohnMerlino,回答你问题中的“为什么”部分:异常类实现了 Serializable 接口,eclipse 警告你没有设置 serialVersionUID,如果你想要序列化该类,设置 serialVersionUID 是一个好主意,以避免 JonSkeet 的帖子概述的问题。 - zpon
显示剩余17条评论

521
如果你只是因为实现的原因而进行序列化(例如,你序列化一个HTTPSession,无论它是否被存储,你可能不关心反序列化一个表单对象),那么你可以忽略这个问题。
如果你真的在使用序列化,它只有在直接使用序列化存储和检索对象时才有意义。 serialVersionUID代表你的类版本,如果当前版本与其前一版本不兼容,则应增加它。
大多数情况下,你可能不会直接使用序列化。如果是这种情况,请通过点击快速修复选项生成默认的SerialVersionUID,然后就不用担心它了。

88
如果您不使用序列化进行永久存储,建议使用@SuppressWarnings而不是添加值。这样可以使类更清晰,并保留serialVersionUID机制以保护您免受不兼容更改的影响。 - Tom Anderson
29
我不理解为什么使用一行代码(@SuppressWarnings注释)相对于另一行代码(serializable id)会导致类变得更加混乱。如果你不打算将对象序列化到永久存储中,为什么不直接使用“1”?在这种情况下,你无论如何都不会关心自动生成的ID。 - MetroidFan2002
76
@MetroidFan2002:我认为@TomAnderson关于“serialVersionUID”保护您免受不兼容更改的观点是正确的。如果您不希望将类用于永久存储,则使用“@SuppressWarnings”可以更好地记录意图。 - AbdullahC
11
如果你的类的当前版本与之前的版本不兼容,那么你应该增加它的serialVersionUID。首先,探索Serialization的广泛对象版本支持,(a) 以确保类现在真正是不兼容序列化的方式,这根据规范是相当困难的; (b) 尝试一些方案,如自定义read/writeObject()方法、readResolve/writeReplace()方法、serializableFields声明等,以确保流仍然兼容。更改实际的serialVersionUID是最后的手段,这是绝望的建议。 - user207421
4
当类的初始作者明确引入时,serialVersionUID 的增量会出现。我认为,由 JVM 生成的序列化 ID 应该是可以的。这是我在序列化方面看到的最好的答案。 - overexchange
显示剩余4条评论

366

我不能错过介绍Josh Bloch的书《Effective Java》(第二版)的机会。第10章是Java序列化方面的不可或缺的资源。

根据Josh的说法,自动生成的UID基于类名、实现的接口以及所有公共和受保护成员生成。只要以任何方式更改这些内容,都将更改serialVersionUID。因此,只有当您确定该类的不止一个版本永远不会被序列化(在进程之间或稍后从存储中检索)时,才无需修改它们。

如果现在忽略它们,并发现稍后需要以某种方式更改类但仍需与旧版本兼容,则可以使用JDK工具serialver在旧类上生成serialVersionUID,并在新类上明确设置它。(根据您的更改,您可能还需要通过添加writeObjectreadObject方法来实现自定义序列化 - 请参见Serializable javadoc或前述第10章。)


40
如果关注一个类与旧版本的兼容性,那么可能需要关注SerializableVersionUID吗? - Ziggy
15
如果新版本将任何公共成员更改为受保护的,则默认的SerializableVersionUID将不同,并且会引发InvalidClassExceptions。 - Chander Shivdasani
3
类名,已实现的接口,所有公共和受保护的方法,以及所有实例变量。 - Achow
42
值得注意的是,Joshua Bloch建议对于每个可序列化类都值得指定serial version uid。引用自第11章:无论您选择什么样的序列化形式,在您编写的每个可序列化类中声明一个显式的serial version UID。这消除了serial version UID作为不兼容性潜在来源的可能性(第74条)。这还有一个小的性能优势。如果没有提供serial version UID,则需要进行昂贵的计算来在运行时生成它。 - Ashutosh Jindal
2
看起来很相关。以下是使用IDE生成序列版本UID的几种方法链接:https://mkyong.com/java/how-to-generate-serialversionuid/ - Ben

145

您可以告诉Eclipse忽略这些serialVersionUID警告:

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

如果您不知道的话,还有很多其他警告可以在此部分启用(甚至将其报告为错误),其中许多功能非常有用:

  • 潜在的编程问题:可能发生意外的布尔值赋值
  • 潜在的编程问题:空指针访问
  • 不必要的代码:本地变量未被读取
  • 不必要的代码:冗余的null检查
  • 不必要的代码:不必要的强制类型转换或'instanceof'

还有许多其他的警告。


26
点赞仅仅因为原帖的作者似乎没有在序列化任何东西。如果原帖作者说“我正在对这个东西进行序列化……”那么你会得到一个踩的投票 :P - John Gardner
15
@Gardner -> 同意!但是问答者也想知道为什么他可能不想被警告。 - Ziggy
11
提问者显然关心为什么需要 UID。因此,简单地告诉他忽略警告应该被投反对票。 - cinqS

125

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

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

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

但是,长时间保留序列化数据并不太常见。更常见的情况是使用序列化机制将数据临时写入缓存或通过网络发送到具有相关代码库相同版本的另一个程序。在这种情况下,您不关心维护向后兼容性。您只关心确保通信的代码库确实具有相关类的相同版本。为了方便这样的检查,您必须像以前一样维护serialVersionUID并在更改类时不要忘记更新它。

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

为了帮助这种情况,Java平台提供了一种选择,不需要手动设置serialVersionUID。相反,在编译时会生成类结构的哈希值并用作ID。这种机制可以确保您永远不会有相同ID的不同类结构,因此您不会遇到上述难以跟踪的运行时序列化失败问题。
但是,自动生成的ID策略也有一个缺点。即相同类的生成ID可能在不同编译器之间不同(正如Jon Skeet所提到的那样)。因此,如果在使用不同编译器编译的代码之间通信序列化数据,建议仍然手动维护ID。
如果您需要向后兼容数据,就像第一个用例中提到的那样,您也可能想要自行维护ID。这样做可以获得可读的ID,并更好地控制何时以及如何更改它们。

5
添加或删除非瞬态字段不会使类的序列化不兼容。因此,在进行这些更改时没有必要“升级”它。 - user207421
4
@EJP: 嗯?在我的世界里,添加数据肯定会改变序列化数据。 - Alexander Torstling
3
请阅读我的发言。我并没有说它不会“改变序列化数据”。我说的是它“不会使类无法进行序列化”。这不是同一件事。你需要阅读《对象序列化规范》中的版本控制章节。 - user207421
3
我明白添加一个非瞬态字段并不一定意味着使类无法序列化,但这是一种结构性改变,会改变序列化的数据。通常情况下,您希望在这样做时提高版本号,除非您处理了向后兼容性,这也是我在后面解释的。那么你到底想表达什么? - Alexander Torstling
5
我的观点和我之前说的一样。添加或删除非瞬态字段并不会使类无法进行序列化。因此,每次这样做时您都不需要增加serialVersionUID。 - user207421

81
什么是serialVersionUID,为什么我要使用它? SerialVersionUID 是每个类的唯一标识符,JVM 使用它来比较类的版本,确保在序列化期间使用的相同类在反序列化期间被加载。
指定一个serialVersionUID可以给我们更多的控制权,尽管如果您不指定,JVM会生成一个。由于不同的编译器之间生成的值可能会有所不同,因此指定一个值可以解决这个问题。 此外,有时您希望禁止旧序列化对象的反序列化[向后不兼容性],在这种情况下,您只需更改serialVersionUID即可。 Java文档对Serializable的定义中说:

默认的serialVersionUID计算非常敏感于类细节,这些细节可能因编译器实现而异,因此可能导致意外的InvalidClassException 在反序列化过程中。

因此,你必须声明serialVersionUID,因为它可以给我们更多的控制。这篇文章提供了一些关于这个话题的好点子。

3
@Vinothbabu,但是 serialVersionUID 是静态变量,因此静态变量不能被序列化。那么 JVM 如何在不知道反序列化对象的版本号的情况下进行版本检查呢? - Kranthi Sama
3
这个答案中没有提到的一件事是,如果你不知道为什么而盲目地包含 serialVersionUID,可能会导致意想不到的后果。Tom Anderson 对 MetroidFan2002 的回答发表了评论,解释说:“我认为,如果您不是在使用序列化进行永久存储,那么应该使用 @SuppressWarnings 而不是添加值。这样可以减少类的混乱程度,并保留 serialVersionUID 机制保护您免受不兼容更改的能力。” - Kirby
10
serialVersionUID 不是每个类的“唯一标识符”,完全限定类名才是。它是一个版本指示器,用于识别序列化对象的版本。请注意,此处不提供任何解释。 - user207421
一个 serialVersionUID 可以用于不同的类吗? - Brooklyn99

65

原始问题要求解释“为什么很重要”和“何时使用序列版本号”的例子,那么我找到了一个。

比方说你创建了一个Car类,将其实例化并写入对象流。这个压缩后的汽车对象在文件系统中存在了一段时间。同时,如果Car类被修改并添加了一个新字段。后来,当你尝试读取(即反序列化)压缩后的Car对象时,你会得到java.io.InvalidClassException,因为所有可序列化的类都会自动分配一个唯一标识符。当类的标识符与压缩对象的标识符不相等时,就会抛出此异常。如果你认真思考,会发现这个异常是由于添加了新字段而引起的。你可以通过声明显式的serialVersionUID来控制版本,从而避免抛出此异常。显式声明serialVersionUID也有一定的性能优势(因为不需要计算)。因此,最佳实践是在创建可序列化类时立即添加自己的serialVersionUID,如下所示:

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

5
@abbas 为什么应该这样做?请解释一下它会带来什么不同。 - user207421
3
@abbas,这个意图并不与使用逐步增加的自然数 1 等相冲突。 - Vadzim
3
@BillK,我认为序列化检查与类名和serialVersionUID绑定在一起。因此,不同类和库的不同编号方案不会以任何方式干扰。或者你是指代码生成库吗? - Vadzim
1
@abbas serialVersionUID与“找到正确的类版本”没有任何关系。 - user207421
1
我一直觉得很奇怪的是,当没有明确声明serialVersionUID时,推导它的算法基于包、名称、属性,但也包括方法... 方法不会被序列化,添加/删除方法对对象的序列化形式没有影响,那么为什么在添加/删除/更改方法时要产生不同的serialVersionUID呢? - Volksman
显示剩余4条评论

61

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

序列化允许将对象转换为流,以便通过网络发送该对象或保存到文件或保存到数据库以备后用。

序列化有一些规则:

  • 只有类或其超类实现了Serializable接口的对象才可以序列化。

  • 即使其超类没有实现Serializable接口,一个对象本身也可以进行序列化。但是,在可序列化类层次结构中第一个未实现Serializable接口的超类必须具有无参数构造函数。如果违反此规则,则readObject()方法会在运行时产生java.io.InvalidClassException异常。

  • 所有基本类型都可以序列化。

  • 瞬态字段(使用transient修饰)不会被序列化(即不会被保存或还原)。实现Serializable接口的类必须标记那些不支持序列化的类的瞬态字段(例如文件流)。

  • 静态字段(使用static修饰)不会被序列化。

当对Object进行序列化时,Java运行时将关联序列版本号,即serialVersionID

我们何时需要使用serialVersionID:

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

让我们通过一个示例来尝试一下。

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)

请纠正我如果我错了 - 本地类是您当前在类路径中使用的类,而流类是由另一方使用的类(例如,返回答案并序列化响应的服务器)。当您与已更新其第三方库但您(客户端)尚未完成此操作的服务器通信时,您可能会遇到此情况。 - Victor

42

如果您永远不需要将对象序列化为字节数组并发送/存储它们,那么您就不必担心这个问题。但是如果您需要这样做,那么您必须考虑您的 serialVersionUID,因为对象的反序列化器将使用它来匹配其类加载器版本的对象。请在Java语言规范中了解更多相关信息。


11
如果你不打算将对象序列化,为什么它们要实现Serializable接口? - erickson
9
@erickson - 父类可能是可序列化的,比如ArrayList,但是你想让自己的对象(比如修改过的ArrayList)以它为基础,但永远不会序列化创建的集合。 - MetroidFan2002
6
Java语言规范中没有提到它。它在对象版本规范中提到了。 - user207421
5
这是Java 8 [对象版本规范]的链接:http://docs.oracle.com/javase/8/docs/platform/serialization/spec/serialTOC.html。 - Basil Bourque

37
如果你在一个你从未考虑序列化的类上收到这个警告,而且你没有声明自己实现了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方法。我认为在这种情况下不应该有警告(因为通常不能在不同版本的类之间可靠地序列化和反序列化这样的匿名类),但我不确定编译器如何识别这一点。


4
我认为你说的组合优于继承在讨论像ArrayList这样的类时更有意义。然而,许多框架要求人们从可序列化的抽象超类(例如Struts 1.2的ActionForm类或Saxon的ExtensionFunctionDefinition)进行扩展,在这种情况下,这个解决方案是行不通的。我认为你说的没错,如果忽略某些情况下(例如继承自抽象可序列化类),警告会很好。 - piepera
2
如果您将一个类作为成员添加,而不是继承它,那么您将不得不为您希望使用的成员类的每个方法编写一个包装器方法,这在许多情况下都是不可行的...除非Java有类似于Perl的__AUTOLOAD函数,但我不知道。 - M_M
4
当你要委托很多方法给你的包装对象时,使用委托可能不合适。但我认为这种情况表明设计存在问题 - 你的类(例如"MainGui")的用户不应该需要调用包装对象(例如JFrame)的许多方法。 - Paŭlo Ebermann
1
我不喜欢委托的原因是需要保留对委托的引用。每个引用都意味着更多的内存。如果我需要一个包含100个对象的CustomizedArrayList,那么这并不重要,但如果我需要数百个只包含几个对象的CustomizdeArrayLists,则内存使用量会显著增加。 - WVrock
2
Throwable是可序列化的,而且只有Throwable是可抛出的,因此不可能定义一个不可序列化的异常。委托也是不可能的。 - Phil
@Phil 我认为这是有意设计的 - 即使您不想序列化异常类,其他人可能希望序列化由您抛出的异常。(当然,可以争论大多数异常类应该使用自动生成的serialVersionUID并且不需要固定的serialVersionUID。) - Paŭlo Ebermann

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