序列化过程中serialVersionUID到底有什么用

serialVersionUID警告
这个警告相信大家应该并不陌生,当我们实现了Serializable接口时,IDEA就会有这个警告,告诉我们实体类必须声明serialVersionUID属性并赋值。既然有这样的提示,那么也从侧面说明了serialVersionUID在序列化过程中的重要性。本文就从现象到本质,详细的分析一下serialVersionUID到底起着怎样的作用。

序列化与反序列化

既然是分析在序列化过程中的作用,那么肯定要先来了解一下什么是序列化和反序列化啦。不过相信大家对于这个概念已经很熟悉,这里就做个简单的介绍。

序列化:把Java对象转转换成字节序列的过程。主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。

反序列化:把字节序列恢复为Java对象的过程。主要的工作就是根据字节流中所保存的对象状态及描述信息,重建Java对象,恢复对象状态。

现象一:不指定serialVersionUID会怎样

为了更好的了解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。不然在修改类的时候,就会发生异常。

现象二:如果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的选项,十分方便。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值