为什么阿里巴巴要求程序员谨慎修改serialVersionUID 字段的值

作者:微信小助手

发布时间:2018-12-28T08:29:53

序列化是一种对象持久化的手段。普遍应用在网络传输、RMI等场景中。类通过实现 java.io.Serializable 接口以启用其序列化功能。

在我的博客中,其实已经有多篇文章介绍过序列化了,对序列化的基础知识不够了解的朋友可以参考以下几篇文章:

Java对象的序列化与反序列化
深入分析Java的序列化与反序列化
单例与序列化的那些事儿

在这几篇文章中,我分别介绍过了序列化涉及到的类和接口、如何自定义序列化策略、transient关键字和序列化的关系等,还通过学习ArrayList对序列化的实现源码深入学习了序列化。并且还拓展分析了一下序列化对单例的影响等。

但是,还有一个知识点并未展开介绍,那就是关于serialVersionUID 。这个字段到底有什么用?如果不设置会怎么样?为什么《阿里巴巴Java开发手册》中有以下规定:

背景知识

在展开本文的介绍之前,先来简单介绍一些和序列化有关的知识,内容均来自文章开头的三个文章链接中。


Serializable 和 Externalizable

Java类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法进行序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。

如果读者看过Serializable的源码,就会发现,他只是一个空的接口,里面什么东西都没有。Serializable接口没有方法或字段,仅用于标识可序列化的语义。但是,如果一个类没有实现这个接口,想要被序列化的话,就会抛出java.io.NotSerializableException异常。

它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化的呢?

原因是在执行序列化的过程中,会执行到以下代码:

if (obj instanceof String) {
   writeString((String) obj, unshared);
} else if (cl.isArray()) {
   writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
   writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
   writeOrdinaryObject(obj, desc, unshared);
} else {
   if (extendedDebugInfo) {
       throw new NotSerializableException(
           cl.getName() + "\n" + debugInfoStack.toString());
   } else {
       throw new NotSerializableException(cl.getName());
   }
}

在进行序列化操作时,会判断要被序列化的类是否是EnumArraySerializable类型,如果都不是则直接抛出NotSerializableException

Java中还提供了Externalizable接口,也可以实现它来提供序列化能力。

Externalizable继承自Serializable,该接口中定义了两个抽象方法:writeExternal()readExternal()

当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()readExternal()方法。否则所有变量的值都会变成默认值。

transient

transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

自定义序列化策略

在序列化过程中,如果被序列化的类中定义了writeObjectreadObject 方法,虚拟机会试图调用对象类里的 writeObjectreadObject 方法,进行用户自定义的序列化和反序列化。

如果没有这样的方法,则默认调用是 ObjectOutputStreamdefaultWriteObject 方法以及 ObjectInputStreamdefaultReadObject 方法。

用户自定义的 writeObjectreadObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

所以,对于一些特殊字段需要定义序列化的策略的时候,可以考虑使用transient修饰,并自己重写writeObjectreadObject 方法,如java.util.ArrayList中就有这样的实现。

以上,就是一些读者需要掌握和和序列化有关的知识。

我们随便找几个Java中实现了序列化接口的类,如String、Integer等,我们可以发现一个细节,那就是这些类除了实现了Serializable外,还定义了一个serialVersionUID

那么,到底什么是serialVersionUID呢?为什么要设置这样一个字段呢?

什么是serialVersionUID

序列化是将对象的状态信息转换为可存储或传输的形式的过程。我们都知道,Java对象是保存在JVM的堆内存中的,也就是说,如果JVM堆不存在了,那么对象也就跟着消失了。

而序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。

虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致,这个所谓的序列化ID,就是我们在代码中定义的serialVersionUID

如果serialVersionUID变了会怎样

我们举个例子吧,看看如果serialVersionUID被修改了会发生什么?


public class SerializableDemo1 {
   public static void main(String[] args) {
       //Initializes The Object
       User1 user = new User1();
       user.setName("hollis");
       //Write Obj to File
       ObjectOutputStream oos = null;
       try {
           oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
           oos.writeObject(user);
       } catch (IOException e) {
           e.printStackTrace();
       } finally {
           IOUtils.closeQuietly(oos);
       }
   }
}

class User1 implements Serializable {
   private static final long serialVersionUID = 1L;
   private String name;
   public String getName() {
       return name;
   }
   public void setName(String name) {
       this.name = name;
   }
}

我们先执行以上代码,把一个User1对象写入到文件中。然后我们修改一下User1类,把serialVersionUID的值改为2L

class User1 implements Serializable {
   private static final long serialVersionUID = 2L;
   private String name;
   public String getName() {
       return name;
   }
   public void setName(String name) {
       this.name = name;
   }
}

然后执行以下代码,把文件中的对象反序列化出来:

public