高效Java(第三版) Effective Java
1. 考虑使用静态工厂方法替代构造方法 2. 当构造方法参数过多时使用builder模式 3. 使用私有构造方法或枚类实现Singleton属性 4. 使用私有构造方法执行非实例化 5. 使用依赖注入取代硬连接资源 6. 避免创建不必要的对象 7. 消除过期的对象引用 8. 避免使用Finalizer和Cleaner机制 9. 使用try-with-resources语句替代try-finally语句 10. 重写equals方法时遵守通用约定 11. 重写equals方法时同时也要重写hashcode方法 12. 始终重写 toString 方法 13. 谨慎地重写 clone 方法 14. 考虑实现Comparable接口 15. 使类和成员的可访问性最小化 16. 在公共类中使用访问方法而不是公共属性 17. 最小化可变性 18. 组合优于继承 19. 如果使用继承则设计,并文档说明,否则不该使用 20. 接口优于抽象类 21. 为后代设计接口 22. 接口仅用来定义类型 23. 优先使用类层次而不是标签类 24. 优先考虑静态成员类 25. 将源文件限制为单个顶级类 26. 不要使用原始类型 27. 消除非检查警告 28. 列表优于数组 29. 优先考虑泛型 30. 优先使用泛型方法 31. 使用限定通配符来增加API的灵活性 32. 合理地结合泛型和可变参数 33. 优先考虑类型安全的异构容器 34. 使用枚举类型替代整型常量 35. 使用实例属性替代序数 36. 使用EnumSet替代位属性 37. 使用EnumMap替代序数索引 38. 使用接口模拟可扩展的枚举 39. 注解优于命名模式 40. 始终使用Override注解 41. 使用标记接口定义类型 42. lambda表达式优于匿名类 43. 方法引用优于lambda表达式 44. 优先使用标准的函数式接口 45. 明智审慎地使用Stream 46. 优先考虑流中无副作用的函数 47. 优先使用Collection而不是Stream来作为方法的返回类型 48. 谨慎使用流并行 49. 检查参数有效性 50. 必要时进行防御性拷贝 51. 仔细设计方法签名 52. 明智而审慎地使用重载 53. 明智而审慎地使用可变参数 54. 返回空的数组或集合不要返回null 55. 明智而审慎地返回Optional 56. 为所有已公开的API元素编写文档注释 57. 最小化局部变量的作用域 58. for-each循环优于传统for循环 59. 熟悉并使用Java类库 60. 需要精确的结果时避免使用float和double类型 61. 基本类型优于装箱的基本类型 62. 当有其他更合适的类型时就不用字符串 63. 注意字符串连接的性能 64. 通过对象的接口引用对象 65. 接口优于反射 66. 明智谨慎地使用本地方法 67. 明智谨慎地进行优化 68. 遵守普遍接受的命名约定 69. 仅在发生异常的条件下使用异常 70. 对可恢复条件使用检查异常,对编程错误使用运行时异常 71. 避免不必要地使用检查异常 72. 赞成使用标准异常 73. 抛出合乎于抽象的异常 74. 文档化每个方法抛出的所有异常 75. 在详细信息中包含失败捕获信息 76. 争取保持失败原子性 77. 同步访问共享的可变数据 78. 避免过度同步 79. EXECUTORS, TASKS, STREAMS 优于线程 80. 优先使用并发实用程序替代wait和notify 81. 线程安全文档化 82. 明智谨慎地使用延迟初始化 83. 不要依赖线程调度器 84. 其他替代方式优于Java本身序列化 85. 非常谨慎地实现SERIALIZABLE接口 86. 考虑使用自定义序列化形式 87. 防御性地编写READOBJECT方法 88. 对于实例控制,枚举类型优于READRESOLVE 89. 考虑序列化代理替代序列化实例

考虑序列化代理替代序列化实例

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

90. 考虑序列化代理替代序列化实例

正如在条目 85和 条目86中提到并贯穿本章的讨论,实现Serializable接口的决定,增加了出现bug和安全问题的可能性,因为它允许使用一种语言之外的机制来创建实例,而不是使用普通的构造方法。然而,有一种技术可以大大降低这些风险。这种技术称为序列化代理模式(serialization proxy pattern)。

序列化代理模式相当简单。首先,设计一个私有静态嵌套类,它简洁地表示外围类实例的逻辑状态。这个嵌套类称为外围类的序列化代理。它应该有一个构造方法,其参数类型是外围类。这个构造方法只是从它的参数拷贝数据:它不需要做任何一致性检查或防御性拷贝。按照设计,序列化代理的默认序列化形式是外围类的最好的序列化形式。外围类及其序列化代理都必须声明以实现Serializable。

例如,考虑在条目 50中编写的不可变Period类,并在条目 88中进行序列化。以下是该类的序列化代理。 Period非常简单,其序列化代理与该属性具有完全相同的属性:

