一、为什么使用序列化
由于在系统底层,数据的传输形式是简单的字节序列形式传输,在系统底层,并不认识Java对象,只知道字节序列,所以想要达到进程通讯的目的,需要先将数据进行序列化,即将对象转化为字节序列的过程。而字节序列被响应的进程使用的时候,进程为了识别这些字节序列,就需要对这些字节序列进行反序列化操作,把字节序列转换成Java对象。
1.序列化
将数据结果或者对象转化为二进制串的过程
2.反序列化
将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程
序列化/反序列化的目的
序列化:主要用于网络传输,数据持久化,也可以叫做编码
反序列化:主要用于从网络、磁盘上读取字节数组还原成原始对象,也可以叫做解码
(1)永久的保存对象数据(将对象数据保存在文件或者磁盘中)
(2)通过序列化操作将对象数据在网络上进行传输(由于网络传输是以字节流的方式对数据进行传输的,因此序列化的目的是将对象数据转换成字节流的形式)
(3)将对象数据在进程间进行传输(Activity之间传递对象数据时,需要在当前的Activity中对对象数据进行序列化操作,在另一个Activity中需要进行反序列化操作将数据取出)
(4)Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长(即每个对象都在JVM中)但在现实应用中,就可能要停止JVM运行,但有要保存某些指定的对象,并在将来重新读取被保存的对象。这是Java对象序列化就能实现该功能(可以选择入数据库、或者文件的形式保存)
(5)序列化对象的时候只是针对变量进行序列化,不针对方法进行序列化
(6)在Intent之间,基本的数据类型直接进行相关传递即可,但是一旦数据类型比较复杂的时候,就需要进行序列化操作。
二、序列化协议特性
1.通用性
2.强健性/鲁棒性
3.可调适性/可读性
4.性能
(1)空间开销:序列化需要在原有的数据上加上描述字段,为反序列化解析使用。如果序列化过程引入的额外开销过高,可能会导致过大的网络、磁盘等各方面压力。对于海量分布式存储系统,数据量往往以TB为单位,巨大的额外空间开销意味着高昂的成本
(2)时间开销:复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈
5.可扩展性/兼容性
业务系统需求的更新周期变快,老的系统也需要维护。如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。
6.安全性/访问限制
在序列化选型的过程中,安全性的考虑往往发生在跨局域网的场景。当通讯发生在公司之间或者跨机房的时候,处于安全的考虑,对于跨局域网的访问往往被限制为基于HTTP/HTTPS的80和433端口。如果使用的序列化协议没有兼容而成熟的HTTP传输层框架支持,可能会导致以下三种结果:
(1)因为访问限制而降低服务可用性
(2)被迫重新实现安全协议而导致实施成本大大提高
(3)开放更多的防火墙端口和协议访问,而牺牲了安全性
三、Serializable接口
Serializable接口可以用来标识当前类可以被ObjectOutputStream序列化,以及被ObjectInputStream反序列化,Serializable是一个空接口,就是起一个标识的作用。
1.Serializable的简单使用和注意事项
public class Student implements Serializable {
//serialVersionUID唯一标识了一个可序列化的类
private static final long serialVersionUID = -2100492893943893602L;
private String name;
private String sax;
private Integer age;
//Course也需要实现Serializable接口
private List courses;
//用transient关键字标记的成员变量不参与序列化(在被反序列化后,transient变量的值
//被设为初始值,如 int 型的是 0,对象型的是 null)
private transient Date createTime;
//静态成员变量属于类不属于对象,所以不会参与序列化(对象序列化保存的是对象的“状态”,
//也就是它的成员变量,因此序列化不会关注静态变量)
private static SimpleDateFormat simpleDateFormat = newSimpleDateFormat();
// 一个可序列化的类中,存在不可序列化的属性,那么这个类的无参构造函数一定要可以访问
// 否则在反序列化过程中,会因为无法调用无参构造函数初始化不可序列化的属性而报错
public Student() {
System.out.println("Student: empty");
}
public Student(String name, String sax, Integer age) {
System.out.println("Student: " + name + " " + sax + " " + age);
this.name = name;
this.sax = sax;
this.age = age;
courses = new ArrayList<>();
createTime = new Date();
}
...
}
////Course也需要实现Serializable接口
public class Course implements Serializable {
private static final long serialVersionUID = 667279791530738499L;
private String name;
private float score;
...
}
Serializable接口在使用的时候,可以配合transient关键字,使用transient关键字修饰的对象,不能被序列化;静态成员也不能被序列化。
2.serialVersionUID与兼容性
(1)serialVersionUID的作用
serialVersionUID用来表明类的不同版本间的兼容性。如果修改了此类,则需要修改这个值。否则以前用老版本的类序列化的类恢复时会报错:InvalidClassException
(2)兼容性问题
为了在反序列化时,确保类版本的兼容性,最好在每个要序列化的类中加入private static final
long serialVersionUID这个属性,具体数值自定义。这样做的目的,即使某个类在与之对应的对象已经序列化出去后做了修改,该对象依然可以被正确的反序列化。否则,如果不显示定义该属性,这个属性值将由JVM根据类的相关信息计算,而修改后的类的计算结果与修改前的类的计算结果往往不同,从而造成对象的反序列化因为类版本不兼容而失败。
不显示定义这个属性值的另一个坏处是,不利于程序在不同的JVM之间的移植。因为不同的编译器实现该属性值的计算策略可能不同,从而造成虽然类没有改变,但是可能因为JVM不同,出现因为类版本不兼容而无法正确反序列化的现象
3.Externalizable接口
Externalizable接口继承自Serializable接口,通过覆写其writeExternal和readExternal方法,针对类的部分属性做序列化和反序列化。实现了Externalizable接口,一定要有无参构造函数,如果没有无参构造函数,则反序列化的时候会报错;并且实现了Externalizable接口之后,并不需要serialVersionUID;
4.ObjectOutputStream.writeObject原理分析
ObjectOutputStream构造函数
public ObjectOutputStream(OutputStream out) throws IOException {
verifySubclass();
bout = new BlockDataOutputStream(out);
handles = new HandleTable(10, (float) 3.00);
subs = new ReplaceTable(10, (float) 3.00);
// 这设置为false,是在writeObject执行时判断执行的分支
enableOverride = false;//enableOverride = false
...
}
ObjectOutputStream.writeObject
public final void writeObject(Object obj) throws IOException {
//enableOverride=false,不走这里
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {//一般情况都走这里
writeObject0(obj, false);
...
}
ObjectOutputStream.writeObject0
在这里,判断传入的Object对象的具体类型,根据类型选择相对应的操作,如果不满足这四种类型条件,则抛出异常
/**
- Underlying writeObject/writeUnshared implementation.
*/
private void writeObject0(Object obj, boolean unshared)
throws IOException{
...
// remaining cases
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) {
//如果实现了Serializable接口
writeOrdinaryObject(obj, desc, unshared);
} else {//如果没有实现Serializable接口,会报NotSerializableException
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
...
}
ObjectOutputStream.writeOrdinaryObject
因为Java序列化和反序列化有可能会实现Serializable或者是Externalizable,所以在这里会有两个不同的输出情况
private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared)
...
if (desc.isExternalizable() && !desc.isProxy()) {
//如果对象实现了Externalizable接口,那么执行writeExternalData((Externalizable) obj)方法
writeExternalData((Externalizable) obj);
} else {
//如果对象实现的是Serializable接口,那么执行的是writeSerialData(obj, desc)
writeSerialData(obj, desc);
}
...
}
ObjectOutputStream.writeSerialData
/**
* Writes instance data for each serializable class of given object,
from
* superclass to subclass.
* 最终写序列化的方法
*/
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException{
...
if (slotDesc.hasWriteObjectMethod()) {
//如果writeObjectMethod != null(目标类中定义了私有的writeObject
//方法),那么将调用目标类中的writeObject方法
// 其实就是需要序列化的类重写了writeObject方法,由开发者自己实现序列化的功能
...
slotDesc.invokeWriteObject(obj, this);
...
} else {
//如果如果writeObjectMethod == null,
//那么将调用默认的defaultWriteFields方法来读取目标类中的属性
// 调用BlockDataOutputStream.write将二进制流序列化写入
defaultWriteFields(obj, slotDesc);
}
}
}
ObjectStreamClass
在ObjectStreamClass中,ObjectOutputStream(ObjectInputStream)会寻找目标类中的私有的writeObject(readObject)方法,赋值给变量writeObjectMethod(readObjectMethod)
/**
* Creates local class descriptor representing given class.
*/
private ObjectStreamClass(final Class> cl) {
...
if (externalizable) {
cons = getExternalizableConstructor(cl);
} else {
//,在序列化(反序列化)的时候,ObjectOutputStream(ObjectInputStream)
// 会寻找目标类中的私有的writeObject(readObject)方法,
// 赋值给变量writeObjectMethod(readObjectMethod)
cons = getSerializableConstructor(cl);
writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class>[] { ObjectOutputStream.class },Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class>[] { ObjectInputStream.class }, Void.TYPE);
readObjectNoDataMethod = getPrivateMethod(cl, "readObjectNoData",
null, Void.TYPE);
hasWriteObjectData = (writeObjectMethod != null);
}
domains = getProtectionDomains(cons, cl);
writeReplaceMethod = getInheritableMethod(
cl, "writeReplace", null, Object.class);
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
return null;
}
});
...
}
5.重写readObject、writeObject、writeReplace、readResolve
如果是重写了readObject和writeObject,则会通过反射进行序列化。
在调用顺序中,writeReplace会优先于writeObject调用;readResolve会在readObject之后调用。
如果想要对某个字段做特殊处理,则可以在writeReplace和readResolve中进行。即在序列化之前,和反序列化之后。
如果重写了writeReplace方法,则会在ObjectOutputStream中的writeObject0方法中先调用writeReplace方法。
在ObjectOutputStream类中的writeSerialData方法,会先判断是否重写了writeObject方法。
而且writeObject0方法会调用writeSerialData方法,所以writeReplace会优先于writeObject方法。
readResolve方法的调用顺序其实类似。即在ObjectInputStream中的readOrdinaryObject方法中,会优先调用readSerialData方法,在readSerialData方法中会判断是否重写了readObject,如果重写了,则通过readObject进行反序列化,然后再readSerialData方法执行完成之后,在readOrdinaryObject中接着调用了readResolve方法对反序列化之后的结果进行处理。
6.Serializable接口实现序列化需要注意
(1)多引用写入
在默认情况下, 对于一个实例的多个引用,为了节省空间,只会写入一次,后面会追加几个字节代表某个实例的引用。
当一个引用对象已经被序列化,那么修改了这个引用对象之后,想要再次对这个引用对象进行序列化,有两种方式解决,一种就是调用ObjectOutputStream.reset()方法,然后再写;还有一种方案就是oos.writeUnshared(course);通过writeUnshared来进行第二次序列化。
(2)子类实现序列化,父类不实现序列化/ 对象引用
在readObject时抛出java.io.NotSerializableException异常。子类实现了Serializable序列化的时候,其父类也是需要实现的,或者父类有无参构造函数,否则就会报错。
(3)单例模式的序列化问题/反射问题
单例模式的序列化和反序列化,需要重写readResolve方法,返回单例对象。如果不重写,则会导致单例模式在序列化->反序列化后失败。
序列化会导致单例模式失效。
因为单例实现Serializable将单例序列化,则并不会是同一个对象,单例就不是单例了。而重写readResolve方法,直接将单例对象返回。