简介最近忙着写Java业务代码,麻木地搬着Ctrl-C和Ctrl-V的砖头,在定义Java的时候不知道重复了多少次“implementsSerializable”的C/实体对象V大法之后,脑子里突然冒出一个念头(A):我问自己“Java实体对象为什么一定要实现Serializable接口?”对于这个问题,我脑子里的另一个想法(B)立马给了回复“居然问这么幼稚基础的问题,Serilizable接口的实现就是为了序列化!”,想法(A):“哦,好吧!不过,什么?然后?”这时,思(B)陷入沉寂,突然觉得有些肤浅。看来我写了Java这么多年都没怎么关注Serializable接口!为什么一定要实现Serializable接口?它的基本原理是什么?为什么一定要连载?什么是序列化?关于这些问题,不知道各位读者朋友有没有遇到过类似的问题。如果是的话,让我们一起在这篇文章中寻找答案吧!当然,如果你对这些问题很清楚,欢迎大家发表看法!Serializable接口概述Serializable是java.io包中定义的语义级接口,用于实现Java类的序列化操作。Serializable序列化接口没有任何方法或字段,仅用于标识可序列化语义。实现了Serializable接口的类可以被ObjectOutputStream转换成字节流,也可以被ObjectInputStream解析成对象。例如,我们可以将序列化后的对象写入文件,再从文件中读取并反序列化为对象,即使用表示对象及其数据的类型信息和字节在内存中重新创建对象。而这一点对于面向对象的编程语言来说是非常重要的,因为不管是什么编程语言,底层的IO操作部分还是由操作系统来完成,底层的IO操作都是字节流,所以写操作涉及转换将编程语言数据类型转换为字节流,读取操作涉及将字节流转换为编程语言类型的特定数据类型。Java是一种面向对象的编程语言,对象是其主要数据的类型载体。为了完成对对象数据的读写操作,需要有一种方式让JVM知道如何将对象数据转换为字节流,以及如何将字节流数据转换为具体的对象,而Serializable接口就承担了这样的功能角色。下面我们可以通过一个例子将序列化后的对象存储到一个文件中,然后从文件中反序列化为一个对象。代码示例如下:首先定义一个序列化对象User:publicclassUserimplementsSerializable{privatestaticfinallongserialVersionUID=1L;privateStringuserId;privateStringuserName;publicUser(StringuserId,StringuserName){this.userId=userId;this.userName=userName;}}然后我们写一个测试类来读写对象。我们首先测试将对象写入文件:publicclassSerializableTest{/***将User对象以文本形式写入磁盘*/publicstaticvoidwriteObj(){Useruser=newUser("1001","Joe");try{ObjectOutputStreamobjectOutputStream=newObjectOutputStream(newFileOutputStream("/Users/guanliyuan/user.txt"));objectOutputStream.writeObject(user);objectOutputStream.close();}catch(IOExceptione){e.printStackTrace();}}publicstaticvoidmain(Stringargs[]){writeObj();}}运行上面的代码,我们将User对象及其携带的数据写入到文本user.txt中。我们可以看到此时user.txt中存储的数据是什么格式:java.io.NotSerializableException:cn.wudimanong.serializable.Useratjava.io.ObjectOutputStream。writeObject0(ObjectOutputStream.java:1184)在java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)atcn.wudimanong.serializable.SerializableTest.writeObj(SerializableTest.java:19)atcn.wudimanong.serializable.SerializableTest.main(SerializableTest.java:27)我们看到对象数据以如下形式持久化binarytext在进行反序列化测试之前,我们可以尝试去掉实现User的Serializable接口的代码部分,看看此时写操作是否还能成功。结果如下:果然不出所料,抛出NotSerializableException,提示不可序列化异常,也就是说没有实现Serializable接口的对象无法通过IO操作进行持久化。接下来,我们继续编写测试代码,尝试将持久化写入user.txt文件的对象数据重新转换为Java对象,代码如下:publicclassSerializableTest{/***从文本中提取类并赋值给内存中的Class*/publicstaticvoidreadObj(){try{ObjectInputStreamobjectInputStream=newObjectInputStream(newFileInputStream("/Users/guanliyuan/user.txt"));try{Objectobject=objectInputStream.readObject();Useruser=(User)object;System.out.println(user);}catch(ClassNotFoundExceptione){e.printStackTrace();}}catch(IOExceptione){e.printStackTrace();}}publicstaticvoidmain(Stringargs[]){readObj();}}通过反序列化操作,可以将持久化对象字节流数据再次通过IO转换为Java对象,结果如下:cn.wudimanong.serializable.User@6f496d9f这时候,如果我们尝试再次去掉实现User的Serializable接口的那部分代码,发现不能然后将文本转化为序列化对象,报错信息为:ava.io.InvalidClassException:cn.wudimanong.serializable.User;classinvalidfordeserializationatjava.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:157)atjava.io。ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:862)在java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream).java:2038)atjava.io.ObjectInputStream.readObject0(ObjectInputStream.java:1568)atjava.io.ObjectInputStream.readObject(ObjectInputStream.java:428)atcn.wudimanong.serializable.SerializableTest.readObj(SerializableTest.java:31)atcn.wudimanong.serializable.SerializableTest.main(SerializableTest.java:44)提示非法类型转换异常,说明Java中如何实现对象的IO读写操作,必须实现Serializable接口,否则代码会报错错误!Serialization&Deserialization通过上面的讲解和例子,相信大家对Serializable接口的作用有了更具体的认识。接下来,我们就进入理论层面,看看什么是序列化/反序列化。序列化是指将一个对象转换成字节序列的过程,我们称之为对象序列化,就是将内存中的这些对象变成一系列字节(bytes)描述的过程。另一方面,反序列化是将持久字节文件数据恢复为对象的过程。那么什么情况下需要序列化呢?常见的场景大概有两种:1)当需要将内存中的对象状态数据保存到文件或数据库中时,这种场景比较常见。比如我们在使用mybatis框架写持久层insert对象数据到数据库的时候;2)、当网络通信需要使用socket来传输网络中的对象时,比如我们使用RPC协议进行网络通信时;对于JVM,serialVersionUID必须是持久化类必须有标记。只有有了这个标记JVM才能让类创建的对象通过它的IO系统转换成字节数据,从而实现持久化,而这个标记就是Serializable接口。在反序列化的过程中,需要通过serialVersionUID来决定加载这个对象的类,所以我们在实现Serializable接口的时候,一般要尽可能明确的定义serialVersionUID,比如:privatestaticfinallongserialVersionUID=1L;在序列化过程中,如果接收者为对象加载了一个类,如果对象的serialVersionUID与对应的持久化类不同,反序列化过程中就会抛出InvalidClassException。比如在前面的反序列化例子中,我们有意将User类的serialVersionUID改为2L,如:privatestaticfinallongserialVersionUID=2L;那么这个时候反序列化实例化的时候就会抛出异常,如下:java.io.InvalidClassException:cn.wudimanong.serializable.User;localclassincompatible:streamclassdescserialVersionUID=1,localclassserialVersionUID=2atjava.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)atjava.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1880)atjava.ioe.ObjectInput.readClassDesc(ObjectInputStream.java:1746)atjava.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2037)atjava.io.ObjectInputStream.readObject0(ObjectInputStream.java:1568)atjava.io.ObjectInputStream.readObject(ObjectInputStream.java:428)atcn.wudimanong.serializable.SerializableTest.readObj(SerializableTest.java:31)atcn.wudimanong.serializable.SerializableTest.main(SerializableTest.java:44)如果我们在序列化中没有显式声明serialVersionUID,序列化运行时这个类的默认serialVersionUID值会从各种asp中计算出来类的ects。但是Java官方强烈建议所有要序列化的类都显式声明serialVersionUID字段,因为如果JVM高度依赖默认的serialVersionUID生成,可能会导致它与编译器的实现细节耦合,从而可能导致反序列化。在此过程中发生意外的InvalidClassException。因此,为了确保serialVersionUID值在不同Java编译器实现之间的一致性,Serializable接口的实现者必须显式声明serialVersionUID字段。另外,serialVersionUID字段的声明尽量用private关键字修饰。这是因为该字段的声明只适用于声明的类。这个字段作为成员变量被子类继承是没有用的!有一点需要特别注意:,数组类不能显式声明serialVersionUID,因为它们总是有默认的计算值,但是在数组类的反序列化过程中也放弃了匹配serialVersionUID值的要求。参考资料:https://docs.oracle.com/javase/8/docs/api/java/io/Serializable.html?is-external=truehttp://www.tutorialspoint.com/java/java_serialization.htm
