现在的位置: 首页 > 综合 > 正文

《Effective java》读书笔记10——序列化

2013年10月05日 ⁄ 综合 ⁄ 共 5163字 ⁄ 字号 评论关闭

对象序列化是一个提供将对象编码成字节流,并从字节流编码中重新构建对象的框架,包括以下两个过程:

序列化:将对象编码成字节流。

反序列化:从字节流编码中重新构建对象。

一旦对象被序列化后,对象的字节流编码就可以从一台正在运行的java虚拟机被传递到另一台java虚拟机上,或者被存储到硬盘上,以便以后发序列化时使用。序列化技术为远程通信提供了标准的线路级对象表示法,也为JavaBean提供了标准的持久化数据格式。

谨慎使用序列化接口:

JDK对序列化的支持相当简单,只需要实现一个序列化标记接口Serializable即可,但是序列化在实际使用过程中会遇到长期开销大的问题,《Effective java》中指出了序列化3大代价:

(1).一旦一个类被发布,就大大降低了“改变类的实现”的灵活性。

如果一个类实现了序列化标记接口,它的字节流编码(序列化形式)就变成了它导出API的一部分,一旦这个类被广泛使用,往往必须永远支持这种序列化形式。如果不设计一种自定义的序列化形式,而仅仅使用默认的序列化形式,则类的序列化形式将被永远束缚在类最初的内部表示法上,即如果使用默认的序列化形式,类中私有的和包私有的实例域都将变成到的API的一部分,不方便类的演进和兼容。

下面是一个序列化类不兼容的例子:

java序列化类中有与流的唯一标识符(Streamunique identifier)有关的字段,也叫序列版本UID(Serialversion UID),这个标识号唯一地与类相关联,如果在序列化的类中没有显式指定,则系统会根据类名称、类所实现的接口名称、以及类中所有的公有和受保护成员以及方法名称生成一个运行时的标识号。

如果类的新版本中演变修改的某些名称,例如增加了某些方法或字段等,自动产生的序列版本也会跟着发生变化,因此就会经常出现序列化不兼容异常如下:

Caused by: java.io.InvalidClassException: models.member.Member; local class incompatible: stream classdesc serialVersionUID = 8996579512119659486, local class serialVersionUID = -7513555048418418149

异常原因:

远程的序列化版本是8996579512119659486,本地的序列化版本是-7513555048418418149

解决方法:

将本地的序列化的类中的版本号改成和远程中一样,即在本地的序列化类里的private static final long serialVersionUID =  8996579512119659486.

预防方法:取消JDK自动生成序列化版本,在序列化类中显式指定序列化版本如下:

private static final longserialVersionUID =  1L;

关于序列化不兼容问题,大概有以下几个原因:

a.该类的序列版本号与从流中读取的类描述符的版本号不匹配(jdk版本可能更换会造成这个问题)

b.该类包含未知数据类型

c.该类没有提供可访问的无参数构造方法

(2).序列化增加了出现Bug和安全漏洞的可能性。

通常情况下,对象是利用构造器来创建的,序列化机制提供了另一种对象创建机制,无论是否接受默认的行为,还是覆盖默认的行为,反序列化机制都是一个隐藏的构造器,具备与其他构造器相同的特点。

因为反序列化中没有显式的构造器,所以很容易忘记要确保:反序列化过程必须也要保证所有“由真正的构造器建立起来的约束关系”,并且不允许攻击者访问正在构造过程中的对象的内部信息。

依靠默认的反序列化机制,很容易使对象的约束关系遭到破坏,以及遭受非法访问。

(3).随着类发行新版本,相关的测试负担也大大增加。

当一个可序列化类被修改的时候,需要检查以下两个关键点:

a.在新版本中序列化一个实例,然后在旧版本中反序列化。

b.在旧版本中序列化一个实例,然后在新版本中反序列化。

上述的测试工作量与可序列化类数量和发行版本的乘积成正比,除了要测试二进制兼容性(序列化-反序列化),还需要测试语义兼容性(反序列化的结果是序列化对象的复制品)。

序列化的使用原则:

(1).依赖序列化传输或者存储的对象需要实现序列化:

使用RMI协议传输对象如EJB的实体Bean等就需要序列化,如果一个类将要加入到某个框架中,并且该框架依赖于序列化来实现对象传输或者持久化,则必须实现序列化。

若一个类要成为一个序列化类的组件,则最好对类进行序列化以方便使用。

一般的值类如Data,BigInteger和大多数的集合类应该实现序列化,而代表活动实体的类如线程池则一般不应被序列化。

(2).为了继承而设计的类和用户接口应该尽可能少地去实现序列化:

专门为了继承而设计的类和用户接口如果没有实现了序列化,在扩展类或者实现接口的子类也是不可序列化的,如果超类没有提供可访问的无参构造器,则子类也不可能做到可序列化,如果不可序列化类的子类可能需要序列化,则一定要在不可序列化的基类中添加无参构造器。

某些情况下,为了继承而设计的类和用户接口参与到需要序列化框架中,也是可以被序列化的,JDK中为了继承而设计的类中,真正实现了序列化接口的有:

Throwable类:RMI异常可以从服务器端传回到客户端。

