高效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以上的版本。

50. 必要时进行防御性拷贝

愉快使用Java的原因,它是一种安全的语言(safe language)。 这意味着在缺少本地方法(native methods)的情况下,它不受缓冲区溢出,数组溢出,野指针以及其他困扰C和C ++等不安全语言的内存损坏错误的影响。 在一种安全的语言中,无论系统的任何其他部分发生什么,都可以编写类并确切地知道它们的不变量会保持不变。 在将所有内存视为一个巨大数组的语言中,这是不可能的。

即使在一种安全的语言中,如果不付出一些努力,也不会与其他类隔离。必须防御性地编写程序,假定类的客户端尽力摧毁类其不变量。随着人们更加努力地试图破坏系统的安全性,这种情况变得越来越真实,但更常见的是,你的类将不得不处理由于善意得程序员诚实错误而导致的意外行为。不管怎样,花时间编写在客户端行为不佳的情况下仍然保持健壮的类是值得的。

如果没有对象的帮助,另一个类是不可能修改对象的内部状态的,但是在无意的情况下提供这样的帮助却非常地容易。例如,考虑以下类,表示一个不可变的时间期间:

// Broken "immutable" time period class

public final class Period {

    private final Date start;

    private final Date end;



    /**

     * @param  start the beginning of the period

     * @param  end the end of the period; must not precede start

     * @throws IllegalArgumentException if start is after end

     * @throws NullPointerException if start or end is null

     */

    public Period(Date start, Date end) {

        if (start.compareTo(end) > 0)

            throw new IllegalArgumentException(

                start + " after " + end);

        this.start = start;

        this.end   = end;

    }

    public Date start() {

        return start;

    }

    public Date end() {

        return end;

    }

    ...    // Remainder omitted

}

乍一看,这个类似乎是不可变的,并强制执行不变式,即period实例的开始时间并不在结束时间之后。然而,利用Date类是可变的这一事实很容易违反这个不变式:

// Attack the internals of a Period instance

Date start = new Date();

Date end = new Date();

Period p = new Period(start, end);

end.setYear(78);  // Modifies internals of p!

从Java 8开始,解决此问题的显而易见的方法是使用Instant(或LocalDateTimeZonedDateTime)代替Date,因为Instant和其他java.time包下的类是不可变的(条目17)。Date已过时,不应再在新代码中使用。 也就是说,问题仍然存在:有时必须在API和内部表示中使用可变值类型,本条目中讨论的技术也适用于这些时间。

为了保护Period实例的内部不受这种攻击,必须将每个可变参数的防御性拷贝应用到构造方法中,并将拷贝用作Period实例的组件,以替代原始实例:

// Repaired constructor - makes defensive copies of parameters

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(

          this.start + " after " + this.end);

}

有了新的构造方法后,前面的攻击将不会对Period实例产生影响。注意,防御性拷贝是在检查参数(条目49)的有效性之前进行的,有效性检查是在拷贝上而不是在原始实例上进行的。虽然这看起来不自然,但却是必要的。它在检查参数和拷贝参数之间的漏洞窗口期间保护类不受其他线程对参数的更改的影响。在计算机安全社区中,这称为 time-of-check/time-of-use或TOCTOU攻击[Viega01]。

还请注意,我们没有使用Date的clone方法来创建防御性拷贝。因为Date是非final的,所以clone方法不能保证返回类为java.util.Date的对象,它可以返回一个不受信任的子类的实例,这个子类是专门为恶意破坏而设计的。例如,这样的子类可以在创建时在私有静态列表中记录对每个实例的引用,并允许攻击者访问该列表。这将使攻击者可以自由控制所有实例。为了防止这类攻击,不要使用clone方法对其类型可由不可信任子类化的参数进行防御性拷贝。

虽然替换构造方法成功地抵御了先前的攻击,但是仍然可以对Period实例进行修改,因为它的访问器提供了对其可变内部结构的访问:

