详解Java序列化机制
概况
在程序中为了能直接以Java对象的形式进行保存,然后再重新得到该Java对象,这就需要序列化能力。序列化其实可以看成是一种机制,按照一定的格式将Java对象的某状态转成介质可接受的形式,以方便存储或传输。其实想想就大致清楚基本流程,序列化时将Java对象相关的类信息、属性及属性值等等保存起来,反序列化时再根据这些信息构建出Java对象。而过程可能涉及到其他对象的引用,所以这里引用的对象的相关信息也要参与序列化。
Java中进行序列化操作需要实现Serializable或Externalizable接口。
序列化的作用
- 提供一种简单又可扩展的对象保存恢复机制。
- 对于远程调用,能方便对对象进行编码和解码,就像实现对象直接传输。
- 可以将对象持久化到介质中,就像实现对象直接存储。
- 允许对象自定义外部存储的格式。
序列化例子
FileOutputStreamf=newFileOutputStream("tmp.o"); ObjectOutputs=newObjectOutputStream(f); s.writeObject("test"); s.writeObject(newArrayList()); s.flush();
常见的使用方式是直接将对象写入流中,比如上述例子中,创建了FileOutputStream对象,其对应输出到tmp.o文件中,然后创建ObjectOutputStream对象嵌套前面的输出流。当我们调用writeObject方法时即能进行序列化操作。
writeObject方法这里需要说明下,在对某个对象进行写入时,它其实不仅仅序列化自己,还会去遍历寻找相关引用的其他对象,由自己和其他引用对象组成的一个完整的对象图关系都会被序列化。
对于数组、enum、Class类对象、ObjectStreamClass和String等都会做特殊处理,而其他对象序列化则需要实现Serializable或Externalizable接口。
反序列化例子
FileInputStreamin=newFileInputStream("tmp.o"); ObjectInputStreams=newObjectInputStream(in); Stringtest=(String)s.readObject(); Listlist=(ArrayList)s.readObject();
针对序列化则存在反序列化操作,通过流直接读取对象,先创建FileInputStream对象,其对应输入文件为tmp.o,然后创建ObjectInputStream对象嵌套前面的输入流,接着则可以调用readObject方法读取对象。
其中调用readObject方法反序列操作的过程,除了会恢复对象自己之外还会遍历整个完整的对象图,创建整个对象图包含的所有对象。
serialVersionUID有什么用
在序列化操作时,经常会看到实现了Serializable接口的类会存在一个serialVersionUID属性,并且它是一个固定数值的静态变量。比如如下,这个属性有什么作用?其实它主要用于验证版本一致性,每个类都拥有这么一个ID,在序列化的时候会一起被写入流中,那么在反序列化的时候就被拿出来跟当前类的serialVersionUID值进行比较,两者相同则说明版本一致,可以序列化成功,而如果不同则序列化失败。
privatestaticfinallongserialVersionUID=-6849794470754667710L;
一般情况下我们可以自己定义serialVersionUID的值或者IDE帮我们自动生成,而如果我们不显示定义serialVersionUID的话,这不代表不存在serialVersionUID,而是由JDK帮我们生成,生成规则是会利用类名、类修饰符、接口名、字段、静态初始化信息、构造函数信息、方法名、方法修饰符、方法签名等组成的信息,经过SHA算法生成摘要即是最终的serialVersionUID值。
父类序列化什么情况
如果一个子类实现了Serializable接口而父类没有实现该接口,则在序列化子类时,子类的属性状态会被写入而父类的属性状态将不被写入。所以如果想要父类属性状态也一起参与序列化,就要让它也实现Serializable接口。
另外,如果父类未实现Serializable接口则反序列化生成的对象会再次调用父类的构造函数,以此完成对父类的初始化。所以父类属性初始值一般都是类型的默认值。比如下面,Father类的属性不会参与序列化,反序列化时Father对象的属性的值为默认值0。
publicclassFather{ publicintf; publicFather(){ } } publicclassSonextendsFatherimplementsSerializable{ publicints; publicSon(){ super(); } }
哪些字段会序列化
在序列化时类的哪些字段会参与到序列化中呢?其实有两种方式决定哪些字段会被序列化,
1.默认方式,Java对象中的非静态和非transient的字段都会被定义为需要序列的字段。
2.另外一种方式是通过ObjectStreamField数组来声明类需要序列化的对象。
可以看到普通的字段都是默认会被序列化的,而对于某些包含敏感信息的字段我们不希望它参与序列化,那么最简单的方式就是可以将该字段声明为transient。
如何使用ObjectStreamField?举个例子,如下,A类中有name和password两个字段,通过ObjectStreamField数组声明只序列化name字段。这种声明的方式不用纠结为什么这样,这仅仅是约定了这样而已。
publicclassAimplementsSerializable{ Stringname; Stringpassword privatestaticfinalObjectStreamField[]serialPersistentFields ={newObjectStreamField("name",String.class)}; }
枚举类型的序列化
Enum类型的序列化与普通的Java类的序列化有所不同,那么在深入之前可以先看这篇文章深入了解下枚举,《[从JDK角度认识枚举enum][JDK_enum]》。
所以我们知道枚举被编译后会变成一个继承java.lang.Enum的类,而且枚举里面的元素被声明成staticfinal,另外生成一个静态代码块static{},最后还会生成values和valueOf两个方法。Enum类是一个抽象类,主要有name和ordinal两个属性,分别用于表示枚举元素的名称和枚举元素的位置索引。
Enum类型参与序列化时只会将枚举对象中的name属性写入,而其他的属性则不参与进来。在反序列化时,则是先读取name属性,然后再通过java.lang.Enum类的valueOf方法找到对应的枚举类型。
除此之外,不能自定义Enum类型的序列化,所以writeObject,readObject,readObjectNoData,writeReplace以及readResolve等方法在序列化时会被忽略,类似的,serialPersistentFields和serialVersionUID属性都会被忽略。
最后,在序列化场景中,涉及到使用枚举的情况时要仔细设计好,不然很可能会因为后面升级修改了枚举类的结构而导致反序列化失败。
Externalizable接口作用
Externalizable接口主要就是提供给用户自己控制序列化内容,虽然前面我们也看到了transient和ObjectStreamField能定义序列化的字段,但通过Externalizable接口则能更加灵活。可以看到它其实继承了Serializable接口,提供了writeExternal和readExternal两个方法,也就是在这两个方法内控制序列化和反序列化的内容。
publicinterfaceExternalizableextendsjava.io.Serializable{ voidwriteExternal(ObjectOutputout)throwsIOException; voidreadExternal(ObjectInputin)throwsIOException,ClassNotFoundException; }
比如下面的例子,我们可以在writeExternal方法中额外写入Date对象,然后再写入value值。对应的,反序列化时则是在readExternal方法中读取Date对象和value。这样就完成了自定义序列化操作。
publicclassExternalizableTestimplementsExternalizable{ publicStringvalue="test"; publicExternalizableTest(){ } publicvoidwriteExternal(ObjectOutputout)throwsIOException{ Dated=newDate(); out.writeObject(d); out.writeObject(value); } publicvoidreadExternal(ObjectInputin)throwsIOException,ClassNotFoundException{ Dated=(Date)in.readObject(); System.out.println(d); System.out.println((String)in.readObject()); } }
写入时替换对象
正常情况下序列化某个对象时写入的正是当前的对象,但如果说我们要替换当前的对象而写入其他对象的话则可以通过writeReplace方法来实现。比如下面,person类通过writeReplace方法最终可以写入Object数组对象。所以我们在反序列化时就不再是转换成Person类型,而是要转换为Object数组对象。
classPersonimplementsSerializable{ privateStringname; privateintage; publicPerson(Stringname,intage){ this.name=name; this.age=age; } privateObjectwriteReplace()throwsObjectStreamException{ Object[]properties=newObject[2]; properties[0]=name; properties[1]=age; returnproperties; } }
ObjectInputStreamois=newObjectInputStream(newFileInputStream("test.o")); Object[]properties=(Object[])ois.readObject();
读取时替换对象
上面介绍了在写入时可以替换对象,而在读取时也同样支持替换对象的,它是通过readResolve方法实现的。比如下面,在readResolve方法返回2222,则反序列化读取时不再是Person对象,而是2222。
classPersonimplementsSerializable{ privateStringname; privateintage; publicPerson(Stringname,intage){ this.name=name; this.age=age; } privateObjectreadResolve()throwsObjectStreamException{ return2222; } }
ObjectInputStreamois=newObjectInputStream(newFileInputStream("test.o")); Objecto=ois.readObject();
以上就是详解Java序列化机制的详细内容,更多关于Java序列化机制的资料请关注毛票票其它相关文章!