为什么不能轻易修改 serialVersionUID 字段

简介: 为什么不能轻易修改 serialVersionUID 字段


阿里巴巴开发手册,(四)OOP 规约,第 13 条解释如下:

【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果 完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。 说明:注意 serialVersionUID 不一致会抛出序列化运行时异常。

首先需要解释一下这条规则,并不是要求你一定不可以修改,而是根据自己的需要来修改。我们先了解一下 serialVersionUID 是干嘛的。

序列化

首先我们需要了解一下序列化,我们可以简单了理解序列化就是把 Java 对象转换成另一种形态的数据,这种形态的数据可以用于存储或者是传输。因为本身 Java 对象是存在内存中,没有办法直接存储或者是传输,序列化的出现来解决这个问题,那么反序列化就是把形态数据重新转换为 Java 对象。当然这个形态可以是多种格式,比如我们详知的 JSON,或者是 Byte,也可以是我们的自定义形式,比如 K-V 形式,如下图。

Java 默认序列化

说到 serialVersionUID 就不得不说到 Java 默认的序列化,为了提供上文我们说的存储和传出,Java 默认提供了一种序列化方式。只要序列化的类实现 java.io.Serializable 接口,这样就可以做序列化和反序列化了。我简单罗列了一下代码这样更直观(有所删减)。

User.java

public class User implements java.io.Serializable {
    private String name;
    public User(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return name;
    }
}
复制代码

SerializerTest.java

User user = new User("码匠笔记");
FileOutputStream fo = new FileOutputStream("user.bytes");
ObjectOutputStream so = new ObjectOutputStream(fo);
so.writeObject(user);
FileInputStream fi = new FileInputStream("user.bytes");
ObjectInputStream si = new ObjectInputStream(fi);
user = (User) si.readObject();
复制代码

代码已经比较直观,User 实现了 Serializable 接口,通过 ObjectOutputStream 把类转化为字节码存入 user.bytes,然后再使用 ObjectInputStream 把字节码从 user.bytes 读入内存。 这样非常简单就实现了 Java 的序列化,说了这么多它真的有用途吗?当然,比如Java 自带的远程调用组件 RMI

serialVersionUID

终于说到重点了,为什么不能轻易修改 serialVersionUID?可是上面的代码中我们明明就没有设置 serialVersionUID。那我们调整一下例子,再测试一次。 User.java

public class User implements java.io.Serializable {
    private String name;
    public User(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return name;
    }
}
复制代码

UserSerializeTest.java

public class UserSerializeTest {
    public static void main(String[] args) throws Exception {
        User user = new User("码匠笔记");
        FileOutputStream fo = new FileOutputStream("user.bytes");
        ObjectOutputStream so = new ObjectOutputStream(fo);
        so.writeObject(user);
        so.close();
    }
}
复制代码

User.java

public class User implements java.io.Serializable {
    private String name;
    private String desc;
    public User(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return name;
    }
}
复制代码

UserDeserializeTest.java

public class UserDeserializeTest {
    public static void main(String[] args) throws Exception {
        FileInputStream fi = new FileInputStream("user.bytes");
        ObjectInputStream si = new ObjectInputStream(fi);
        User user = (User) si.readObject();
        System.out.println(user);
        si.close();
    }
}
复制代码

如上代码,我们先运行 UserSerializeTest.java,然后修改 User.java,添加一个属性 desc,然后再次运行代码。果然出错了?

Exception in thread "main" java.io.InvalidClassException: 
com.github.codedrinker.p1413.User; 
local class incompatible: 
stream classdesc serialVersionUID = 6360520658036414457, 
local class serialVersionUID = -3025746955499933156
复制代码

显示 serialVersionUID 不相同,反序列化失败了,可是我们没有定义 serialVersionUID 是为什么呢?(所有源码文末均有获取方式)

是时候让源码君出面了,我们查看 java.io.ObjectStreamClass#writeNonProxy ,如果当前类(User)没有定义 serialVersionUID,就会调用java.io.ObjectStreamClass#computeDefaultSUID生成默认的序列化唯一标示。我们简单的看代码,发现他的生成规则是根据类名,结果明,方法和属性等参数生成的 hash 值,所以我们给 User 添加了 desc 属性,所以对应的 serialVersionUID 肯定会变化。

JVM 规范[1] 里面也有具体的解释

The stream-unique identifier is 
a 64-bit hash of the class name, 
interface class names, 
methods, and fields. 
复制代码

