什么是serialVersionUID?为什么要使用它?
当 serialVersionUID
丢失时,Eclipse 会发出警告。
可序列化类 Foo 未声明静态final long 类型的serialVersionUID 字段
什么是 serialVersionUID
以及为什么它很重要? 请举例说明缺少 serialVersionUID
会导致问题的情况。
Eclipse issues warnings when a serialVersionUID
is missing.
The serializable class Foo does not declare a static final
serialVersionUID field of type long
What is serialVersionUID
and why is it important? Please show an example where missing serialVersionUID
will cause a problem.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
发布评论
评论(26)
如果您在一个从未考虑过序列化的类上收到此警告,并且您没有声明自己实现了 Serialized
,那么通常是因为您继承自实现了 Serialized 的超类。 通常,委托给这样的对象而不是使用继承会更好。
因此,
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 方法。 我认为在这种情况下不应该出现警告(因为您通常无法在类的不同版本中可靠地序列化和反序列化此类匿名类),但我不确定编译器如何识别这一点。
要理解字段serialVersionUID的重要性,应该了解序列化/反序列化的工作原理。
当序列化类对象被序列化时,Java 运行时将序列化版本号(称为serialVersionUID)与该序列化对象相关联。 当您反序列化此序列化对象时,Java 运行时将序列化对象的serialVersionUID 与类的serialVersionUID 进行匹配。 如果两者相等,则仅继续进行进一步的反序列化过程,否则抛出 InvalidClassException。
因此我们得出结论,为了使序列化/反序列化过程成功,序列化对象的serialVersionUID必须等于类的serialVersionUID。 如果程序员在程序中显式指定serialVersionUID值,则相同的值将与序列化对象和类关联,而不管序列化和反序列化平台如何(例如,序列化可以在像windows这样的平台上使用sun或MS JVM 和反序列化可能位于使用 Zing JVM 的不同平台 Linux 上。
但是,如果程序员没有指定serialVersionUID,那么在对任何对象进行序列化\反序列化时,Java运行时将使用自己的算法来计算它。 这个serialVersionUID计算算法因JRE而异。 也有可能对象序列化的环境使用一个 JRE(例如:SUN JVM),而发生反序列化的环境使用 Linux Jvm(zing)。 在这种情况下,与序列化对象关联的serialVersionUID将不同于在反序列化环境中计算的类的serialVersionUID。 反过来反序列化也不会成功。 因此,为了避免这种情况/问题,程序员必须始终指定可序列化类的serialVersionUID。
至于缺少serialVersionUID可能会导致问题的示例:
我正在开发这个Java EE应用程序,该应用程序由使用EJB
模块的Web模块组成。 Web 模块远程调用 EJB
模块,并传递实现 Serialized
的 POJO
作为参数。
这个 POJO 类被打包在 EJB jar 中以及 Web 模块的 WEB-INF/lib 中它自己的 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
:当我知道它将离开 Java VM 的上下文时。
当我为我的应用程序使用 ObjectInputStream
和 ObjectOutputStream
时,或者如果我知道我使用的库/框架将使用它时,我就会知道这一点。 SerialVersionID 确保不同版本或供应商的不同 Java VM 能够正确互操作,或者如果在 VM 外部存储和检索它(例如 HttpSession
),则即使在应用程序重新启动和升级期间,会话数据也可以保留服务器。
对于所有其他情况,我都会使用,
@SuppressWarnings("serial")
因为大多数时候默认的 serialVersionUID
就足够了。 这包括Exception
、HttpServlet
。
为什么在 Java 中的 Serialized
类中使用 SerialVersionUID
?
在序列化
期间,Java 运行时为类创建一个版本号,以便稍后可以对其进行反序列化。 此版本号在 Java 中称为 SerialVersionUID
。
SerialVersionUID
用于对序列化数据进行版本控制。 仅当类的 SerialVersionUID
与序列化实例匹配时,您才能反序列化该类。 当我们不在类中声明 SerialVersionUID
时,Java 运行时会为我们生成它,但不建议这样做。 建议将 SerialVersionUID
声明为 private static final long
变量以避免默认机制。
当您通过实现标记接口 java.io.Serialized 将类声明为可序列化时,Java 运行时将使用默认的序列化机制将该类的实例保留到磁盘中,前提是您没有这样做使用Externalized
接口自定义流程。
SerialVersionUID用于对象的版本控制。 您也可以在类文件中指定serialVersionUID。 不指定serialVersionUID的后果是,当您添加或修改类中的任何字段时,已序列化的类将无法恢复,因为为新类和旧序列化对象生成的serialVersionUID将不同。 Java 序列化过程依赖于正确的serialVersionUID 来恢复序列化对象的状态,并在serialVersionUID 不匹配的情况下抛出java.io.InvalidClassException
阅读更多:http://javarevisited.blogspot.com/2011/04/top-10-java-serialization-interview.html#ixzz3VQxnpOPZ
如果你想修改大量没有设置serialVersionUID的类,同时保持与旧类的兼容性,像IntelliJ Idea、Eclipse这样的工具就不够了,因为它们会生成随机数,并且不能处理一堆文件一气呵成。 我提出了以下 bash 脚本(对于 Windows 用户,我很抱歉,请考虑购买 Mac 或转换为 Linux)来轻松修改 serialVersionUID 问题:
base_dir=$(pwd)
src_dir=$base_dir/src/main/java
ic_api_cp=$base_dir/target/classes
while read f
do
clazz=${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/$f
done
保存此脚本,将 add_serialVersionUID.sh 保存到 ~/bin 中。 然后,您在 Maven 或 Gradle 项目的根目录中运行它,如下所示:
add_serialVersionUID.sh < myJavaToAmend.lst
此 .lst 包含用于添加serialVersionUID 的 java 文件列表,格式如下:
com/abc/ic/api/model/domain/item/BizOrderTransDO.java
com/abc/ic/api/model/domain/item/CardPassFeature.java
com/abc/ic/api/model/domain/item/CategoryFeature.java
com/abc/ic/api/model/domain/item/GoodsFeature.java
com/abc/ic/api/model/domain/item/ItemFeature.java
com/abc/ic/api/model/domain/item/ItemPicUrls.java
com/abc/ic/api/model/domain/item/ItemSkuDO.java
com/abc/ic/api/model/domain/serve/ServeCategoryFeature.java
com/abc/ic/api/model/domain/serve/ServeFeature.java
com/abc/ic/api/model/param/depot/DepotItemDTO.java
com/abc/ic/api/model/param/depot/DepotItemQueryDTO.java
com/abc/ic/api/model/param/depot/InDepotDTO.java
com/abc/ic/api/model/param/depot/OutDepotDTO.java
此脚本在后台使用 JDK serialVer 工具。 因此,请确保您的 $JAVA_HOME/bin 在 PATH 中。
Joshua Bloch 在《Effective Java》中详细记录了这个问题。 一本非常好的书,必读。 我将概述以下一些原因:
序列化运行时为每个可序列化类提供一个称为“序列版本”的数字。 这个数字称为serialVersionUID。 现在这个数字背后有一些数学,它是根据类中定义的字段/方法得出的。 对于同一个类,每次都会生成相同的版本。 在反序列化期间使用此数字来验证序列化对象的发送者和接收者是否已为该对象加载了与序列化兼容的类。 如果接收方加载的对象类与相应发送方的类具有不同的serialVersionUID,则反序列化将导致 InvalidClassException。
如果类是可序列化的,您还可以通过声明名为“serialVersionUID”的字段来显式声明您自己的serialVersionUID,该字段必须是静态的、最终的且类型为long。 大多数 IDE(例如 Eclipse)可以帮助您生成那么长的字符串。
每次序列化对象时,都会为该对象的类打上版本 ID 号的标记。此 ID 称为 serialVersionUID 且它是根据有关类结构的信息计算的。 假设你创建了一个 Employee 类,它的版本 ID #333(由 JVM 分配),现在当你序列化该类的对象(假设 Employee 对象)时,JVM 会将 UID 分配给它#333。
考虑一种情况 - 将来您需要编辑或更改您的类,在这种情况下,当您修改它时,JVM 将为它分配一个新的 UID(假设#444)。
现在,当您尝试反序列化员工对象时,JVM 会将序列化对象(员工对象)的版本 ID(#333)与类的版本 ID(#444)进行比较(因为它已更改)。 比较后,JVM 会发现两个版本的 UID 不同,因此反序列化将失败。
因此,如果每个类的serialVersionID是由程序员自己定义的。 即使类将来进化,也会是一样的,因此即使类发生了变化,JVM 总会发现该类与序列化对象兼容。 更多信息可以参考 HEAD FIRST JAVA 第 14 章。
长话短说,该字段用于检查序列化数据是否可以正确反序列化。 序列化和反序列化通常由程序的不同副本进行 - 例如服务器将对象转换为字符串,客户端将接收到的字符串转换为对象。 该字段表明两者对于该对象是什么有相同的想法。 此字段在以下情况下会有所帮助:
您在不同位置有许多不同的程序副本(例如 1 个服务器和 100 个客户端)。 如果你要改变你的对象,改变你的版本号,并且忘记更新这个客户端,它就会知道他不能反序列化
您已将数据存储在某个文件中,稍后您尝试使用更新的文件打开它带有修改对象的程序版本 - 如果您保持版本正确,您就会知道该文件不兼容
什么时候它很重要?
最明显的是 - 如果您向对象添加一些字段,旧版本将无法使用它们,因为它们的对象结构中没有这些字段。
不太明显 - 当您反序列化对象时,字符串中不存在的字段将保留为 NULL。 如果您从对象中删除了字段,旧版本会将此字段保留为 allways-NULL,如果旧版本依赖于此字段中的数据,则可能会导致错误行为(无论如何,您创建它是为了某些东西,而不仅仅是为了好玩:-))
最不明显 - 有时您会改变在某些字段的含义中所表达的想法。 例如,当您 12 岁时,您的意思是“自行车”下的“自行车”,但当您 18 岁时,您的意思是“摩托车” - 如果您的朋友邀请您“骑自行车穿越城市”,那么您将是唯一一个骑着自行车来,你就会明白在不同领域保持相同的含义是多么重要:-)
“serialVersionUID”是一个 64 位数字,用于在反序列化过程中唯一标识一个类。 当序列化一个对象时,该类的serialVersionUID也会写入该文件。 每当反序列化该对象时,java运行时都会从序列化数据中提取该serialVersionUID值,并将与该类关联的相同值进行比较。 如果两者不匹配,则会抛出“java.io.InvalidClassException”。
如果一个可序列化的类没有显式声明一个serialVersionUID,那么序列化运行时将根据该类的各个方面(如字段、方法等)计算该类的serialVersionUID值,您可以参考这个演示应用程序的链接。
首先回答你的问题,当我们不在类中声明 SerialVersionUID 时,Java 运行时会为我们生成它,但该过程对许多类元数据敏感,包括字段数量、字段类型、字段访问修饰符、实现的接口因此,建议我们自己声明它,并且 Eclipse 会警告您同样的情况。
序列化:
我们经常使用重要的对象,其状态(对象变量中的数据)非常重要,以至于在将对象状态发送到其他机器时,我们不能冒因电源/系统故障(或)网络故障而丢失它的风险。 这个问题的解决方案被称为“持久化”,它的意思就是持久化(保存/保存)数据。 序列化是实现持久性的许多其他方法之一(通过将数据保存到磁盘/内存)。 保存对象的状态时,为对象创建一个标识非常重要,以便能够正确地读回它(反序列化)。 这个唯一标识ID就是SerialVersionUID。
您不应该不手动定义serialVersionUID,至少在没有仔细考虑的情况下是这样。
许多教程、开发人员和 linting 工具对这个领域都有错误的想法。 lombok-team 的这篇优秀文章解释了为什么工具不能生成serialVersionUID,并且在这个过程中也解释了许多开发人员对这个概念的误解,我强烈建议在尝试任何“解决方案”之前阅读整篇文章这个问题。
https://github.com/projectlombok/lombok/wiki/WHY-NOT :-serialVersionUID
它们解释了 Java 中的serialVersionUID 机制的作用:
- 所有类都有一个serialVersionUID,无论您是否定义它。
- 任何序列化对象除了其类名和数据之外还存储serialVersionUID
- 如果序列化对象的serialVersionUID 与当前执行的JVM 中同一类的serialVersionUID 不匹配,则反
- 序列化失败 每次类更改时,默认的serialVersionUID 都会更改。
不定义serialVersionUID所获得的默认行为实际上正是您想要的。如果可序列化类发生更改,系统会阻止您将旧数据反序列化到其中,因为这可能会产生垃圾。
仅当您想要将旧数据反序列化到修改后的类中时,才需要通过在类上定义一个serialVersionUID来告诉Java这是可以的,并确保您实际上可以读取旧数据嗯>。 您可以通过不进行损害二进制兼容性的更改或使用 writeReplace/readResolve 或 writeObject/readObject 来实现此目的。
这很困难,并且正确地实现这一点的责任在于程序员。 你真的应该考虑一下是否有必要。
java.io.Serialized
可能是您得到的最好的解释:
序列化运行时与每个可序列化类关联一个版本号,称为
serialVersionUID
,它在反序列化期间使用,以验证序列化对象的发送者和接收者是否已为该对象加载了以下类:与序列化兼容。 如果接收方为对象加载的类与相应发送方的类具有不同的serialVersionUID
,则反序列化将导致InvalidClassException< /代码>
。 可序列化类可以通过声明名为
serialVersionUID
的字段来显式声明自己的serialVersionUID
,该字段必须是静态、最终且类型为long
:
ANY-ACCESS-MODIFIER 静态最终长serialVersionUID = 42L;
如果可序列化类未显式声明
serialVersionUID
,则序列化运行时将根据该类的各个方面计算该类的默认serialVersionUID
值,如下所示Java(TM) 对象序列化规范中进行了描述。 但是,强烈建议所有可序列化类显式声明serialVersionUID
值,因为默认serialVersionUID
计算对可能变化的类详细信息高度敏感取决于编译器实现,因此可能会在反序列化期间导致意外的InvalidClassExceptions
。 因此,为了保证不同 java 编译器实现之间的serialVersionUID
值一致,可序列化类必须声明显式serialVersionUID
值。 还强烈建议显式serialVersionUID
声明尽可能使用 private 修饰符,因为此类声明仅适用于立即声明的类 —serialVersionUID
字段作为继承成员没有用处。
我不能错过这个机会来插入 Josh Bloch 的书 Effective Java(第 2 期)版)。 第10章是Java序列化的必备资源。
根据 Josh 的说法,自动生成的 UID 是根据类名、实现的接口以及所有公共和受保护成员生成的。 以任何方式更改其中任何一个都会更改 serialVersionUID
。 因此,只有当您确定不会序列化该类的多个版本(无论是跨进程还是稍后从存储中检索)时,您才不需要弄乱它们。
如果你暂时忽略它们,后来发现你需要以某种方式更改类但保持与旧版本类的兼容性,你可以使用 JDK 工具 serialver 生成 serialVersionUID
在旧类上,并在新类上显式设置它。 (根据您的更改,您可能还需要通过添加 writeObject 和 readObject 方法来实现自定义序列化 - 请参阅 javadoc 或上述第 10 章。)
serialVersionUID
有助于序列化数据的版本控制。 序列化时其值与数据一起存储。 反序列化时,会检查相同版本,看看序列化数据与当前代码的匹配情况。
如果您想对数据进行版本控制,通常从 serialVersionUID
0 开始,并在类的每次结构更改中更改序列化数据(添加或删除非瞬态字段)。
内置的反序列化机制 (in.defaultReadObject()
) 将拒绝对旧版本的数据进行反序列化。 但如果您愿意,您可以定义自己的 readObject() - 可以读回旧数据的函数。 然后,此自定义代码可以检查 serialVersionUID
以了解数据所在的版本并决定如何反序列化它。 如果您存储的序列化数据在代码的多个版本中仍然存在,则此版本控制技术非常有用。
但存储如此长的时间跨度的序列化数据并不常见。 更常见的是使用序列化机制将数据临时写入缓存等,或通过网络将其发送到具有相同版本的代码库相关部分的另一个程序。
在这种情况下,您对维护向后兼容性不感兴趣。 您只关心确保正在通信的代码库确实具有相同版本的相关类。 为了便于进行此类检查,您必须像以前一样维护 serialVersionUID
,并且在更改类时不要忘记更新它。
如果您确实忘记更新该字段,则可能会得到一个类的两个不同版本,它们具有不同的结构,但具有相同的 serialVersionUID
。 如果发生这种情况,默认机制 (in.defaultReadObject()
) 将不会检测到任何差异,并尝试反序列化不兼容的数据。 现在,您可能会遇到神秘的运行时错误或静默失败(空字段)。 这些类型的错误可能很难发现。
因此,为了帮助此用例,Java 平台为您提供了不手动设置 serialVersionUID
的选择。 相反,类结构的哈希值将在编译时生成并用作 id。 这种机制将确保您永远不会拥有具有相同 id 的不同类结构,因此您不会遇到上面提到的这些难以跟踪的运行时序列化失败。
但自动生成的 ID 策略有一个缺点。 也就是说,不同编译器为同一类生成的 id 可能不同(如上面 Jon Skeet 提到的)。 因此,如果您在使用不同编译器编译的代码之间传递序列化数据,建议无论如何手动维护 ids。
如果您像前面提到的第一个用例那样向后兼容数据,您可能还想自己维护 id。 这是为了获得可读的 ID 并更好地控制它们何时以及如何更改。
什么是serialVersionUID以及为什么要使用它?
SerialVersionUID
是每个类的唯一标识符,JVM
使用它来比较类的版本,确保在反序列化期间加载序列化期间使用的相同类。
指定一个可以提供更多控制,尽管如果您不指定,JVM 也会生成一个。 不同编译器生成的值可能不同。 此外,有时您只是出于某种原因想要禁止旧序列化对象的反序列化[向后不兼容
],在这种情况下,您只需更改serialVersionUID。
默认的serialVersionUID计算对类高度敏感
细节可能会因编译器实现而异,并且可以
因此会导致意外的InvalidClassException
反序列化。
因此,您必须声明serialVersionUID,因为它给我们更多的控制权。
本文对此主题有一些很好的观点。
最初的问题询问了“为什么它很重要”和“示例”,其中此序列版本 ID
会很有用。 好吧,我找到了一个。
假设您创建一个 Car
类,实例化它,并将其写入对象流。 展平的汽车对象会在文件系统中保留一段时间。 同时,如果通过添加新字段来修改 Car
类。 稍后,当您尝试读取(即反序列化)扁平化的 Car
对象时,您会得到 java.io.InvalidClassException
– 因为所有可序列化的类都会自动获得唯一的标识符。 当类的标识符不等于扁平化对象的标识符时,会抛出此异常。 如果你认真思考一下,抛出异常是因为添加了新字段。 您可以通过声明显式的serialVersionUID 自行控制版本控制来避免引发此异常。 显式声明您的 serialVersionUID
也有一个小的性能优势(因为不需要计算)。 因此,最佳实践是在创建可序列化类后立即将您自己的serialVersionUID添加到它们中,如下所示:
public class Car {
static final long serialVersionUID = 1L; //assign a long value
}
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
首先我需要解释一下什么是序列化。
序列化允许将对象转换为流,以便通过网络发送该对象或保存到文件或保存到数据库以供以后使用。
序列化有一些规则。
仅当对象的类或其超类实现 Serialized 接口时,对象才是可序列化的
对象是可序列化的(其本身实现了 Serialized 接口),即使其超类不是可序列化的。 但是,可序列化类层次结构中的第一个超类(未实现 Serialized 接口)必须具有无参数构造函数。 如果违反了这一点,readObject()将在运行时产生java.io.InvalidClassException
所有基本类型都是可序列化的。
瞬态字段(带有瞬态修饰符)不会被序列化(即不保存或恢复)。 实现 Serialized 的类必须标记不支持序列化的类(例如文件流)的瞬态字段。
静态字段(带有 static 修饰符)未序列化。
当
Object
被序列化时,Java 运行时会关联序列版本号,即serialVersionID
。我们需要serialVersionID的地方:
在反序列化期间验证发送方和接收方在序列化方面是否兼容。 如果接收方使用不同的
serialVersionID
加载类,则反序列化将以InvalidClassCastException
结束。可序列化类可以通过声明名为
serialVersionUID
的字段来显式声明自己的serialVersionUID
,该字段必须是 static、final 且类型为 long。让我们通过一个例子来尝试一下。
创建序列化对象
反序列化对象
注意:现在更改 Employee 类的serialVersionUID 并保存:
并执行 Reader 类。 不执行 Writer 类,您将得到异常。
First I need to explain what serialization is.
Serialization allows to convert an object to a stream, for sending that object over the network OR Save to file OR save into DB for later usage.
There are some rules for serialization.
An object is serializable only if its class or its superclass implements the Serializable interface
An object is serializable (itself implements the Serializable interface) even if its superclass is not. However, the first superclass in the hierarchy of the serializable class, that does not implements Serializable interface, MUST have a no-arg constructor. If this is violated, readObject() will produce a java.io.InvalidClassException in runtime
All primitive types are serializable.
Transient fields (with transient modifier) are NOT serialized, (i.e., not saved or restored). A class that implements Serializable must mark transient fields of classes that do not support serialization (e.g., a file stream).
Static fields (with static modifier) are not serialized.
When
Object
is serialized, Java Runtime associates the serial version number aka, theserialVersionID
.Where we need serialVersionID:
During the deserialization to verify that sender and receiver are compatible with respect to serialization. If the receiver loaded the class with a different
serialVersionID
then deserialization will end withInvalidClassCastException
.A serializable class can declare its own
serialVersionUID
explicitly by declaring a field namedserialVersionUID
that must be static, final, and of type long.Let's try this with an example.
Create Serialize Object
Deserialize the object
NOTE: Now change the serialVersionUID of the Employee class and save:
And execute the Reader class. Not to execute the Writer class and you will get the exception.