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

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

可序列化类Foo不声明静态Finallong

类型的序列版本UID字段

什么是serialVersionUID,为什么它很重要?请举一个缺少serialVersionUID会导致问题的例子。

1097886 次浏览

#0的文档可能是你会得到的最好的解释:

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

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

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

如果你永远不需要将对象序列化为字节数组并发送/存储它们,那么你不必担心。如果你这样做了,那么你必须考虑你的序列化版本UID,因为对象的反序列化器将使其与类加载器拥有的对象版本相匹配。在Java语言规范中阅读更多信息。

我不能错过这个机会来介绍Josh Bloch的书有效Java(第2版)。第10章是关于Java序列化的不可或缺的资源。

根据Josh的说法,自动生成的UID是基于类名、实现的接口以及所有公共和受保护的成员生成的。以任何方式更改其中任何一个都会更改serialVersionUID。因此,只有当您确定该类的序列化版本不会超过一个时(无论是跨进程还是稍后从存储中检索),您才无需弄乱它们。

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

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

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

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

  • 潜在的编程问题:可能的意外布尔赋值
  • 潜在的编程问题:空指针访问
  • 不必要的代码:永远不会读取局部变量
  • 不必要的代码:冗余空检查
  • 不必要的代码:不必要的转换或“instance of”

还有更多。

如果你序列化只是因为你必须为了实现而序列化(谁在乎你是否为HTTPSession序列化,例如……如果它被存储,你可能不关心de-serializing表单对象),那么你可以忽略这一点。

如果您实际上正在使用序列化,那么只有计划直接使用序列化存储和检索对象才重要。serialVersionUID代表您的类版本,如果您的类的当前版本与以前的版本不向后兼容,您应该将其递增。

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

如果CheckStyle可以验证实现Serializable的类上的序列版本UID是否具有良好的值,即它与串行版本ID生成器产生的值相匹配,那就太好了。例如,如果你有一个具有许多可序列化DTO的项目,记住删除现有的序列版本UID并重新生成它是一件痛苦的事情,目前(我知道的)验证这一点的唯一方法是为每个类重新生成并与旧类进行比较。这非常非常痛苦。

如果你在一个你从未考虑过序列化的类上收到这个警告,并且你没有声明自己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有数百个方法,当你想在类中调用你的自定义方法时,你不需要这些方法。)

不可避免地会出现警告(或者序列版本UID)的一种情况是,当你从AbstractAction扩展时,通常是在匿名类中,只添加actionPerformd-方法。我认为在这种情况下不应该有警告(因为你通常不能可靠地序列化和反序列化这样的匿名类,无论如何跨越类的不同版本),但我不确定编译器如何识别这一点。

不用麻烦,默认计算非常好,足以满足99,9999%的情况。如果你遇到问题,你可以——如前所述——在需要时引入UID(这是极不可能的)

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

如果你想版本化你的数据,你通常从serialVersionUID开始,然后在你的类的每一次结构更改(改变序列化数据(添加或删除非瞬态字段))时撞击它。

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

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

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

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

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

但是自动生成id策略有一个缺点,即同一个类在不同编译器之间生成的id可能不同(正如Jon Skeet上面提到的)。因此,如果你在使用不同编译器编译的代码之间通信序列化数据,建议无论如何手动维护id。

如果您与第一个用例中提到的数据向后兼容,您可能还想自己维护id。这是为了获得可读的id并更好地控制它们何时以及如何更改。

字段数据表示存储在类中的一些信息。类实现了Serializable接口,所以eclipse自动提供声明serialVersionUID字段。让我们从那里设置的值1开始。

如果你不希望这个警告到来,使用这个:

@SuppressWarnings("serial")

最初的问题要求“为什么它很重要”和“示例”,这个Serial Version ID将是有用的。我找到了一个。

假设你创建了一个Car类,实例化它,并将其写入对象流。扁平化的汽车对象在文件系统中停留一段时间。同时,如果Car类通过添加一个新字段进行修改。稍后,当你尝试读取(即反序列化)扁平化的Car对象时,你会得到java.io.InvalidClassException——因为所有可序列化的类都自动被赋予一个唯一标识符。当类的标识符不等于扁平化对象的标识符时,会引发此异常。如果你真的考虑一下,会因为添加了新字段而引发异常。你可以通过声明一个显式的seralVersionUID来控制自己的版本来避免抛出此异常。显式声明你的serialVersionUID(因为不必计算)也有一个小的性能优势。因此,最好的做法是在创建它们后立即将自己的serializable类添加到它们中,如下所示:

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

