这个警告相信大家应该并不陌生,当我们实现了Serializable
接口时,IDEA就会有这个警告,告诉我们实体类必须声明serialVersionUID
属性并赋值。既然有这样的提示,那么也从侧面说明了serialVersionUID
在序列化过程中的重要性。本文就从现象到本质,详细的分析一下serialVersionUID
到底起着怎样的作用。
既然是分析在序列化过程中的作用,那么肯定要先来了解一下什么是序列化和反序列化啦。不过相信大家对于这个概念已经很熟悉,这里就做个简单的介绍。
序列化:把Java对象转转换成字节序列的过程。主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。
反序列化:把字节序列恢复为Java对象的过程。主要的工作就是根据字节流中所保存的对象状态及描述信息,重建Java对象,恢复对象状态。
为了更好的了解serialVersionUID
的作用,我们需要先看两个现象,首先来看第一个现象:如我没有显示的指定serialVersionUID
会怎样呢,是不是就没办法序列化了呢。来看如下代码:
/**
* 实现序列化接口但不提供serialVersionUID
*/
@Data
public class UserWithoutVersion implements Serializable {
private String userNo;
private String name;
}
/**
* 将实体类写入到文件(为了简化代码结构去掉了try-catch)
*/
public class OutputObject {
public static void main(String[] args) throws IOException {
String fileDir = "./src/main/resources/file/";
// 没有serialVersionUID
UserWithoutVersion userWithoutVersion = new UserWithoutVersion();
userWithoutVersion.setUserNo("20200526");
userWithoutVersion.setName("未显示指定serialVersionUID");
// 写入文件
FileOutputStream userWithoutVersionFile = new FileOutputStream(fileDir + "userWithoutVersionFile");
ObjectOutputStream outputStream = new ObjectOutputStream(userWithoutVersionFile);
outputStream.writeObject(userWithoutVersion);
outputStream.close();
}
}
执行上述代码,将UserWithoutVersion
对象进行序列化,代码运行无误,并且已经将对象写入文件,说明没有指定serialVersionUID
仍然可以实现序列化操作,这么看来好像serialVersionUID
并没有起到什么作用嘛。不过别急着下结论,我们接着往下看。假设有一天我们因为某种原因修改了UserWithoutVersion
类,为其新增了一个private Integer age
属性,然后让我们再对文件进行反序列化。
/**
* 将文件反序列化成对象(为了简化代码结构去掉了try-catch)
*/
public class SerialWithoutVersion {
public static void main(String[] args) throws Exception {
String fileName = "./src/main/resources/file/userWithoutVersionFile";
// 将对象进行反序列化
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(fileName));
UserWithoutVersion userWithoutVersion = (UserWithoutVersion) inputStream.readObject();
System.out.println(userWithoutVersion);
}
}
运行代码,直接凉凉,错误信息为java.io.InvalidClassException:local class incompatible: stream classdesc serialVersionUID = -5852468924593134574, local class serialVersionUID = 733911509349552403
。
我们并没指定serialVersionUID
,但是错误信息却提示我们serialVersionUID
不一致,还给出了两个serialVersionUID
的值分别为733911509349552403
和-1218014011296924874
,由此可见,serialVersionUID
在序列化和反序列的过程中起到一个标识的作用,就算我们没有显示的指定serialVersionUID
系统仍然会自动生成一个serialVersionUID
的值。
因此文章开头提到的IDEA的警告提示是非常有道理的,一旦类实现了Serializable
接口,就建议明确的定义一个serialVersionUID
。不然在修改类的时候,就会发生异常。
基于现象一,我们知道了在实现Serializable
接口时需要显示的指定serialVersionUID
,那么以此为基础我们再来看看第二个现象:如果手动修改了serialVersionUID
会产生怎样的影响呢。我们还是通过代码来验证一下:
/**
* 实现序列化接口并且提供serialVersionUID,但是手动修改其值
*/
@Data
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String userNo;
private String name;
}
然后参照步骤一中的流程,先将User
进行序列化操作,序列化完成后我们手动将serialVersionUID
的值改成2L
然后进行反序列化操作,程序不出意外的报错了,错误原因同样也是serialVersionUID
不一致。关于这一现象的解释,我觉得引用《阿里巴巴Java开发手册》中的一条规定十分合适:
分析问题当然是不能只看表面的,我们要透过现象看本质,所以接下来我们来看一下源码,深入理解serialVersionUID
的作用。
为了简化代码结构,重点分析核心代码,这里先给出反序列化过程中的调用链
ObjectInputStream.readObject() -> readObject0() -> readOrdinaryObject() -> readClassDesc() -> readNonProxyDesc() -> ObjectStreamClass.initNonProxy()
在initNonProxy()
方法中 ,关键代码如下:
void initNonProxy(ObjectStreamClass model, Class<?> cl,
ClassNotFoundException resolveEx, ObjectStreamClass superDesc)
throws InvalidClassException {
long suid = Long.valueOf(model.getSerialVersionUID());
ObjectStreamClass osc = null;
if (cl != null) {
······
// 比较文件的serialVersionUID和本地对象的serialVersionUID
if (model.serializable == osc.serializable &&!cl.isArray()
&& suid != osc.getSerialVersionUID()) {
// 抛出异常并显示两者的serialVersionUID
throw new InvalidClassException(osc.name,
"local class incompatible: " +
"stream classdesc serialVersionUID = " + suid +
", local class serialVersionUID = " +
osc.getSerialVersionUID());
}
······
}
}
从这一段代码可以看出,serialVersionUID
是作为序列化和反序列化过程中的唯一标识而存在的,在进行反序列化时,JVM会把传来的字节流中的serialVersionUID
与本地相应实体类的serialVersionUID
进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException
。这样就很好的解释了现象二产生的原因。
接下来我们深入进去查看一下getSerialVersionUID()
方法的源码:
public long getSerialVersionUID() {
// REMIND: synchronize instead of relying on volatile?
if (suid == null) {
suid = AccessController.doPrivileged(
new PrivilegedAction<Long>() {
public Long run() {
return computeDefaultSUID(cl);
}
}
);
}
return suid.longValue();
}
可以看到,在获取字节流和实体类中的serialVersionUID
时,如果值为null
就会调用computeDefaultSUID()
方法生成默认的serialVersionUID
,这也就解释了现象一:没有指定serialVersionUID
的值,仍然可以序列化。
通过以上的分析可以得出结论:serialVersionUID
在序列化过程中起到唯一标识的作用,是用来验证版本一致性的,只有当serialVersionUID
一致时才能反序列化成功,否则将会抛出java.io.InvalidClassException
。因此在如果一个类实现了Serializable
接口,一定要指定serialVersionUID
,否则如果在版本迭代过程中改变了实体类的属性,反序列化就会报错。同时,在做兼容性升级的时候不要改变类中的serialVersionUID
的值。比如各个版本的JDK中String
类中的serialVersionUID
一直都是-6849794470754667710L
。
文章开头提到的IDEA中的警告提示默认是关闭的,需要我们手动打开,Editor -> Inspections -> Serializable class without serialVersionUID
勾选这个选项就可以了,然后在实现序列化接口的就会有提示,并且鼠标悬停在警告上,还会出现自动生成serialVersionUID
的选项,十分方便。