Effective java笔记(十),序列化
将一个对象编码成字节流称作将该对象「序列化」。相反,从字节流编码中重新构建对象被称作「反序列化」。一旦对象被「序列化」后,它的编码就可以从一台虚拟机传递到另一台虚拟机,或被存储到磁盘上,供以后「反序列化」使用。序列化技术为JavaBean组件结构提供了标准的持久化数据格式。
74、谨慎的实现Serializable接口
一个类实现Serializable接口需要付出的代价:
- 一旦一个类被发布,就大大降低了「改变这个类的实现」的灵活性。若一个类实现了Serializable接口,它就成了这个类导出API的一部分。
- 增加了出现Bug和安全漏洞的可能性。序列化机制是一种语言之外的对象创建机制,反序列化是一个「隐藏的构造器」,具备与其他构造器相同的特点。因此,反序列化过程必须要保证所有的约束关系。
- 随着发行新的版本,相关的测试负担也增加了。
每个可序列化的类都有一个唯一的名为serialVersionUID的标识号与它相关联。若类在私有的静态final的long域中没有显式的指定这个标识号,系统就会自动的为该类产生一个标识号,这时类的兼容性将会遭到破坏,在运行时导致InvalidClassException异常。
为了继承而设计的类应该尽可能少的去实现Serializable接口,用户自定义的接口也应该尽可能少的继承Serializable接口。例外,Throwable、Component和HttpServlet抽象类。
内部类不应该实现Serializable即可。静态成员类可以实现Serializable接口。
75、考虑使用自定义的序列化形式
对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法(存储结构)应该是独立的。如果一个对象的物理表示法等同于它的逻辑内容,就适用于使用默认的序列化形式。如:
public class Name implements Serializable {
/**
* Last name. Must be non-null.
* @serial
*/
private final String lastName;
/**
* first name. Must be non-null.
* @serial
*/
private final String firstName;
private final String middleName;
....
}
在这段代码中,Name类的实例域精确的反应了它的逻辑内容,可以使用默认的序列化形式。注意:虽然lastName、firstName和middleName域是私有的,但它们仍然需要有注释文档。因为,这些私有域定义了一个公有的API,即这个类的序列化形式。@serial标签用来告知Javadoc工具,把这些文档信息放在有关序列化形式的特殊文档页中。
当一个对象的物理表示法与它的逻辑内容之间有实质性的不同时,使用默认序列化形式有如下缺点:
- 它将这个类的导出API永远束缚在了该类的内部表示法上。如,私有内部类变成公有API的一部分。
- 会消耗过多的空间和时间
- 会引起栈溢出
- 其约束关系可能遭到严重破坏,如散列表
如:
//默认序列化形式
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry implements Serializable {
String data;
Entry next;
Entry previous;
}
....
}
自定义序列化:
public final class StringList implements Serializable {
private static final long serialVersionUID = ...;
private transient int size = 0; //不会被序列化
private transient Entry head = null;
private static class Entry {
String data;
Entry next;
Entry previous;
}
public final void add(String s) { ... }
/**
* Serialize this {@code StringList} instance
*
* @serialData The size of the list (the number of strings it contains)
* is emitted ({@code int}), followed by all of its elements (each a
* {@code String}), in the proper sequence.
*/
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(size);
for(Entry e = head; e != null; e = e.next ) {
s.writeObject(e.data);
}
}
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
int num = s.readInt();
for(int i=0; i < num; i++) {
add((String)s.readObject());
}
}
.....
}
注意:尽管StringList的所有域都是transient,但writeObject和readObject的首要任务仍是调用defaultXxxObject方法,这样可以极大的增强灵活性。另外尽管writeObject是私有的,仍然需要文档注释。
无论自定义序列化还是默认序列化,对于一个线程安全的对象,必须在序列化方法上强制同步。如:
private synchronized void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
}
总之,当要将一个类序列化时,应该仔细考虑采用默认序列化还是自定义序列化。选择错误的序列化形式对于一个类的复杂性和性能都会有永久的负面影响。
76、保护性编写readObject方法
readObject方法实际上相当于一个公有的构造器,如同其它构造器一样,readObject方法必须检查其参数的有效性,并且在必要的时候进行保护性拷贝。readObject是一个「用字节流作为唯一参数」的构造器,当面对一个人工仿造的字节流时,readObject产生的对象可能会违反它所属类的约束条件,所以必须在readObject中增加约束性检查,若有效性检查失败,抛出InvalidObjectException异常。如:
public final class Period {
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 getStart() {
return new Date(start.getTime());
}
public Date getEnd() {
return new Date(end.getTime());
}
//反序列化时增加约束条件
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
if(start.compareTo(end) > 0) {
throw new InvalidObjectException(start + " after " + end);
}
}
}
在这段代码中,尽管readObject中增加了有效性检查,但通过伪造字节流创建可变的Period实例仍是可能的。做法是:字节流以Period实例开头,然后附加上两个额外的引用执行Period实例中两个私有的Date域。攻击者从ObjectInputStream中读取Period实例,然后读取其后的「恶意引用」,通过这个引用攻击者就可以修改Period中私有的Date域。如:
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
Period period = new Period(new Date(), new Date());
out.writeObject(period);
byte[] ref = {0x71, 0, 0x7e, 0, 5}; //指向period中私有域start的字节
bos.write(ref);
ref[4] = 4; //指向period中私有域end的字节
bos.write(ref);
//反序列化
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Period period1 = (Period)in.readObject();
//ref1指向period1中私有域start指向的对象,可通过这个引用修改不可变对象
Date ref1 = (Date)in.readObject();
Date ref2 = (Date)in.readObject();
因此,对于每个可序列化的不可变类,若它包含了私有的可变组件(对象的引用),那么在它的readObject方法中,必须对这些组件进行保护性拷贝。否则,它内部的约束条件可能遭受破坏。如:
private void readObject(ObjectInputStream s) {
s.defaultReadObject();
start = new Date(start.getTime());
end = new Date(end.getTime());
if(start.compareTo(end) > 0) {
throw new InvalidObjectException(start + " after " + end);
}
}
注意:final域必须在对象构造时初始化,为了使用readObject方法,必须将start和end域做成非final的。
编写readObject方法的指导原则:
- 对于对象引用域必须保持为私有的类,要保护性的拷贝这些域中的每个对象。
- 对于任何约束条件,若检查失败,则抛出一个InvalidObjectException异常。检查应在保护性拷贝之后。
- 无论直接方式还是间接方式,都不要调用类中任何可被覆盖的方法,否则反序列时可能会失败。
77、对于实例控制,枚举类型优先于readResolve
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { }
....
}
如上所示,若这个Singleton类的声明上加上「implements Serializable」,它就不再是一个Singleton。无论该类使用的是默认的序列化形式还是自定义的序列化形式。因为任何一个readObject方法,它都会返回一个新建的实例。
对于一个正在被反序列化的对象,若它的类定义了一个readResolve方法,那么在反序列化后,新建对象上的readResolve方法就会被调用。然后该方法返回的对象引用将被返回,取代新建的对象,而新建的对象将被垃圾回收。
public class Elvis implements Serializable {
public static final transient Elvis INSTANCE = new Elvis();
private Elvis() { }
....
private Object readResolve() {
//Return the one true Elvis
return INSTANCE;
}
}
该方法忽略了被反序列化的对象,只返回该类初始化时创建的Elvis实例。若依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient的。,否则能被人工仿造的字节流攻击。静态成员不属于对象,不参与序列化。
通过将一个可序列化的实例受控的类编写成枚举,可以绝对保证除了所声明的常量外,不会有别的实例。如:
public enum Elvis {
INSTANCE;
....
}
另外,readResolve的可访问性很重要。若把readResolve方法放在一个final类上,它就应该是私有的。若readResolve方法是受保护的或共有的,并且子类没有覆盖它,对序列化过的子类实例进行反序列化,就会产生一个超类实例,这可能导致ClassCastException异常。
总之,应该尽可能的使用枚举类型来实施实例控制的约束条件,若做不到,就必须提供一个readResolve方法,并将引用类型的域声明为transient的。
78、考虑用序列化代理代替序列化实例
序列化代理模式能够极大的减少实现Serializable接口所带来的风险。
实现序列化代理模式的步骤:
- 首先为可序列化的类设计一个私有的静态嵌套类,精确的表示外围类实例的逻辑状态。它有一个单独的构造器,其参数类型为外围类。外围类及其序列化代理都必须实现Serializable接口。
- 将writeReplace方法添加到外围类中。
- 在SerializableProxy类中提供readResolve方法,它返回逻辑上相等的外围类的实例。
如:
//外围类不需要serialVersionUID
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 getStart() {
return new Date(start.getTime());
}
public Date getEnd() {
return new Date(end.getTime());
}
//在序列化之前,将外围类的实例转变成它的序列化代理
private Object writeReplace(){
return new SerializationProxy(this);
}
//防止被攻击者使用
private void readObject(ObjectInputStream stream)
throws InvalidObjectException{
throw new InvalidObjectException("Proxy required");
}
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = ...;
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);
}
}
}
正如保护性拷贝一样,序列化代理可以阻止伪字节流的攻击及内部域的盗用攻击。与使用保护性拷贝不同,使用序列化代理允许Period的域为final的,这可以保证Period类真正不可变。序列化代理模式更容易实现,它不必考虑哪些域会被序列化攻击,也不必显示的执行有效性检查。
序列化代理的局限性:不能与可以被客户端扩展的类兼容,也不能与对象图中包含循环的类兼容,比保护性拷贝性能低。
Effective java笔记(十),序列化的更多相关文章
- Effective Java笔记一 创建和销毁对象
Effective Java笔记一 创建和销毁对象 第1条 考虑用静态工厂方法代替构造器 第2条 遇到多个构造器参数时要考虑用构建器 第3条 用私有构造器或者枚举类型强化Singleton属性 第4条 ...
- effective java笔记之单例模式与序列化
单例模式:"一个类有且仅有一个实例,并且自行实例化向整个系统提供." 单例模式实现方式有多种,例如懒汉模式(等用到时候再实例化),饿汉模式(类加载时就实例化)等,这里用饿汉模式方法 ...
- Effective java笔记(二),所有对象的通用方法
Object类的所有非final方法(equals.hashCode.toString.clone.finalize)都要遵守通用约定(general contract),否则其它依赖于这些约定的类( ...
- effective java笔记之java服务提供者框架
博主是一名苦逼的大四实习生,现在java从业人员越来越多,面对的竞争越来越大,还没走出校园,就TM可能面临失业,而且对那些增删改查的业务毫无兴趣,于是决定提升自己,在实习期间的时间还是很充裕的,期间自 ...
- Effective java -- 9 并发/序列化
关于同步的问题,想弄明白java,同步不会是不行的.这不书弄完后还会从<java并发编程实战>和<java并发编程的艺术>选一本或者都看. 第六十六条:同步访问共享的可变数据说 ...
- Effective java笔记(一),创建与销毁对象
1.考虑用静态工厂方法代替构造器 类的一个实例,通常使用类的公有的构造方法获取.也可以为类提供一个公有的静态工厂方法(不是设计模式中的工厂模式)来返回类的一个实例.例如: //将boolean类型转换 ...
- Effective java笔记(六),方法
38.检查参数的有效性 绝大多数方法和构造器对于传递给它们的参数值都会有限制.如,对象引用不能为null,数组索引有范围限制等.应该在文档中指明所有这些限制,并在方法的开头处检查参数,以强制施加这些限 ...
- Effective java笔记5--通用程序设计
一.将局部变量的作用域最小化 本条目与前面(使类和成员的可访问能力最小化)本质上是类似的.将局部变量的作用域最小化,可以增加代码的可读性和可维护性,并降低出错的可能性. 使一个局部变量的作用 ...
- effective java 笔记1--序言
一.序言 程序设计的几条基本原则: 1.清晰性和简洁性最为重要,模块的用户永远也不应该被模块的行为所迷惑,所以写良好的注释是必需的. 2.模块要竟可能小,但也不能太小,好一个深奥的哲学问题. 3.代码 ...
随机推荐
- ABP理论学习之内嵌资源文件
返回总目录 本篇目录 介绍 创建内嵌文件 暴露内嵌文件 使用内嵌文件 介绍 在一个web应用中,有供客户端使用的javascript,css,xml等文件.它们一般是作为分离的文件被添加到web项目中 ...
- NoSQL初探之人人都爱Redis:(3)使用Redis作为消息队列服务场景应用案例
一.消息队列场景简介 “消息”是在两台计算机间传送的数据单位.消息可以非常简单,例如只包含文本字符串:也可以更复杂,可能包含嵌入对象.消息被发送到队列中,“消息队列”是在消息的传输过程中保存消息的容器 ...
- 当程序以Windows Services形式启动时当前路径不对
当程序以Windows Services形式启动时当前路径不对 @(操作系统)[博客|dotNet] 很多时候我们需要将我们的程序写成利用Windows服务的形式来让它能够自启动.今天遇到一个问题,当 ...
- 60分钟Python快速学习(给发哥一个交代)
60分钟Python快速学习 之前和同事谈到Python,每次下班后跑步都是在听他说,例如Python属于“胶水语言啦”,属于“解释型语言啦!”,是“面向对象的语言啦!”,另外没有数据类型,逻辑全靠空 ...
- Rxjava异常处理
异常处理 在Rxjava订阅的Observable有时会抛出异常,在RxJava中有两大类策略,一个是准备备用的Observable,在发生异常时将subscriber订阅到新的Observable上 ...
- struts tags
HTTP ERROR 500 Problem accessing /showognl.jsp. Reason: Server Error Caused by: org.apache.jasper.Ja ...
- Hibernate增删查改语句
我用的数据库是MySQL,实体类叫Product create table Product ( proId integer not null auto_increment, proName varch ...
- WebDriver--操控浏览器
前一篇讲述了元素的定位方法,现在开始练习如何写自动化测试脚本 我使用的编辑工具是PyCharm,今后该博客中所写的有关Python脚本,都是在该工具中编写的. WebDriver提供了控制浏览器大小. ...
- SikuliX简介及安装
一.简单介绍 SikuliIDE和Sikuli Script就是现在的SikuliX,最新版本是SikuliX1.1.0, 部分兼容Sikuli JAVA API,支持Python和Ruby,Siku ...
- Jser 设计模式系列之面向对象 - 接口封装与继承
GOF在<设计模式>中说到:面向接口编程,而非面向实现编程 鉴于此,这个概念可见一斑! JS却不像其他面向对象的高级语言(C#,Java,C++等)拥有内建的接口机制,以确定一组对象和另一 ...