当前位置: 首页 > 科技观察

下次杀了你,我不敢随便改SerialVersionUID

时间:2023-03-14 23:44:49 科技观察

序列化是对象持久化的一种手段。广泛应用于网络传输、RMI等场景。类通过实现java.io.Serializable接口来启用其序列化功能。不过还有一个知识点没有介绍,那就是关于serialVersionUID。这个字段有什么用?如果没有设置会怎样?《阿里巴巴Java开发手册》中为什么会有如下规定:背景知识在介绍本文之前,先简单介绍一下连载相关的一些知识。java.io.Serializable接口以启用其序列化功能。不实现此接口的类不能被序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。如果读者看过Serializable的源码,就会发现它只是一个空接口,里面什么都没有。Serializable接口没有方法或字段,仅用于标识serializable的语义。但是,如果一个类没有实现这个接口,想要序列化,就会抛出java.io.NotSerializableException。它如何保证只有实现接口的方法才能被序列化和反序列化?原因是在序列化过程中,会执行如下代码:在进行序列化操作时,会判断被序列化的优化类是否为Enum、Array或Serializable类型,如果不是,则直接抛出NotSerializableException。Java还提供了Externalizable接口,实现它也可以提供序列化能力。Externalizable继承自Serializable,在该接口中定义了两个抽象方法:writeExternal()和readExternal()。在使用Externalizable接口进行序列化和反序列化时,开发者需要重写writeExternal()和readExternal()方法。否则,所有变量的值都会变成默认值。transienttransient关键字的作用是控制变量的序列化。在变量声明前加上这个关键字可以防止变量被序列化到文件中。反序列化后,瞬态变量的值被设置为初始值。比如int类型为0,object类型为null。自定义序列化策略在序列化过程中,如果被序列化的类定义了writeObject和readObject方法,虚拟机会尝试调用对象类中的writeObject和readObject方法进行用户自定义的序列化和反序列化。如果没有这个方法,默认调用ObjectOutputStream的defaultWriteObject方法和ObjectInputStream的defaultReadObject方法。用户自定义的writeObject和readObject方法允许用户控制序列化过程,例如序列化的值可以在序列化过程中动态改变。因此,当需要为一些特殊字段定义序列化策略时,可以考虑使用transient修饰,自己重写writeObject和readObject方法,比如java.util.ArrayList就有这样的实现。以上是一些读者需要掌握的连载相关知识。我们随便找了几个java中实现了序列化接口的类,比如String,Integer等,可以发现一个细节,就是这些类除了实现了Serializable之外,还定义了一个serialVersionUID。那么,serialVersionUID是什么?为什么要设置这样一个字段?什么是serialVersionUID序列化是将对象的状态信息转换成可以存储或传输的形式的过程。我们都知道Java对象是存放在JVM的堆内存中的,也就是说,如果JVM堆不存在,那么这个对象就会消失。序列化提供了一种解决方案,即使JVM宕机,您也可以保存对象。就像我们平时使用的U盘一样。将Java对象序列化成可以存储或传输的形式(比如二进制流),比如保存在文件中。这样,当再次需要对象时,从文件中读取二进制流,从二进制流中反序列化对象。虚拟机是否允许反序列化,不仅仅取决于类路径和函数代码是否一致,还有很重要的一点就是两个类的序列化ID是否一致。这个所谓的序列化ID就是我们在代码中定义的serialVersionUID。如果serialVersionUID改变了会发生什么我们举个例子看看如果serialVersionUID被修改了会发生什么?我们先执行上面的代码,向文件中写入一个User1对象。然后我们修改User1类,将serialVersionUID的值改为2L。然后执行如下代码反序列化文件中的对象:执行结果如下:java.io.InvalidClassException:com.hollis.User1;localclassincompatible:streamclassdescserialVersionUID=1,localclassserialVersionUID=2可以查到,上面的代码抛出java.io.InvalidClassException并声明serialVersionUID不一致。这是因为,在反序列化时,JVM会将传入字节流中的serialVersionUID与对应的本地实体类的serialVersionUID进行比较。如果它们相同,则认为它们是一致的,可以反序列化,否则它们将序列化版本不一致的异常是InvalidCastException。这也是《阿里巴巴Java开发手册》中规定在兼容性升级中,修改class时,不修改serialVersionUID的原因。除非是完全不兼容的两个版本。因此,serialVersionUID实际上是验证版本一致性。有兴趣的读者可以看看各个版本的JDK代码。那些向后兼容类的serialVersionUID没有改变。比如String类的serialVersionUID一直是-6849794470754667710L。不过笔者认为这个规范其实可以更严格一些,即如果一个类实现了Serializable接口,就必须手动添加一个privatestaticfinallongserialVersionUID变量并设置初始值。为什么要显式指定一个serialVersionUID如果我们没有在类中显式定义一个serialVersionUID,看看会发生什么。尝试修改上面的demo代码,先用下面的类定义一个对象,没有定义serialVersionUID,写入文件。然后我们修改User1类并为其添加一个属性。Tryingtoreaditfromthefileanddeserializeit.执行结果:java.io.InvalidClassException:com.hollis.User1;localclassincompatible:streamclassdescserialVersionUID=-2986778152837257883,localclassserialVersionUID=7961728318907695402同样,抛出了InvalidClassException,并且指出两个serialVersionUID不同,分别是-2986778152837257883和7961728318907695402。从这里可以看出系统自己添加了一个serialVersionUID。因此,一旦类实现了Serializable,建议显式定义一个serialVersionUID。否则,修改类时,会出现异常。serialVersionUID有两种显示方式:一种是默认1L,例如:privatestaticfinallongserialVersionUID=1L;另一种是根据类名、接口名、成员方法和属性等生成一个64位的哈希字段,例如:privatestaticfinallongserialVersionUID=xxxxL;后一种方法可以借助IDE生成,后面会介绍。其背后的原理很清楚。想知道为什么,我们先看下源码,分析一下为什么修改serialVersionUID会抛出异常。没有明确定义,默认的serialVersionUID是怎么来的呢?为了简化代码量,反向序列化的调用链如下:在initNonProxy中,关键代码如下:在反序列化过程中,比较serialVersionUID,如果发现不相等,则抛出异常直接地。仔细看看getSerialVersionUID方法:当没有定义serialVersionUID时,会调用computeDefaultSUID方法生成一个默认的serialVersionUID。这也找到了上述两个问题的根源。其实在代码中做了严格的校验,未定义时会自动生成一个serialVersionUID。IDEATip为了保证我们不会忘记定义serialVersionUID,我们可以调整IntellijIDEA的配置。实现Serializable接口后,如果没有定义serialVersionUID,IDEA(如eclipse)会提示:并且可以一键生成:当然这个配置默认是不生效的,需要在IDEA中手动设置:在图中标3的地方(SerializableclasswithoutserialVersionUIDconfiguration),打上勾,保存。总结serialVersionUID用于验证版本一致性。所以在做兼容性升级的时候,不要改变类中serialVersionUID的值。特别是,由于本文的标题并不能完全表达本文的全部内容,在此再次强调:由于serialVersionUID是用来验证版本一致性的,所以在做版本升级的时候记得修改这个字段(非兼容)升级)。值哦,这样可以避免序列化的乱七八糟。如果一个类实现了Serializable接口,一定要记得定义serialVersionUID,否则会出现异常。你可以在IDE里设置让他帮你提示,一键快速生成serialVersionUID。之所以会出现异常,是因为在反序列化过程中进行了校验,如果没有明确定义,会根据类名和属性自动生成一个。