将一个对象编码成字节流称作将该对象「序列化」。相反,从字节流编码中重新构建对象被称作「反序列化」。一旦对象被「序列化」后,它的编码就可以从一台虚拟机传递到另一台虚拟机,或被存储到磁盘上,供以后「反序列化」使用。序列化技术为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笔记(十),序列化的更多相关文章

  1. Effective Java笔记一 创建和销毁对象

    Effective Java笔记一 创建和销毁对象 第1条 考虑用静态工厂方法代替构造器 第2条 遇到多个构造器参数时要考虑用构建器 第3条 用私有构造器或者枚举类型强化Singleton属性 第4条 ...

  2. effective java笔记之单例模式与序列化

    单例模式:"一个类有且仅有一个实例,并且自行实例化向整个系统提供." 单例模式实现方式有多种,例如懒汉模式(等用到时候再实例化),饿汉模式(类加载时就实例化)等,这里用饿汉模式方法 ...

  3. Effective java笔记(二),所有对象的通用方法

    Object类的所有非final方法(equals.hashCode.toString.clone.finalize)都要遵守通用约定(general contract),否则其它依赖于这些约定的类( ...

  4. effective java笔记之java服务提供者框架

    博主是一名苦逼的大四实习生,现在java从业人员越来越多,面对的竞争越来越大,还没走出校园,就TM可能面临失业,而且对那些增删改查的业务毫无兴趣,于是决定提升自己,在实习期间的时间还是很充裕的,期间自 ...

  5. Effective java -- 9 并发/序列化

    关于同步的问题,想弄明白java,同步不会是不行的.这不书弄完后还会从<java并发编程实战>和<java并发编程的艺术>选一本或者都看. 第六十六条:同步访问共享的可变数据说 ...

  6. Effective java笔记(一),创建与销毁对象

    1.考虑用静态工厂方法代替构造器 类的一个实例,通常使用类的公有的构造方法获取.也可以为类提供一个公有的静态工厂方法(不是设计模式中的工厂模式)来返回类的一个实例.例如: //将boolean类型转换 ...

  7. Effective java笔记(六),方法

    38.检查参数的有效性 绝大多数方法和构造器对于传递给它们的参数值都会有限制.如,对象引用不能为null,数组索引有范围限制等.应该在文档中指明所有这些限制,并在方法的开头处检查参数,以强制施加这些限 ...

  8. Effective java笔记5--通用程序设计

    一.将局部变量的作用域最小化      本条目与前面(使类和成员的可访问能力最小化)本质上是类似的.将局部变量的作用域最小化,可以增加代码的可读性和可维护性,并降低出错的可能性. 使一个局部变量的作用 ...

  9. effective java 笔记1--序言

    一.序言 程序设计的几条基本原则: 1.清晰性和简洁性最为重要,模块的用户永远也不应该被模块的行为所迷惑,所以写良好的注释是必需的. 2.模块要竟可能小,但也不能太小,好一个深奥的哲学问题. 3.代码 ...

随机推荐

  1. poj1002-487-3279(字符串处理)

    一,题意: 中文题,不解释!二,思路: 1,处理输入的电话号码 2,排序num[]数组 3,输出三,步骤: 1,消除 -.Q.Z 三种字符,将一个电话号码转化为一个整数存如num[]数组 如:num[ ...

  2. 学习笔记: Delphi之线程类TThread

    新的公司接手的第一份工作就是一个多线程计算的小系统.也幸亏最近对线程有了一些学习,这次一接手就起到了作用.但是在实际的开发过程中还是发现了许多的问题,比如挂起与终止的概念都没有弄明白,导致浪费许多的时 ...

  3. 关于MongoDB你需要知道的几件事

    Henrique Lobo Weissmann是一位来自于巴西的软件开发者,他是itexto公司的联合创始人,这是一家咨询公司.近日,Henrique在博客上撰文谈到了关于MongoDB的一些内容,其 ...

  4. 眼见为实:.NET类库中的DateTimeOffset用途何在

    在 EnyimMemcachedCore(支持.NET Core的memached客户端)中实现 Microsoft.Extensions.Caching.Distributed.IDistribut ...

  5. Mac下设置Android源代码编译环境

    在Mac下编译Android最麻烦的就是设置Android的编译环境了,做完这一步基本上剩下的就是近乎傻瓜式的操作了.说起来也简单就三步,设置大小写敏感的文件系统.安装编译工具.设置文件系统同时能打开 ...

  6. 解决微信公众号OAuth出现40029(invalid code,不合法的oauth_code)的错误

    关于OAuth 官方教程:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842&token=&la ...

  7. C语言 · 送分啦

    问题描述 这题想得分吗?想,请输出"yes":不想,请输出"no". 输出格式 输出包括一行,为"yes"或"no". ...

  8. Uiautomator 2.0之BySelector类学习小记

    1. BySelector与By静态类 1.1 BySelector类为指定搜索条件进行匹配UI元素, 通过UiDevice.findObject(BySelector)方式进行使用. 1.2 By类 ...

  9. NULL的陷阱:Merge

    NULL表示unknown,不确定值,所以任何值(包括null值)和NULL值比较都是不可知的,在on子句,where子句,Merge或case的when子句中,任何值和null比较的结果都是fals ...

  10. 深入学习jQuery节点关系

    × 目录 [1]后代元素 [2]祖先元素 [3]兄弟元素 前面的话 DOM可以将任何HTML描绘成一个由多层节点构成的结构.节点之间的关系构成了层次,而所有页面标记则表现为一个以特定节点为根节点的树形 ...