这里我们也可以使用 JDK 自带的工具 serialver 来验证一下,这个工具和 JDK 默认的生成 serialVersionUID 的规则一样。

serialver com.github.codedrinker.p1413.User
复制代码

可以得到添加 desc 前后的 serialVersionUID ,输出如下

// 未添加 desc
 private static final long serialVersionUID 
 = 6360520658036414457L;
 // 添加 desc
 private static final long serialVersionUID 
 = -3025746955499933156L;
复制代码

好了,所以看到这里我们修改一个地方就可以解决这个问题了。手工定义一个 serialVersionUID 代码如下 User.java

public class User implements java.io.Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    public User(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return name;
    }
}
复制代码

这样再重复上面的运行步骤,就可以成功的做反序列化了。

到这里我们就全部明白了为什么文档里面说明不能轻易的修改 serialVersionUID 了。但是每次定义成 1L 也不是办法,所以可以配置一下 IDEA,这样就可以创建类的时候提示自动生成了。

其他序列化方式

手册里面只提到了 Java 默认的序列化方式,其实还有很多性能很不错的序列化方式,正如上文中提到的 JSON 或是字节流,比如现在比较流行的几种:HessianKryoFastjsonProtoBufJackson 等,具体在这里就不展开了,他们都有自己的优缺点,下面是 jvm-serializers[2] 输出的它们的性能比较,可以在选择的时候有限参考下。




目录
相关文章
|
13天前
|
SQL
将查询出来数据中相对应的字段根据枚举类更改为其中文内容
将查询出来数据中相对应的字段根据枚举类更改为其中文内容
|
Java 索引
【Java 虚拟机原理】Class 字节码二进制文件分析 三 ( 访问和修饰标志 | 类索引 | 父类索引 | 接口计数器 | 接口表 | 字段计数器 | 字段表 )(二)
【Java 虚拟机原理】Class 字节码二进制文件分析 三 ( 访问和修饰标志 | 类索引 | 父类索引 | 接口计数器 | 接口表 | 字段计数器 | 字段表 )(二)
107 0
【Java 虚拟机原理】Class 字节码二进制文件分析 三 ( 访问和修饰标志 | 类索引 | 父类索引 | 接口计数器 | 接口表 | 字段计数器 | 字段表 )(二)
|
Java 索引
【Java 虚拟机原理】Class 字节码二进制文件分析 三 ( 访问和修饰标志 | 类索引 | 父类索引 | 接口计数器 | 接口表 | 字段计数器 | 字段表 )(三)
【Java 虚拟机原理】Class 字节码二进制文件分析 三 ( 访问和修饰标志 | 类索引 | 父类索引 | 接口计数器 | 接口表 | 字段计数器 | 字段表 )(三)
97 0
【Java 虚拟机原理】Class 字节码二进制文件分析 三 ( 访问和修饰标志 | 类索引 | 父类索引 | 接口计数器 | 接口表 | 字段计数器 | 字段表 )(三)
|
存储 SQL JSON
如何不改表结构动态扩展字段?
痛点 软件行业唯一不变的就是变化,比如功能上线之后,客户或 PM 需要对已有的功能增加一些合理的需求,完成这些工作必须通过添加字段解决,或者某些功能的实现需要通过增加字段来降低实现的复杂性等等。
665 0
如何不改表结构动态扩展字段?
去掉serialVersionUID的警告
去掉serialVersionUID的警告
89 0
|
存储 IDE Java
为什么阿里巴巴禁止开发人员修改serialVersionUID 字段的值
介绍一下关于serialVersionUID 。这个字段到底有什么用?如果不设置会怎么样?为什么《Java开发手册》中有那样的规定?
为什么阿里巴巴禁止开发人员修改serialVersionUID 字段的值
|
程序员
Attribute(特性),怎么用才更好? —— 字段编号被误解了
  上一篇里(Attribute(特性),怎么用才更好? ),有人说,“坚决杜绝magic number ”,这个magic number指的就是字段编号吧,其实您误解了。   一提到字段编号,可能有些人的第一反应就是这样的用法:     Person1.2000020,或者Person1[2000020],或者ds[2000020]。
861 0
|
Java Scala
slick对超过22个属性的表进行映射的两种办法
版权声明:本文为博主原创文章,未经博主允许不得转载 slick是scala的一个FRM(Functional Relational Mapper)框架,即函数式的关系数据库编程工具库。使用slick不同于使用java的hibernate或者是mybatis,对其进行迭代开发非常方便,因为其对表的映射基于函数式的编程方式。
1085 0

热门文章

最新文章