ITEM 50: MAKE DEFENSIVE COPIES WHEN NEEDED
Java是一种安全的语言,这意味着在没有本机方法的情况下,它不受缓冲区溢出、数组溢出、野指针和其他内存损坏错误的影响,而这些错误困扰着像 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
}
乍一看,这个类似乎是不可变的,并且强制一个周期的开始不跟随它的结束。然而,利用日期是可变的这一事实很容易违反这个不变量:
// 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(或 Local-DateTime 或 ZonedDateTime)来代替日期,因为 Instant(和 java.time 类)是不可变的(item 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 实例没有影响。注意,防御副本是在检查参数的有效性之前制作的(item 49),并且有效性检查是在副本上而不是在原件上执行的。虽然这看起来不自然,但却是必要的。在检查参数和复制参数之间的漏洞窗口期间,它保护类不受其他线程对参数的更改。在计算机安全社区,这被称为检查时间/使用时间或 TOCTOU 攻击[Viega01]。
还请注意,我们没有使用 Date 的 clone 方法来创建防御性副本。因为 Date 是非 final 的,所以不能保证克隆方法返回一个 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 实际上是不可变的。无论一个程序员多么恶毒或无能,都不可能违背这样一个不变条件:一个周期的开始不等于结束(除非求助于诸如 native 方法和反射之类的外部语言手段)。这是真的,因为除了 Period 本身之外,任何类都无法访问 Period 实例中的任何可变字段。这些字段真正封装在对象中。
与构造函数不同,在访问器中允许使用克隆方法来创建防御副本。这是因为我们知道 Period 的内部 Date 对象的类是java.util.Date,而不是某个不可信的子类。也就是说,出于第13项中列出的原因,通常最好使用构造函数或静态工厂来复制实例。
参数的防御性复制并不只针对不可变类。每当您编写一个方法或构造函数,在内部数据结构中存储对客户机提供的对象的引用时,请考虑客户机提供的对象是否可能是可变的。如果是,请考虑在对象被输入数据结构之后,您的类是否能够容忍对象中的更改。如果答案是否定的,则必须防御性地复制对象,并将副本输入到数据结构中,而不是原始结构中。举个例子:如果你正在考虑使用一个对象引用作为一个具备此元素在一个内部 Set 实例或作为一个 key 在内部 Map 实例中,您应该意识到的不变量设置或地图会损坏如果对象被修改后插入。
在将内部组件返回给客户端之前对其进行防御性复制也是如此。无论您的类是否是不可变的,在返回对可变内部组件的引用之前都应该三思。你应该尽可能返回一个防御副本。记住非零长度数组总是可变的。因此,在将内部数组返回给客户机之前,应该始终创建一个防御性的副本。或者,您可以返回数组的不可变视图。这两项技术见 item 15。
可以说,所有这些的真正教训是,在可能的情况下,应该使用不可变对象作为对象的组件,这样就不必担心防御性复制(item 17)。在我们的 Period 示例中,使用 Instant (或 LocalDateTime 或ZonedDateTime),除非您使用的是 Java 8 之前的版本。如果使用较早的版本,一种选择是将Date.gettime() 返回的 long 原语存储在 Date 引用中。
与防御性复制相关的性能损失并不总是合理的。如果一个类相信它的调用者不会修改一个内部组件,可能是因为这个类和它的客户端都是同一个包的一部分,那么就应该避免防御性的复制。在这种情况下,类文档应该表明调用者不能修改受影响的参数或返回值。
即使跨越包边界,在将可变参数集成到对象之前,也不总是适当的。有一些方法和构造函数,它们的调用指示参数引用的对象的显式切换。当调用这样的方法时,客户端承诺不再直接修改对象。希望获得客户端提供的可变对象所有权的方法或构造函数必须在其文档中明确说明这一点。
包含调用指示控制转移的方法或构造函数的类无法防御恶意客户端。只有当一个类和它的客户端之间存在相互信任,或者对类的不变量的破坏只会对客户端造成伤害时,这样的类才是可接受的。后一种情况的一个例子是包装器类模式(item 18)。
根据包装器类的性质,客户机可以在包装后直接访问对象,从而破坏类的不变量,但这通常只会损害客户机。总之,如果一个类有从它的客户端获取或返回给它的可变组件,那么这个类必须防御性地复制这些组件。如果复制的成本过高,并且类信任它的客户端不会不恰当地修改组件,那么防御性的复制可能会被概述客户端不修改受影响组件的责任的文档所取代。
网友评论