Effective Java之对于实例控制,枚举类型优于readResolve(七十七)

Object  readResolve()

这个方法会紧挨着readObject()之后被调用,该方法的返回值将会代替原来反序列化的对象,而原来readObject()反序列化的对象将会立即丢弃。

readObject()方法在序列化单例类时尤其有用。当然,如果使用java5提供的enum来定义枚举类,则完全不用担心,程序没有任何问题。

我们从一个单例模式开始:

    public final class MySingleton {  
     private MySingleton() { }  
     private static final MySingleton INSTANCE = new MySingleton();  
     public static MySingleton getInstance() { return INSTANCE; }  
    }  

如果实现了序列化,那么会执行readObject方法或默认的序列化。他们都会返回一个新建的实例,也就违反了单例。

readResolve()方法正好满足需求:

    public final class MySingleton {  
     private MySingleton() { }  
     private static final MySingleton INSTANCE = new MySingleton();  
     public static MySingleton getInstance() { return INSTANCE; }  
     private Object readResolve() throws ObjectStreamException {  
      // instead of the object we're on,   
      // return the class variable INSTANCE  
      return INSTANCE;   
     }  
    }  

书中提到了如果单例实例中存在非transient对象引用,就会有被攻击的危险,例子有点难懂,我在这里简单地解释一下;

因为单例包含一个非transient对象引用域,这个域内容在Singleton的readResolve方法运行之前被反序列化,于是,攻击者截胡,把这段readResolve方法运行之前的字节流截下来(不让他运行readResolve),来个偷梁换柱,把对象引用域指向自己写的“盗用者”。

于是,把readResolve方法运行之前的字节流 换成 偷梁换柱的字节流,再让他执行一次,系统看到单例对象有个非transient对象引用域,指向“盗用者”(因为偷梁换柱了),所以系统也会序列化“盗用者”。

盗用者是这样的:

class ElvisStealer implement Serializable{
    static Elvis impersonator;
    private Elvis payload;
    private Object readResolve(){
        impersonator = payload;
        return new String[]{"foolish"};
    }
}

序列化盗用者“ElvisStealer”的时候,执行它的readResolve()方法,impersonator = payload;偷偷 把第二次序列化的payload 记录下来(作为攻击者,其实没有必要,只是为了展示),然后返回一个错误的对象引用。

但是枚举类型就完全不同了:

// Enum singleton - the preferred approach
public enum Elvis {
    INSTANCE;
    private String[] favoriteSongs =
    { "Hound Dog", "Heartbreak Hotel" };
    public void printFavorites() {
    System.out.println(Arrays.toString(favoriteSongs));
}
}

这样做完全没有后顾之忧,因为枚举enum生来就是支持序列化的,下面的官方对枚举的序列化的声明的翻译:

在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我们看一下这个valueOf方法:

    public static > T valueOf(Class enumType,  
                                                    String name) {  
            T result = enumType.enumConstantDirectory().get(name);  
            if (result != null)  
                return result;  
            if (name == null)  
                throw new NullPointerException("Name is null");  
            throw new IllegalArgumentException(  
                "No enum const " + enumType +"." + name);  
        }  

总结:尽可能用枚举类型来控制实例,否则,就要必须提供readResolve方法,并确保类的所有实例域都是基本类型或transient的。

你可能感兴趣的:(java菜鸟的优化代码之路)