Component抽象类:GUI可以被发送、保存和恢复。

HttpServlet抽象类:HTTP会话状态可以被缓存。

(3).内部类不应该实现序列化:

内部类使用编译器产生的合成域来保存指向外部类实例引用,以及保存来自外部作用域的局部变量的值,java规范对应内部类合成域如果对应到类定义中没有明确的规定,因此内部类的默认序列化形式定义是不清楚的。

注意:静态成员内部类可以实现序列化。

自定义序列化形式:

默认序列化形式与自定义序列化形式比较:

(1).默认序列化形式:

描述了该对象内部所包含的所有的非transient数据,以及每一个可以从该对象到达的其他对象的内部数据,包含了所有这些对象被链接起来后的拓扑结构。

好处是程序员不需要额外工作,由java虚拟机实现。缺点是类的序列化形式永远绑定在原始类表示上面,不方便扩展兼容。

一个使用双向链表实现字符串列表的默认序列化例子如下:

public final class StringList implements SeriaLizable{
	private int size = 0;
	private Entry head = null;
	private static class Entry implements Serializable{
	String date;
	Entry next;
	Entry previous;
}
public final void add(String s){……}
}

使用默认序列化形式,序列化将会保存链表中所有的项以及这些项之间的所有双向链接,这会导致以下4个缺点:

a.类导出的API永远束缚在该类的内部表示法上,即绑定在了双向链表上,如果以后向使用其他数据结构替换双向链表将会变得非常困难。

b.消耗过多的空间,默认的序列化由于保存了双向链表中所有的项以及这些项之间的链接关系,导致序列化形式庞大,占用了过多的空间。

c.消耗过多的时间,序列化逻辑并不了解对象图的拓扑关系,导致必须要经过一个开销很大的图遍历。

d.引起栈溢出,默认的序列化过程需要对对象图执行一次递归遍历,导致栈溢出。

(2).自定义序列化形式:

可以自定义在序列化和反序列化时写出和读入的数据,只包含该对象表示的逻辑数据,不需要包含数据拓扑结构屋里表示法。

自定义序列化可以从灵活性、性能和正确性等多个方面满足程序需求。

使用自定义序列化形式的双向链表实现字符串列表例子如下:

public final class StringList implements SeriaLizable{
	//实例域都是瞬时的,即不能默认序列化
private transient int size = 0;
	private transient Entry head = null;
	private static class Entry {//链表不再需要序列化
	String date;
	Entry next;
	Entry previous;
}
public final void add(String s){……}
//自定义序列化对象
private void writeObject(ObjectOutputStream os) throws IOException{
	os.defaultWriteObject();//默认序列化
	os.writeInt(size);//序列化列表的大小
	//序列化列表元素数据
	for(Entry e = head; e != null; e = e.next){
	os.writeObject(e.data);
}
}
//自定义反序列化对象
private void readObject(ObjectInputStream os) throws IOException, ClassNotFoundException{
	os.defaultReadObject();//默认反序列化
	int numElements = os.readInt();//反序列化列表大小
	//反序列化列表元素数据
	for(int i = 0; i < numElements; i++){
	add((String)s.readObject());
}
}
}

虽然字符串列表所有的域都是瞬时的,但是序列化和反序列化方法的首要任务仍然是调用默认的序列化和反序列化方法,这样做的好处是极大的增强了灵活性,可以允许以后在演进类中添加了非瞬时的实例域并且保证向前或者向后兼容性。

如果一个对象的物理表示法等同于它的逻辑内容时,就可以使用默认序列化,否则就需要权衡比较默认序列化和自定义序列化。

注意:在反序列化的readObject方法中,为了防止违反约束条件和伪造字节流攻击,推荐使用保护性拷贝,紧跟着保护性拷贝之后进行约束验证。

序列化代理模式:

使用序列化代理模式可以使用java语言机制来创建实例对象,避免序列化构造对象时的安全性问题和各种各样的错误。

序列化代理模式例子如下:

public final Class Period implements Serializable {
	private final Date start;
	private final Date end;
	public Period(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 start(){
	return new Date(start.getTime());
}
public Date end(){
	return new Date(end.getTime());
}
//在序列化之前调用,将外部类实例转变成内部序列化代理类
private Object writeReplace(){
	return new SerializationProxy(this);
}
//防止攻击者在反序列化时伪造外部类实例当作序列化对象
private void readObject(ObjectInputStream in) throws InvalidObjectException{
	throw new InvalidObjectException(“Proxy required”);
}
	//序列化代理类
	private static class SerializationProxy implements Serializable{
private static final long serialVersionUID = 1L;
private final Date start;
		private final Date end;
		SerializationProxy(Period p){
		this.start = p.start;
		this.end = p.end;
}
//在反序列化时将代理转换为外部类实例
private Object readResolve(){
		return new Period(start, end);
}
}
}

序列化代理模式有两个局限性:

(1).不能与可以被客户端扩展的类兼容。

(2).不能与对象图中包含循环的某些类兼容:

如果企图从一个对象的序列化代理的readResolve方法内部调用外部对象中的方法,就会得到一个ClassCastException异常,因为此时还没有外部类对象,只有它的序列化代理。

抱歉!评论已关闭.