// Second attack on the internals of a Period instance

Date start = new Date();

Date end = new Date();

Period p = new Period(start, end);

p.end().setYear(78);  // Modifies internals of p!

为了抵御第二次攻击,只需修改访问器以返回可变内部字属性的防御性拷贝:

// Repaired accessors - make defensive copies of internal fields

public Date start() {

    return new Date(start.getTime());

}

public Date end() {

    return new Date(end.getTime());

}

使用新的构造方法和新的访问器,Period是真正不可变的。 无论程序员多么恶意或不称职,根本没有办法违反一个period实例的开头不跟随其结束的不变量(不使用诸如本地方法和反射之类的语言外方法)。 这是正确的,因为除了period本身之外的任何类都无法访问period实例中的任何可变属性。 这些属性真正封装在对象中。

在访问器中,与构造方法不同,允许使用clone方法来制作防御性拷贝。 这是因为我们知道Period的内部Date对象的类是java.util.Date,而不是一些不受信任的子类。 也就是说,由于条目13中列出的原因,通常最好使用构造方法或静态工厂来拷贝实例。

参数的防御性拷贝不仅仅适用于不可变类。 每次编写在内部数据结构中存储对客户端提供的对象的引用的方法或构造函数时,请考虑客户端提供的对象是否可能是可变的。 如果是,请考虑在将对象输入数据结构后,你的类是否可以容忍对象的更改。 如果答案是否定的,则必须防御性地拷贝对象,并将拷贝输入到数据结构中,以替代原始数据结构。 例如,如果你正在考虑使用客户端提供的对象引用作为内部set实例中的元素或作为内部map实例中的键,您应该意识到如果对象被修改后插入,对象的set或map的不变量将被破坏。

在将内部组件返回给客户端之前进行防御性拷贝也是如此。无论你的类是否是不可变的,在返回对可拜年的内部组件的引用之前,都应该三思。可能的情况是,应该返回一个防御性拷贝。记住,非零长度数组总是可变的。因此,在将内部数组返回给客户端之前,应该始终对其进行防御性拷贝。或者,可以返回数组的不可变视图。这两项技术都记载于条目15。

可以说,所有这些的真正教训是,在可能的情况下,应该使用不可变对象作为对象的组件,这样就不必担心防御性拷贝(条目17)。在我们的Period示例中,使用Instant(或LocalDateTime或ZonedDateTime),除非使用的是Java 8之前的版本。如果使用的是较早的版本,则一个选项是存储Date.getTime()返回的基本类型long来代替Date引用。

可能存在与防御性拷贝相关的性能损失,并且它并不总是合理的。如果一个类信任它的调用者不修改内部组件,也许是因为这个类和它的客户端都是同一个包的一部分,那么它可能不需要防御性的拷贝。在这些情况下,类文档应该明确指出调用者不能修改受影响的参数或返回值。

即使跨越包边界,在将可变参数集成到对象之前对其进行防御性拷贝也并不总是合适的。有些方法和构造方法的调用指示参数引用的对象的显式切换。当调用这样的方法时,客户端承诺不再直接修改对象。希望获得客户端提供的可变对象的所有权的方法或构造方法必须在其文档中明确说明这一点。

包含方法或构造方法的类,这些方法或构造方法的调用指示控制权的转移,这些类无法防御恶意客户端。 只有当一个类和它的客户之间存在相互信任,或者当对类的不变量造成损害时,除了客户之外,任何人都不会受到损害。 后一种情况的一个例子是包装类模式(第18项)。 根据包装类的性质,客户端可以通过在包装后直接访问对象来破坏类的不变性,但这通常只会损害客户端。

总之,如果一个类有从它的客户端获取或返回的可变组件,那么这个类必须防御性地拷贝这些组件。如果拷贝的成本太高,并且类信任它的客户端不会不适当地修改组件,则可以用文档替换防御性拷贝,该文档概述了客户端不得修改受影响组件的责任。