举个例子,缺少的序列版本UID可能会导致问题:

我正在研究这个JavaEE应用程序,它由一个使用EJB模块的Web模块组成。Web模块远程调用EJB模块并传递一个实现SerializablePOJO作为参数。

这个POJO's类被打包在Web模块的WEB-INF/lib中的EJB jar和它自己的jar中。它们实际上是同一个类,但是当我打包EJB模块时,我解压缩了这个POJO的jar以将其与EJB模块一起打包。

EJB的调用因下面的异常而失败,因为我没有声明它的serialVersionUID

Caused by: java.io.IOException: Mismatched serialization UIDs : Source(Rep.IDRMI:com.hordine.pedra.softbudget.domain.Budget:5CF7CE11E6810A36:04A3FEBED5DA4588)= 04A3FEBED5DA4588 whereas Target (Rep. ID RMI:com.hordine.pedra.softbudget.domain.Budget:7AF5ED7A7CFDFF31:6227F23FA74A9A52)= 6227F23FA74A9A52

要理解字段serialVersionUID的重要性,应该了解序列化/反序列化是如何工作的。

当Serializable类对象被序列化时JavaRuntime将一个序列化版本号(称为SerialVersionUID)与此序列化对象相关联。在反序列化此序列化对象时JavaRuntime将序列化对象的SerialVersionUID与类的SerialVersionUID匹配。如果两者相等,则仅继续进行反序列化,否则抛出InvalidClassException。

所以我们的结论是,要使序列化/反序列化过程成功,序列化对象的序列化版本UID必须等价于类的序列化版本UID。如果程序员在程序中明确指定了序列化版本UID值,那么相同的值将与序列化对象和类相关联,无论序列化和反序列化平台如何(例如。序列化可能在Windows等平台上使用sun或MS JVM完成,而反序列化可能在不同的平台上Linux使用Zing JVM)。

但是如果程序员在对任何对象进行序列化\去序列化时没有指定序列化UID,Java运行时会使用自己的算法来计算它。这个序列化UID计算算法因JRE而异。也有可能对象被序列化的环境使用一个JRE(例如:SUN JVM),而反序列化发生的环境使用LinuxJvm(ze)。在这种情况下,与序列化对象关联的序列化VersionUID将不同于在反序列化环境中计算的类的序列化VersionUID。反过来反序列化也不会成功。所以为了避免这种情况/问题,程序员必须始终指定可序列化类的序列化VersionUID。

什么是序列号版本号,为什么要使用它?

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

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

javadocs#0

默认的序列版本UID计算对类高度敏感详细信息可能因编译器实现而异,并且可以因此导致意外的InvalidClassException反序列化。

因此,您必须声明序列版本UID,因为它给我们更多的控制权

这篇文章在这个话题上有一些很好的观点。

我通常在一个上下文中使用serialVersionUID:当我知道它将离开JavaVM的上下文时。

当我为我的应用程序使用ObjectInputStreamObjectOutputStream时,或者如果我知道我使用的库/框架将使用它时,我就会知道这一点。SerialVersionID确保不同版本的不同JavaVM或供应商将正确互操作,或者如果它在VM之外存储和检索,例如HttpSession,即使在应用程序服务器的重新启动和升级期间,会话数据也可以保持不变。

对于所有其他情况,我使用

@SuppressWarnings("serial")

因为大多数时候默认的serialVersionUID就足够了。这包括ExceptionHttpServlet

SerialVersionUID用于对象的版本控制。你也可以在你的类文件中指定SerialVersionUID。不指定SerialVersionUID的后果是,当你在类中添加或修改任何字段时,已经序列化的类将无法恢复,因为为新类和旧序列化对象生成的SerialVersionUID将是不同的。Java序列化过程依赖于正确的SerialVersionUID来恢复序列化对象的状态,并在SerialVersionUID不匹配的情况下抛出java.io.InvalidClassException

