序列化是一种对象持久化的手段。广泛应用于网络传输、RMI等场景。类通过实现java.io.Serializable接口来启用其序列化功能。在我的博客中,其实有很多文章介绍了连载。对序列化基础知识不够了解的朋友可以参考以下文章:Java对象的序列化与反序列化深入解析Java序列化在这些文章中,我介绍了序列化涉及的类和接口,如何自定义序列化策略,transient关键字和序列化的关系等,还通过学习ArrayList实现序列化的源码深入学习了序列化。并且还扩展分析了序列化对单例的影响。不过还有一个知识点没有介绍,那就是关于serialVersionUID。这个字段有什么用?如果没有设置会怎样?《阿里巴巴Java开发手册》中为什么会有如下规定:背景知识在介绍本文之前,先简单介绍一下连载相关的一些知识。在三篇文章链接中。Serializable和ExternalizableJava类通过实现java.io.Serializable接口启用它们的序列化功能。不实现此接口的类不能被序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。如果读者看过Serializable的源码,就会发现它只是一个空接口,里面什么都没有。Serializable接口没有方法或字段,仅用于标识serializable的语义。但是,如果一个类没有实现这个接口,想要序列化,就会抛出java.io.NotSerializableException。它如何保证只有实现了这个接口的方法才能被序列化和反序列化?原因是在序列化过程中,会执行如下代码:}elseif(objinstanceofEnum){writeEnum((Enum>)obj,desc,unshared);}elseif(objinstanceofSerializable){writeOrdinaryObject(obj,desc,unshared);}else{if(extendedDebugInfo){thrownewNotSerializableException(cl.getName()+"\n"+debugInfoStack.toString());}else{thrownewNotSerializableException(cl.getName());}}序列化操作时会判断要序列化的类是否为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被修改了会发生什么?publicclassSerializableDemo1{publicstaticvoidmain(String[]args){//初始化TheObjectUser1user=newUser1();user.setName("hollis");//WriteObjtoFileObjectOutputStreamoos=null;try{oos=newObjectOutputStream(newFileOutputStream("tempFile"));oos.writeObject(user);}catch(IOExceptione){e.printStackTrace();}finally{IOUtils.closeQuietly(oos);}}}classUser1implementsSerializable{privatestaticfinallongserialVersionUID=1L;privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}}我们首先执行上面的代码,将一个User1对象写入到文件中。然后我们修改User1类,将serialVersionUID的值改为2L。classUser1implementsSerializable{privatestaticfinallongserialVersionUID=2L;privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}}然后执行如下代码反序列化文件中的对象:publicclassSerializableDemo2{publicstaticvoidmain(String[]args){//ReadObjfromFileFilefile=newFile("tempFile");ObjectInputStreamois=null;try{ois=newObjectInputStream(newFileInputStream(file));User1newUser=(User1)ois.readObject();System.out.println(newUser);}catch(IOExceptione){e.printStackTrace();}catch(ClassNotFoundExceptione){e.printStackTrace();}最后{IOUtils.closeQuietly(ois);try{FileUtils.forceDelete(file);}catch(IOExceptione){e.printStackTrace();}}}}执行结果如下: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,写入文件。classUser1implementsSerializable{privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}}然后我们修改User1类,给它添加一个属性。试图从文件中读取它并反序列化它。classUser1implementsSerializable{privateStringname;privateintage;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}publicintgetAge(){returnage;}publicvoidsetAge(intage){this.age=age;}}执行结果:java.io.InvalidClassException:com.hollis.User1;localclassincompatible:streamclassdescserialVersionUID=-2986778152837257883,localclassserialVersionUID=7961728318907695402同样,抛出了InvalidClassException,并且指出两个serialVersionUID不同,分别是-2986778152837257883和7961728318907695402。Itcanbeseenfromherethatthesystem已经添加了一个serialVersionUID本身。因此,一旦类实现了Serializable,建议显式定义一个serialVersionUID。否则,修改类时,会出现异常。serialVersionUID有两种显示方式:一种是默认1L,例如:privatestaticfinallongserialVersionUID=1L;另一种是根据类名、接口名、成员方法和属性等生成一个64位的哈希字段,例如:privatestaticfinallongserialVersionUID=xxxxL;后一种方法可以借助IDE生成,后面会介绍。其背后的原理很清楚。想知道为什么,我们先看下源码,分析一下为什么修改serialVersionUID会抛出异常。没有明确定义,默认的serialVersionUID是怎么来的呢?为了简化代码量,逆向序列化调用链如下:反序列化过程中,会比较serialVersionUID,如果发现不相等,则直接抛出异常。深入了解getSerialVersionUID方法:publiclonggetSerialVersionUID(){//REMIND:synchronizeinsteadofrelyingonvolatile?if(suid==null){suid=AccessController.doPrivileged(newPrivilegedAction
