Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,因此JDK 最好下载 JDK 9以上的版本。java
愉快使用Java的缘由,它是一种安全的语言(safe language)。 这意味着在缺乏本地方法(native methods)的状况下,它不受缓冲区溢出,数组溢出,野指针以及其余困扰C和C ++等不安全语言的内存损坏错误的影响。 在一种安全的语言中,不管系统的任何其余部分发生什么,均可以编写类并确切地知道它们的不变量会保持不变。 在将全部内存视为一个巨大数组的语言中,这是不可能的。git
即便在一种安全的语言中,若是不付出一些努力,也不会与其余类隔离。必须防护性地编写程序,假定类的客户端尽力摧毁类其不变量。随着人们更加努力地试图破坏系统的安全性,这种状况变得愈来愈真实,但更常见的是,你的类将不得不处理因为善意得程序员诚实错误而致使的意外行为。无论怎样,花时间编写在客户端行为不佳的状况下仍然保持健壮的类是值得的。程序员
若是没有对象的帮助,另外一个类是不可能修改对象的内部状态的,可是在无心的状况下提供这样的帮助却很是地容易。例如,考虑如下类,表示一个不可变的时间期间:github
// 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
(或LocalDateTime
或ZonedDateTime
)代替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实例进行修改,由于它的访问器提供了对其可变内部结构的访问:this
// 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项)。 根据包装类的性质,客户端能够经过在包装后直接访问对象来破坏类的不变性,但这一般只会损害客户端。
总之,若是一个类有从它的客户端获取或返回的可变组件,那么这个类必须防护性地拷贝这些组件。若是拷贝的成本过高,而且类信任它的客户端不会不适当地修改组件,则能够用文档替换防护性拷贝,该文档概述了客户端不得修改受影响组件的责任。