// Serialization proxy for Period class
private static class SerializationProxy implements Serializable {
    private final Date start;

    private final Date end;

    SerializationProxy(Period p) {
        this.start = p.start;
        this.end = p.end;
    }

    private static final long serialVersionUID =
        234098243823485285L; // Any number will do (Item  87)
}

接下来,将以下writeReplace方法添加到外围类中。可以将此方法逐字复制到具有序列化代理的任何类中:

// writeReplace method for the serialization proxy pattern
private Object writeReplace() {
    return new SerializationProxy(this);
}

该方法在外围类上的存在,导致序列化系统发出SerializationProxy实例,而不是外围类的实例。换句话说,writeReplace方法在序列化之前将外围类的实例转换为它的序列化代理。

使用此writeReplace方法,序列化系统永远不会生成外围类的序列化实例,但攻击者可能会构造一个实例,试图违反类的不变性。 要确保此类攻击失败,只需把readObject方法添加到外围类中:

// readObject method for the serialization proxy pattern
private void readObject(ObjectInputStream stream)
        throws InvalidObjectException {
    throw new InvalidObjectException("Proxy required");
}

最后,在SerializationProxy类上提供一个readResolve方法,该方法返回外围类逻辑等效的实例。此方法的存在导致序列化系统在反序列化时把序列化代理转换回外围类的实例。

这个readResolve方法只使用其公共API创建了一个外围类的实例,这就是该模式的美妙之处。它在很大程度上消除了序列化的语言外特性,因为反序列化实例是使用与任何其他实例相同的构造方法、静态工厂和方法创建的。这使你不必单独确保反序列化的实例遵从类的不变量。如果类的静态工厂或构造方法确立了这些不变性,而它的实例方法维护它们,那么就确保了这些不变性也将通过序列化来维护。

以下是Period.SerializationProxyreadResolve方法:

// readResolve method for Period.SerializationProxy
private Object readResolve() {
    return new Period(start, end);    // Uses public constructor
}

与防御性拷贝方法(第357页)一样,序列化代理方法可以阻止伪造的字节流攻击(条目 88,第354页)和内部属性盗用攻击(条目 88, 第356页)。 与前两种方法不同,这一方法允许Period类的属性为final,这是Period类成为真正不可变所必需的(条目 17)。 与之前的两种方法不同,这个方法并没有涉及很多想法。 不你必弄清楚哪些属性可能会被狡猾的序列化攻击所破坏,也不必显示地进行有效性检查,作为反序列化的一部分。

还有另一种方法,序列化代理模式比readObject中的防御性拷贝更为强大。 序列化代理模式允许反序列化实例具有与最初序列化实例不同的类。 你可能认为这在实践中没有有用,但并非如此。

考虑EnumSet类的情况(条目 36)。 这个类没有公共构造方法,只有静态工厂。 从客户端的角度来看,它们返回EnumSet实例,但在当前的OpenJDK实现中,它们返回两个子类中的一个,具体取决于底层枚举类型的大小。 如果底层枚举类型包含64个或更少的元素,则静态工厂返回RegularEnumSet; 否则,他们返回一个JumboEnumSet

现在考虑,如果你序列化一个枚举集合,集合枚举类型有60个元素,然后将五个元素添加到这个枚举类型,再反序列化枚举集合。序列化时,这是一个RegularEnumSet实例,但一旦反序列化,最好是JumboEnumSet实例。事实上正是这样,因为EnumSet使用序列化代理模式。如果好奇,如下是EnumSet的序列化代理。其实很简单:

// EnumSet"s serialization proxy
private static class SerializationProxy <E extends Enum<E>>
        implements Serializable {
    // The element type of this enum set.
    private final Class<E> elementType;

    // The elements contained in this enum set.
    private final Enum<?>[] elements;

    SerializationProxy(EnumSet<E> set) {
        elementType = set.elementType;
        elements = set.toArray(new Enum<?>[0]);
    }

    private Object readResolve() {
        EnumSet<E> result = EnumSet.noneOf(elementType);
        for (Enum<?> e : elements)
            result.add((E)e);
        return result;
    }

    private static final long serialVersionUID =
        362491234563181265L;
}

序列化代理模式有两个限制。它与用户可扩展的类不兼容(条目 19)。而且,它与一些对象图包含循环的类不兼容:如果试图从对象的序列化代理的readResolve方法中调用对象上的方法,得到一个ClassCastException异常,因为你还没有对象,只有该对象的序列化代理。

最后,序列化代理模式增强的功能和安全性并不是免费的。 在我的机器上,使用序列化代理序列化和反序列化Period实例,比使用防御性拷贝多出14%的昂贵开销。

总之,只要发现自己必须在不能由客户端扩展的类上编写readObject或writeObject方法时,请考虑序列化代理模式。 使用重要不变性来健壮序列化对象时,这种模式可能是最简单方法。