序列化

大纲:

  • 序列化是什么
  • 序列化解决了什么问题
  • 序列化的具体实现
  • 序列化代理的意图与实现
  • 序列化要注意的点
  • android中使用的序列化方式
  • 序列化的使用 深克隆

定义

将对象编码成一个字节流称之为 序列化

从字节流中重新构建对象称之为 反序列化

有一种更直接的说法,序列化其实就是给写入字节流中的每一个对象 进行编号,而反序列化就是根据对象的编号找到相应对象的引用

输入输出流

image.png
输入与输出是相对的,一般从内存的角度来看:

1.从文件写入内存,为输入流(InputStream),是将二进制流转换为对象的运行时数据结构,对应的是 反序列化 的过程,输入流对应的是read方法

2.从内存写入文件,为输出流(OutputStream),是将对象转换为二进制流,该过程对应的是 序列化 的过程,输出流对应的是write方法

其实输入输出流与序列化与反序列化没有直接联系,这里联系在一起是为了方便记忆

序列化的作用

1
2
3
1.持久化 序列化很大一部分原因是为了进行持久化操作

2.传输 例如跨进程传输,远程传输(RMI)

序列化的实现

对象需要实现序列化,必须要实现 Serializable 接口,如果该对象的成员变量是引用类型则该成员变量,也需要实现 Serializable 接口,否则序列化会失败

下面是标准的序列化与反序列化的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(obj);

byte[] bytes = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
Object o = ois.readObject();

ois.close();
bis.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}

序列化的实现分为使用默认的序列化方式与自定义的序列化方式

标准的序列化

标准的序列化流程比较简单:

1.写入对象的类型信息

2.写入对象成员变量的 类型信息 以及 变量名 并编号,类型信息包含serialVersionUID

3.如果成员变量是引用类型或数组,则需要写入数组与类在虚拟机中的表现类型

4.写入对象的值,由于前面已经将类型的引用全部编号,后续可以直接引用

5.如果是基本类型(除数组与引用类型)直接写入String

6.如果是已经类型,则需要递归前面的操作

自定义序列化

之所以会有自定义的序列化,原因有以下几个:

1.安全校验 对于安全性要求比较严格的数据一般需要通过比较严格的数据校验才使用

2.独立控制序列化与反序列化的流程,方便定制

readObject方法 readObject方法相当于类的另一个共有的构造器,构造器必须要做参数合法性校验,并在有必要时对参数进行保护性拷贝

writeObject方法 如果类中存在该方法,则对象的序列化只会执行该方法而放弃默认的序列化方式,所以如果仅仅是只做参数的安全性校验还需要调用默认的序列化方法

readResolve方法 在单例模式下,如果当前单例类实现了序列化接口,那么该单例就有可能在反序列化时跳过构造函数,而产生不同的对象,所以有必要在readResolve方法中将返回值手动指向创建的全局单例对象

序列化代理

序列化代理是目前认为比较好的对象序列化的方法,原因是通过序列化代理可以控制对象的创建一定需要经过构造函数,从而能在构造函数中对参数进行控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Interval implements Serializable {
private final Date start;
private final Date end;

public Interval(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(start + " after " + end);
}

public Date getStart() {return new Date(start.getTime());}
public Date getEnd() {return new Date(end.getTime());}

@Override
public String toString() {
return "Interval{" + "start=" + start + ", end=" + end + '}';
}

private static class SerializationProxy implements Serializable {
private static final long serivalVersionUID = 213214124141L;
private final Date start;
private final Date end;

SerializationProxy(Interval interval) {
this.start = interval.start;
this.end = interval.end;
}
}

private Object writeReplace() {
return new SerializationProxy(this);
}
}

这个方法的存在导致序列化系统产生一个SerializationProxy实例,代替外围类的实例,也就是说,writeReplace方法在序列化之前,将外围类的实例转变成了它的序列化代理。这样序列化系统就永远不会产生外围类的序列化实例,但是攻击者可能伪造,企图违反该类的约束条件,为了应付这个,我们只需向前面所做的一样,在Interval类里添加一个readObject方法,它禁止了反序列化外围类。

1
2
3
private void readObject(ObjectInputStream ois) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required!");
}

最后,我们只要在SerializationProxy类添加一个readResolve方法,它返回一个逻辑上相当的外围类的实例,它可以让序列化系统在反序列化的时候将序列化代理转变回外围类的实例。

1
2
3
private Object readResolve() {
return new Interval(start, end);
}

这个readResolve方法仅仅利用了外围类的公有构造器创建了外围类的一个实例,它极大的消除了序列化机制中语言本身之外的特征,因为反序列实例是利用与任何其他实例相同的构造器,静态工厂和方法创建的,和直接创建的实例没有什么区别,这样你就不必单独确保反序列化的实例一定要遵守类的约束条件了。
  正如保护性拷贝一样,序列化代理方法可以阻止伪字节流攻击和内部域盗用攻击,而且这种方法允许Interval的域为final,这种方法也也使我们省了不少麻烦,你不必知道哪些域可能受到狡猾的序列化攻击的威胁,你也不必显示地执行有效检查,作为反序列化的一部分。

transient关键字

如果对象的成员变量被transient关键字修饰,那么在序列化与反序列化的过程中会忽略该字段

serialVersionUID

如果一个类实现了Serializable接口,那么请尽量提供serialVersionUID的值,目前这个值有两种提供方式,一个是使用默认的值,一种是编译器提供一个随机数。

如果没有提供UID字段,那么系统在序列化与反序列化会通过一系列的散列算法计算出一个对应的值,如果当前的类在序列化之后发生了改变,那么两个类在反序列化时由于生成的UID对应不上会报错。

1
**如果当前类实现了Serializable接口,为了避免在后面类的迭代过程中反序列化出错,请显示提供serialVersionUID字段**

深克隆

由于通过序列化与反序列化产生的是完全不同的对象,所以可以用来做对象的深克隆,但是深克隆构建对象比复制或者克隆数据域的方法要慢很多

实现代码在上面

1
如果一个对象被同时序列化两次,即便后一次对当前对象的属性作了修改,在序列化时由于当前的对象已经存在,所以存在序列化流中的只是当前对象的一个引用,反序列化出来的是同一个对象。

android中的序列化方式 parelable

1.serializable一般用于数据的持久化,使用I/O读写存储于硬盘
2.parcelable直接在内存中读写,速度要比serializable快很多
3.serializable会使用到反射,而反射是很耗性能的操作
4.arcelable是自己实现封送与解封,过程中不涉及反射等耗性能操作,数据也存储在Native内存中,效率比serializable要高很多

1
综上:serializable实现简单,性能消耗大,效率低,内存占用大。 parcelable实现相对复杂,但是内存占用率较低,且没有反射等耗性能操作,效率极高。在android等移动设备中应该优先使用parcelable
-------------本文结束感谢您的阅读-------------