阅读更多:http://javarevisited.blogspot.com/2011/04/top-10-java-serialization-interview.html#ixzz3VQxnpOPZ

如果你想修改大量一开始就没有设置的类,同时保持与旧类的兼容性,IntelliJ Idea、Eclipse等工具会生成随机数,并且不能一次处理一堆文件。我想出以下bash脚本(我很抱歉Windows用户,考虑购买Mac或转换为Linux)来轻松修改序列VersionUID问题:

base_dir=$(pwd)src_dir=$base_dir/src/main/javaic_api_cp=$base_dir/target/classes
while read fdoclazz=${f//\//.}clazz=${clazz/%.java/}seruidstr=$(serialver -classpath $ic_api_cp $clazz | cut -d ':' -f 2 | sed -e 's/^\s\+//')perl -ni.bak -e "print $_; printf qq{%s\n}, q{    private $seruidstr} if /public class/" $src_dir/$fdone

你保存这个脚本,对你说add_serialVersionUID.sh~/bin。然后你在Maven或Gradle项目的根目录中运行它,如下所示:

add_serialVersionUID.sh < myJavaToAmend.lst

此. lst包括以下格式添加序列版本UID的java文件列表:

com/abc/ic/api/model/domain/item/BizOrderTransDO.javacom/abc/ic/api/model/domain/item/CardPassFeature.javacom/abc/ic/api/model/domain/item/CategoryFeature.javacom/abc/ic/api/model/domain/item/GoodsFeature.javacom/abc/ic/api/model/domain/item/ItemFeature.javacom/abc/ic/api/model/domain/item/ItemPicUrls.javacom/abc/ic/api/model/domain/item/ItemSkuDO.javacom/abc/ic/api/model/domain/serve/ServeCategoryFeature.javacom/abc/ic/api/model/domain/serve/ServeFeature.javacom/abc/ic/api/model/param/depot/DepotItemDTO.javacom/abc/ic/api/model/param/depot/DepotItemQueryDTO.javacom/abc/ic/api/model/param/depot/InDepotDTO.javacom/abc/ic/api/model/param/depot/OutDepotDTO.java

此脚本在引擎盖下使用JDK seralVer工具。因此请确保您的$JAVA_HOME/bin在PATH中。

这个问题在Joshua Bloch的有效Java中得到了很好的记录。一本非常好的书,必须阅读。我将概述以下一些原因:

序列化运行时会为每个可序列化的类提供一个名为Serial version的数字。这个数字称为Serial VersionUID。这个数字背后有一些数学,它是基于类中定义的字段/方法产生的。对于同一个类,每次都会生成相同的版本。这个数字在反序列化期间用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者为该对象加载的类的Serial alVersionUID与相应发送者的类不同,那么反序列化将导致InvalidClassException。

如果该类是可序列化的,您还可以通过声明一个名为“SerrialVersionUID”的字段来显式声明您自己的SerialVersionUID,该字段必须是静态的、最终的和long类型的。大多数IDE如Eclipse可以帮助您生成长字符串。

这个ID称为序列号版本号,它是根据有关类结构的信息计算的。假设你做了一个员工类,它的版本ID为#333(由JVM分配),现在当你将序列化该类的对象(假设员工对象)时,JVM将为它分配UID为#333。

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

为什么在Java的Serializable类中使用SerialVersionUID

serialization期间,Java运行时为类创建一个版本号,以便以后可以对其进行反序列化。这个版本号在Java中称为SerialVersionUID

SerialVersionUID用于对序列化数据进行版本化。只有当类的SerialVersionUID与序列化实例匹配时,才能对类进行反序列化。当我们在类中没有声明SerialVersionUID时,Java运行时为我们生成它,但不推荐。建议将SerialVersionUID声明为private static final long变量以避免默认机制。

当您通过实现标记接口java.io.Serializable将类声明为Serializable时,Java使用默认序列化机制将该类的运行时持久化实例保存到磁盘中,前提是您没有使用Externalizable接口自定义进程。

另见为什么在JavaSerializable类中使用SerialVersionUID

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

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

序列化有一些规则

  • 一个对象只有在它的类或它的超类实现了Serializable接口时才是可序列化的

  • 一个对象是可序列化的(它本身实现了可序列化接口),即使它的超类不是。然而,可序列化类层次结构中的第一个超类,没有实现可序列化接口,必须有一个无参数构造函数。如果违反了这一点,readObject()将在运行时产生一个java.io.InvalidClassException

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

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

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

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

哪里需要序列号

在反序列化过程中,验证发送者和接收者在序列化方面是否兼容。如果接收者用不同的serialVersionID加载类,那么反序列化将以InvalidClassCastException结束。
可序列化类可以通过声明一个名为serialVersionUID的字段来显式声明自己的serialVersionUID,该字段必须是静态的、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 = newFileOutputStream("/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());}}

注意:现在更改员工类的序列版本UID并保存:

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 = 4at 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)

一个简单的解释:

  1. 您是否正在序列化数据?

    序列化基本上是将类数据写入文件/流/等。反序列化是将数据读回类。

  2. 你打算投入生产吗?

    如果你只是用不重要的/虚假的数据测试一些东西,那么不要担心(除非你直接测试序列化)。

  3. 这是第一个版本吗?

    如果是,设置serialVersionUID=1L

  4. 这是第二个、第三个等等的版本吗?

    现在你需要担心serialVersionUID,应该深入研究它。

基本上,如果您在更新需要写入/读取的类时没有正确更新版本,那么当您尝试读取旧数据时,您将收到错误。

长话短说,此字段用于检查序列化数据是否可以正确反序列化。序列化和反序列化通常由不同的程序副本进行-例如服务器将对象转换为字符串,客户端将接收到的字符串转换为对象。此字段告诉两者对该对象是什么的想法相同。当:

  • 您在不同的地方(例如1台服务器和100个客户端)有许多不同的程序副本。如果您更改对象、更改版本号并忘记更新一个this客户端,它将知道他无法反序列化

  • 您已将数据存储在某个文件中,然后尝试使用带有修改对象的程序的更新版本打开它-如果您保持版本正确,您将知道此文件不兼容

什么时候重要?

最明显的是-如果您向对象添加一些字段,旧版本将无法使用它们,因为它们的对象结构中没有这些字段。

不太明显-当你反序列化对象时,字符串中不存在的字段将保留为NULL。如果你从对象中删除了字段,旧版本将始终保留此字段为NULL,如果旧版本依赖此字段中的数据,可能会导致不当行为(无论如何,你创建它是为了某些事情,而不仅仅是为了好玩:-))

