我们这里讨论的序列化是指的语言范畴将一个实例对象编码成字节流,并从字节流编码中重新构建对象实例的能力。将一个对象编码成字节流,称为序列化;从一个字节流中读出一个对象实例,称为反序列化。
这里的序列化需要区别于我们平时说的
JSON&XML
对象序列化,对于JSON&XML
这种字符描述型对象,它们是通用的,不依赖于任何的语言平台。
为什么要写这篇文章?网路上对于Android
中Serializable
和 Parcel
的对比已经有很多了,它们传达给读者的信息归结起来就是一句话:Android中的Parcel
比Java
的Serializable
快,在Android
中推荐使用Parcelable
作为序列化的手段,比如这篇。实际真的如此吗?通过阅读源码和实验结果比较,本文得出了一个大跌眼镜的结果。为了保证文章的完整性,我会分三篇文章来验证我的结论,前两篇主要是关于Serializable
和Parcelable
的用法,如果你对这些很熟悉,我也建议你温故一下,说不定有额外的收获,第三篇是比较这两种序列化方式。如果你阅读完这些文章,我可以保证你能应付Android
开发中对序列化的所有问题(如果不能,请再看一遍 _)。
Android序列化完全解析(一)-Java Serializable
Android序列化完全解析(二)-Parcelable
Android序列化完全解析(三)-拨乱反正,堪比窦娥的Serializable
废话唠完,进入正题,第一篇Java Serialization
Java Serialization
可能你会说,这个接口有什么好说的?作为一个标记接口,我只需要将我的类继承自Serialization
就万事大吉了,一旦一个接口被打上了Serialization
标签,那它就可以自由地被ObjectOutputStream
和ObjectInputStream
进行序列化和反序列化了,但是作为JDK
中一个充满黑科技+槽点的API
,值得我们花点时间回顾一下。
Serialization基础
对于如何将一个对象进行序列化和反序列化,请看我下面的样例代码1,但是我会在一开始的时候列出一些总结性的东西:
-
serialVersionUID
用来标识当前序列化对象的类版本,建议每一个实现Serialization
的类都指定该域。当然如果我们没有指定,JVM
会根据类的信息自动生成一个UID
。我们可以通过JDK
的serialver
命令来查看一个.class
类的UID
,使用方法戳这里。 - 被
transient
描述的域和类的静态变量是不会被序列化的,序列化是针对类实例。 - 需要进行序列化的对象所有的域都必须实现
Serializable
接口,不然会直接报错NotSerializableException
。当然,有两个例外:域为空 或者域被transient
描述是不会报错的。 - 如果一个实现了
Serializable
类的对象继承自另外一个类,那么这个类要么需要继承自Serializable
,要么需要提供一个无参构造器。 - 反序列化产生的对象并不是通过构造器创建的,那么很多依赖于构造器保证的约束条件在对象反序列化时都无法保证。比如一个设计成单例的类如果能够被序列化就可以分分钟克隆出多个实例...
样例代码1
public class SimpleObjectSerial extends SimpleSuperClass implements Serializable {
/**
* 固定写法,故名思意,指定序列化的版本,这个信息在对象序列化时写入到字节流中.
* 比如我们把SimpleObjectSerial对象序列化之后,字节流中不仅仅有对象中各个域的数据,同时还会存储序列化
* 时SimpleObjectSerial类当时的一些信息,比如类名、VERSION_UID等
*
* 反序列化时,JVM会自动检查我们用来反序列化的类信息和字节流中的类信息定义是否一致,如果不一致,会抛出异常
* 一般我们都会在实现了Serializable的类中定义UID字段用于版本控制.
*/
private static final long serialVersionUID = 1L;
//一个普通的域
public String desc;
//静态域
public static int static_field;
//transient域
public transient int transient_field;
public SimpleObjectSerial(String desc ,String super_filed) {
super(super_filed);
this.desc = desc;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
SimpleObjectSerial simpleObjectSerial = new SimpleObjectSerial("desc","super_filed");
simpleObjectSerial.transient_field = 100;
simpleObjectSerial.static_field = 10086;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(simpleObjectSerial);
outputStream.flush();
outputStream.close();
simpleObjectSerial.static_field = 10087;
byte[] bytes = byteArrayOutputStream.toByteArray();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
SimpleObjectSerial deserialObj = (SimpleObjectSerial) objectInputStream.readObject();
System.out.println(deserialObj);
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
appendFiled("desc",desc,stringBuilder);
appendFiled("static_field",static_field,stringBuilder);
appendFiled("transient_field",transient_field,stringBuilder);
appendFiled("super_filed",super_filed,stringBuilder);
return stringBuilder.toString();
}
private StringBuilder appendFiled(String filedName,Object filed,StringBuilder stringBuilder) {
return stringBuilder.append(filedName).append(": ").append(filed).append("\n");
}
}
/**
* 如果一个类实现了Serializable类,同时它继承了一个类,比如这里的SimpleSuperClass,那么父类用么实现
* Serializable接口,要么提供一个默认无参数构造器.道理很简单,构造一个子类需要先构造它的父类
*/
class SimpleSuperClass {
public String super_filed;
public SimpleSuperClass() {}
public SimpleSuperClass(String super_filed) {
this.super_filed = super_filed;
}
}
输出如下:
desc: desc
static_field: 10087
transient_field: 0
super_filed: null
各路黑科技
在一个对象在被序列化和反序列化的过程中,有一系列黑科技方法,主要分为三个阵营:1、如何写入一个对象;2、如何读取一个对象;3、如何保证一个类的逻辑完整性。它们可以必须是固定的方法名和参数,至于修饰符private protected publish
都可以,因为Object**Stream
是通过反射来调用这些方法的。
-
private Object writeReplace() throws ObjectStreamException ObjectOutPutStream
在序列化一个对象时,会先调用这个方法,在这里我们可以替换真正送去序列的对象,如过我们没有重写,那序列化的对象就是最开始的对象。至于为什么要换?后面有解释。 -
private void writeObject(java.io.ObjectOutputStream out) throws IOException
我们可以在这个方法里面自己往流中写入一些数据,如果我们调用ObjectOutputStream.defaultWriteObject()
可以使用默认的序列化过程序列一个对象。 -
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
如果我们调用writeObject
自己定义了序列化过程,别忘了在这里手动去读取数据,而且读取顺序和写入顺序一致,同时我们注意到这里已经把参数inputstream
回调给我们了,我们可以在这个位置注册一个回调,可以在validateObject
方法中检查这个反序列化对象是否合法。 -
private Object readResolve() throws ObjectStreamException
是否替换反序列化出来的对象。
我们通过一段代码来看这些方法调用的顺序。
样例代码2
public class Pojo implements Serializable, ObjectInputValidation {
private String msg;
public Pojo(String msg) {
this.msg = msg;
}
public String getMsg() {
return msg;
}
/**
* 是否替换需要序列化的对象
* @return
* @throws ObjectStreamException
*/
private Object writeReplace() throws ObjectStreamException {
//还没有替换对象之前,这里是helloworld
System.out.println("writeReplace! msg: "+msg);
//替换对象,这里指定为replace
//注意,这里我们不一定要返回一个Pojo对象,我们可以返回任意Object
return new Pojo("replace!");
}
/**
* 写入outputstream流
* @param out
* @throws IOException
*/
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
//这里对象已经被替换成msg为replace的对象
System.out.println("writeObject! msg: "+msg);
//序列化对象
out.defaultWriteObject();
//额外往流中写入一些其它数据,注意顺序
out.writeInt(10086);
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
//在执行defaultReadObject之前,这个对象的msg还没有被指定
//这里需要注意的是,当前这个反序列化出来的对象并不是通过构造器创建的
System.out.println("readObject! msg: "+msg);
//读取
in.defaultReadObject();
//msg被读取出来
System.out.println("readObject! msg: "+msg);
//读取写入的额外数据
System.out.println(in.readInt());
//注册一个检验数据是否完整的回调,注意这个this
in.registerValidation(this, 0);
}
/**
* 是否替换从流中读取出来的对象
* @return
* @throws ObjectStreamException
*/
private Object readResolve() throws ObjectStreamException {
//替换之前的对象
System.out.println("readResolve! msg: "+msg);
//直接替换一个对象,这里需要注意的是readObject中注册的合法检验对象是反序列化产生的对象
// 如果在这里替换,那么检测地还是之前的对象
return new Pojo("readResolve!");
}
@Override
public void validateObject() throws InvalidObjectException {
//注意,这里是readObject中读取的对象,而并不是readResolve替换之后的对象
System.out.println("validateObject! msg: "+msg);
//在这里进行合法性检查,比如msg不能为空
if(msg == null || "".equals(msg)) {
throw new InvalidObjectException("Pojo msg null!");
}
}
public static void main(String[] args) throws Exception {
Pojo pojo = new Pojo("Hello world");
byte[] bytes = serialize(pojo); // Serialization
Pojo p = (Pojo) deserialize(bytes); // De-serialization
System.out.println(p.getMsg());
}
private static byte[] serialize(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
oos.flush();
oos.close();
return baos.toByteArray();
}
private static Object deserialize(byte[] bytes) throws ClassNotFoundException, IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
Object o = ois.readObject();
ois.close();
return o;
}
}
序列化代理
在默认的反序列化过程中,会序列化系统会通过构造器之外的方式创建对象,这就可能导致一些安全性问题(因为序列化的格式是公开的,任何人都可以根据格式生成任何Serializable
类的字节码),想想这个还是有些恐怖的(更多的攻击方式可以查看《EffectiveJava》第77条·)。在《Effectivie Java》中,作者建议我们使用静态内部类作为代理来进行类的序列化,废话不说,直接上码:
样例代码3
/**
* Persion类除了writeReplace在序列化时用到,其他的黑科技方法都不会被调用
* 原因是我们在writeReplace中将序列化对象替换成了内部代理类,所以以后的序列化过程就是针对SerializableProxy
*/
class Persion implements Serializable {
public String desc;
public Persion(String desc) {
this.desc = desc;
}
static class SerializableProxy implements Serializable{
private String desc;
private SerializableProxy(Persion s) {
this.desc = s.desc;
}
/**
* 在这里恢复外围类
* 注意看这里!!!最大的好处就是我们最后得到的外围类是通过构造器构建的!
* @return
*/
private Object readResolve() {
return new Persion(desc);
}
}
/**
* 外围类直接替换成静态内部代理类作为真正的序列化对象
* @return
*/
private Object writeReplace() {
return new SerializableProxy(this);
}
/**
* 这里主要是为了防止攻击,任何以Persion声明的对象字节流都是流氓!!
* 因为我在writeReplace中已经把序列化的实例指向了SerializableProxy
* @param stream
* @throws InvalidObjectException
*/
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("proxy requied!");
}
}
好了,Java
序列化的内容就是这些了,希望你完全GET
到了。_
参考文章:
parcelable-vs-serializable