第71条 慎用延迟初始化
延迟初始化是延迟到需要域的值时才将它初始化的这种行为。如果 永远不需要这个值,这个域就永远不会被初始化。这种方法既适用于静态域,也适用于实例域。虽然延迟初始化主要是一种优化,但它也可以用来打破类和实例初始化中的有害循环。
private FieldType field;
public synchronized FieldType getField() {
if (field == null) {
field = computeFieldValue();
}
return field;
}
大多数的域应该正常地进行初始化,而不是延迟初始化。如果为了达到性能目标,或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的延迟初始化方法。对于实例域,就使用双重检查模式(double-check idiom),对于静态域,则使用lazy initialization holder class idiom。对于可以接受重复初始化的实例域,也可以考虑使用单重检查模式(single-check idiom)。
第72条 不要依赖于线程调度器
当有多个线程可以运行时,线程调度器决定哪个线程将会运行,以及运行的时间。但是不同的JVM其调度器的策略大相径庭。所以我们不要去依赖策略的细节。任何依赖线程调度器而达到正确性或性能要求的程序,很可能是不能被移植的。
要编写健壮的,响应良好的,可移植的多线程应用程序,最好的方法是确保可运行程序的平均数量不明县多于处理器的数量。
不要让应用程序的正确性依赖于线程调度器,不要依赖Thread.yield或者线程优先级。否则,应用程序将既不健壮,也不具有可移植性。
第73条 避免使用线程组
第74条 谨慎地使用Serializable接口
实现序列化而付出的最大的代价是,一旦一个类发布,就大大降低了“改变这个类的实现”的灵活性。如果一个类实现了Serializable接口,它的字节码编码就变成了它导出的API的一部分,一旦这个类被使用,往往必须永远支持这种序列化形式。
第75条 考虑自定义的序列化形式
当你决定要将一个类做成可序列化的时候,请仔细考虑应该采用什么样的序列化形式。只有当默认的序列化形式能够合理地描述对象的逻辑状态时,才能使用默认的序列化形式;否则就要设计一个自定义的序列化形式,通过它合理地描述对象的状态。你应该分配足够多的时间来设计类的序列化形式,就好像分配足够多的时间来设计它的导出方法一样。正如你无法在将来的版本中去掉导出方法一样,你也不能去掉序列化形式中的域;它们必须被永久地保留下去,以确保序列化兼容性(serialization compalibility)。选择错误的序列化形式对于一个类的复杂性和性能都会有永久的负面影响。
第76条 保护性地编写readObject接口
- 当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的引用,就必须要做保护性拷贝
- 不要使用writeUnshared和readUnshared方法,它们通常比保护性拷贝更快,但是他们不提供必要的安全性保护。
- 当你编写readObject方法的时候,尽量尊崇以下指导方针:
- 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象,不可变类的可变组建就属于这一类别。
- 对于任何约束条件,如果检查失败则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。
- 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口
- 无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法
第77条 对于实例控制,枚举类型优于readResolve
尽可能使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个即可实例化又是实例受控的类,就必须提供一个readResolve 方法,并确保该类的所有实例都为基本类型,或是transient的。
第78条 考虑用序列化代理代替序列化实例
public class Period implements Serializable{
private static final long serialVersionUID = 1L;
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if(null == start || null == end || start.after(end)){
throw new IllegalArgumentException("请传入正确的时间区间!");
}
this.start = start;
this.end = end;
}
public Date start(){
return new Date(start.getTime());
}
public Date end(){
return new Date(end.getTime());
}
@Override
public String toString(){
return "起始时间:" + start + " , 结束时间:" + end;
}
/**
* 序列化外围类时,虚拟机会转掉这个方法,最后其实是序列化了一个内部的代理类对象!
* @return
*/
private Object writeReplace(){
System.out.println("进入writeReplace()方法!");
return new SerializabtionProxy(this);
}
/**
* 如果攻击者伪造了一个字节码文件,然后来反序列化也无法成功,因为外围类的readObject方法直接抛异常!
* @param ois
* @throws InvalidObjectException
*/
private void readObject(ObjectInputStream ois) throws InvalidObjectException{
throw new InvalidObjectException("Proxy required!");
}
/**
* 序列化代理类,他精确表示了其当前外围类对象的状态!最后序列化时会将这个私有内部内进行序列化!
*/
private static class SerializabtionProxy implements Serializable{
private static final long serialVersionUID = 1L;
private final Date start;
private final Date end;
SerializabtionProxy(Period p){
this.start = p.start;
this.end = p.end;
}
/**
* 反序列化这个类时,虚拟机会调用这个方法,最后返回的对象是一个Period对象!这里同样调用了Period的构造函数,
* 会进行构造函数的一些校验!
*/
private Object readResolve(){
System.out.println("进入readResolve()方法,将返回Period对象!");
// 这里进行保护性拷贝!
return new Period(new Date(start.getTime()), new Date(end.getTime()));
}
}