最不明显-有时你会改变你在某个领域的含义的想法。例如,当你12岁时,你的意思是“自行车”下的“自行车”,但当你18岁时,你的意思是“摩托车”-如果你的朋友邀请你“骑自行车穿越城市”,而你将是唯一一个骑自行车来的人,你会明白在各个领域保持相同的含义是多么重要:-)

首先回答你的问题,当我们不在类中声明SerialVersionUID时,Java运行时为我们生成它,但是该过程对许多类元数据敏感,包括字段数量,字段类型,字段的访问修饰符,类实现的接口等。

序列化:我们经常使用重要的对象,这些对象的状态(对象变量中的数据)非常重要,以至于我们不能在将对象状态发送到其他机器时因电源/系统故障(或)网络故障而冒失去它的风险。这个问题的解决方案被称为“持久性”,简单地说就是持久化(保存/保存)数据。序列化是实现持久性的许多其他方法之一(通过将数据保存到磁盘/内存)。保存对象状态时,为对象创建一个标识很重要,以便能够正确地读回(反序列化)。这个唯一的标识是ID是SerialVersionUID。

'SerrialVersionUID'是一个64位的数字,用于在反序列化过程中唯一标识一个类。当您序列化一个对象时,该类的SerrialVersionUID也会写入文件。每当您反序列化这个对象时,java运行时都会从序列化的数据中提取这个SerrialVersionUID值,并比较与该类关联的相同值。如果两者不匹配,则会抛出java.io.InvalidClassException。

如果一个可序列化的类没有显式声明序列化版本UID,那么序列化运行时将根据类的各个方面(如字段、方法等)计算该类的序列化版本UID值,您可以参考此链接进行演示应用程序。