一、什么、为什么
1. 序列化的本质(什么)
序列化的目的是持久化一个对象的状态。
a. 状态:当new一个对象的时候,该对象叫做张三,男,24岁(属性name="张三", gender="male", age=24),这些property和对应的value就是一个对象的状态;
补充:
方法本身没有状态,因此序列化的过程中,方法不参与序列化;
类属性表示的是某一类事物的共同状态,而不是某一个对象的状态,因此类属性不参与序列化;
根据JVM规范,transient(瞬态)修饰的对象属性也不会参与序列化;
b. 持久化:new出一个对象的时候,这个对象是在内存当中(栈引用、堆空间、方法区、程序计数器、本地方法栈都有),内存是RAM,特点是保存的数据断电后消失,因此内存中的对象在程序退出以后或者重新打开电脑以后就不存在了,这样的对象不是持久化的。
2. 序列化的应用场景(为什么)
将一个内存对象持久化(准确来说是对象状态持久化),保存到硬盘或者文件,为什么要这么做?这样的技术有什么用?
a. 场景一:保存游戏的进度。游戏开启的时候程序和数据加载进内存,退出的时候可以保存游戏进度,这个“进度”是内存数据,断电消失,如果希望下次开启游戏的时候能够恢复当前的游戏进度,就需要一种技术手段将当前的内存dump到磁盘,dump到磁盘中的数据也需要遵守一定的格式和规范,这就是序列化;
b. 场景二:网络传输对象。网络能够传输的都是电信号,根据电频的高低最终还原成0/1数据的机器码,对象在内存当中也是一片连续的0/1数据,将一片连续的0/1数据跨网络传输,另一台电脑的jvm怎么能够还原出和当前一样的对象?这也是序列化技术的应用。
二、实现
java序列化和反序列化技术有两种方式,默认序列化(将序列化所有非transient/static修饰的属性,无论该属性是基本类型还是引用类型,是否赋值或为null等)和自定义序列化(根据实际需要序列化属性)。
要想实现java序列化,那么这个类应该实现Serializable接口或者Externalizable接口。
1. 默认序列化
最简单的序列化,就是编写的javaBean实现java.io.Serializable接口,该接口是一个空接口,作为序列化标记语义表示该类启用序列化功能,未实现该接口的类将无法使用任何状态序列化或反序列化(摘自JDK API文档),该接口没有方法和属性,仅用于标识可用于序列化的语义。
<span style="font-family:Arial Black;">package cn.wxy.ser.domain;
import java.io.Serializable;
/**
* 最简单的序列化,表示一个类启用了序列化合反序列化功能
* @author reliveIT
*
*/
public class Person implements Serializable{
private String name;
private String gender;
private Integer age;
}</span>
<span style="font-family:Arial Black;">package cn.wxy.ser.domain;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import org.junit.Test;
public class PersonTest {
/**
* 默认序列化:所有非transient/static修饰的属性
* @throws IOException
* @throws FileNotFoundException
*/
@Test
public void testSer() {
Person per = new Person("zhangsan", "male", 24);
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(new File("d:"+File.separator+"per.ser")));
oos.writeObject(per);
} catch (IOException e) {
e.printStackTrace();
} finally {
if(oos != null){
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
oos = null;
}
}
}
/**
* 默认反序列化
*/
@Test
public void testDeser(){
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(new File("d:"+File.separator+"per.ser")));
Person per = (Person)ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if(ois != null){
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
ois = null;
}
}
}
}</span>
2. 自定义序列化
默认序列化是序列化一个类中除了static/transient修饰的所有属性,如果希望自定义序列化,则有如下三种方法:
a. 使用transient修饰不希望被序列化的属性。transient只能修饰Field,当一个属性被transient修饰,则该属性将完全被隔离在序列化之外,反序列化的时候不会获得该属性的值,但是如果一有大量属性的类中只需要序列化较少部分的属性,该方法会显得累赘;
b. 如果该类实现了java.io.Serializable
Ⅰ. 可以重写writeObject/readObject两个方法来实现自定义序列化,默认情况下,该方法调用out.defaultWriteObject/in.defaultReadObject方法;当序列化流不完整的时候,例如序列化版本或者IO流杯篡改,则可以通过readObjectNoData方法来读取反序列化流数据;需要注意的是,序列化Filed的顺序和反序列化的顺序要保持一致,否则异常。
Ⅱ. 更彻底的序列化,重写writeReplace/readResolve,writeReplace在writeObject之前被调用,该方法允许返回另一个对象替代被序列化,就像一条链一样——返回另一个对象,如果这个对象有writeReplace方法则继续走链,如果没有则调用该对象的writeObject实现序列化——替代了最初序列化的对象被序列化;而readResolve总是在readObject之后调用,该方法的返回值总是替代readObject的返回值,而readObject的返回值则被抛弃。
c. 实现java.io.Externalizable(该接口继承了java.io.Serializable),重写writeExternal/readExternal方法。
三、变态序列化
1. 对象引用序列化
BT点:可序列化的类中引用了不可序列化的属性
a. 如果这个不可序列化的引用属性不为null,则在序列化的过程中抛出java.io.NotSerializableException: cn.wxy.ser.ClassIncludeNonSer异常,序列化失败;
b. 如果该引用属性为null,则序列化和反序列化都成功;
2. serialVersionUID
a. 序列化时:保证在一次虚拟机中对同一个对象进行序列化的时候,不会出现不一致情况。这个情况略庞杂,举个简单的例子,name="zhangsan"的对象被序列化后,修改其name="lisi",此时再次对该8对象进行序列化(前提是设置了序列化ID,此后无论该类结构怎么变化,都只是第一次进行writeObject操作的时候才会序列化,以后都只是向文件中输出一个序列化ID),就算修改了属性值,也只是像文件中写入其序列化ID,而不是所有序列化的数据。
<span style="font-family:Arial Black;">package cn.wxy.ser.domain;
import java.io.Serializable;
/**
* 带有序列化ID的javaBean
* @author reliveIT
*/
public class PersonID implements Serializable {
private static final long serialVersionUID = -7189943443958450342L;
private String name;
private String gender;
private Integer age;
public PersonID(String name, String gender, Integer age) {
super();
this.name = name;
this.gender = gender;
this.age = age;
}
}</span>
<span style="font-family:Arial Black;"> /*
* 在一次虚拟机中,设置了序列化ID的类,只有第一次对该类对象进行序列化操作的时候才会保存其所有序列化字节序列
* 之后的所有序列化操作都只向文件中写入一个ID,查看一下代码序列化内容,发现name=zhangsan
* 序列化内容:
* sr cn.wxy.ser.domain.PersonID?0??Z L aget Ljava/lang/Integer;L gendert Ljava/lang/String;L nameq ~ xpsr java.lang.Integer鉅亣8 I valuexr java.lang.Number啲?斷? xp t malet zhangsanq ~
* @throws Exception
*/
@Test
public void testSerID() throws Exception{
PersonID pId = new PersonID("zhangsan", "male", 24);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("d:"+File.separator+"demo"+File.separator+"ser//pid.ser")));
oos.writeObject(pId);
pId.setName("lisi");
oos.writeObject(pId);
oos.close();
}</span>
b. 反序列化时:如果序列化时和反序列化时该对象的类结构发生变化,则通过serialVersionUID来保证兼容,该属性标识着java类的序列化版本,也就是无论该类结构是否变化,只要其ID不发生变化,虚拟机都会将其当成同一个系列化版本。
c. 如果没有提供该ID,则默认由虚拟机根据类结构计算得出,因此如果类结构发生变化,则JVM计算该值也会发生变化,导致反序列化失败。
3. writeReplace/readResolve
继承中这两个方法会显得特别的有意思,首先建议重写这两个方法的时候慎重选用protected/default/public修饰符,应该优先选中private修饰,否则一旦涉及多态则情况变得更加复杂有趣。
4. 继承中的序列化
a. 父类可序列化,则其所有子类可序列话,前提是子类中没有不可序列话的属性;
b. 子类可续列化,但是父类不可序列化,则序列化的时候父类不会被序列化,反序列化的时候需要父类提供默认构造函数。
其中也涉及很多问题,暂时不展开来讨论,后续有心情再论
附注:
本文未完,待续(闲下来有心情的时候···)