Effective Java(3rd)-Item50 在需要的时候制作防御性的副本

  Java是一种安全的语言,这是它的一大优点。这意味着在没有本机方法的情况下,它不受缓冲区溢出、数组溢出、野生指针和其他内存损坏错误的影响,这些错误困扰着C和c++等不安全语言。在一种安全的语言中,可以编写类并确定它们的不变量将保持不变,不管在系统的任何其他部分发生了什么。在将所有内存视为一个巨大数组的语言中,这是不可能的。
  即使使用一种安全的语言,如果您不付出一些努力,也无法与其他类隔离。您必须进行防御性的编程,并假定您的类的客户端会尽最大努力破坏它的不变量。随着人们更加努力地破坏系统的安全性,这一点越来越正确,但更常见的情况是,您的类将不得不处理由善意程序员的诚实错误所导致的意外行为。无论哪种方式,都值得花时间编写面对行为不端的客户端的健壮类。
  虽然如果没有对象的帮助,另一个类是不可能修改对象的内部状态的,但是提供这样的帮助却出奇地容易。例如,考虑下面的类,它声称表示一个不可变的时间段:

Effective Java(3rd)-Item50 在需要的时候制作防御性的副本_第1张图片
image.png

Effective Java(3rd)-Item50 在需要的时候制作防御性的副本_第2张图片
image.png

  乍一看,这个类似乎是不可变的,并且强制一个周期的开始不跟随它的结束。然而,利用日期是可变的这一事实很容易违反这个不变量:


Effective Java(3rd)-Item50 在需要的时候制作防御性的副本_第3张图片
image.png

  从Java 8开始,解决这个问题的明显方法就是使用Instant(或LocalDateTime或ZonedDateTime)代替Date,因为Instant(和其他Java.time 类)是不可变的( item17 )。Date已过时,不应在新代码中使用。尽管如此,问题仍然存在:有时您必须在api和内部表示中使用可变值类型,本项目中讨论的技术适用于这些时候。

  为了保护Period实例的内部不受这种攻击,必须将每个可变参数的防御性副本复制到构造函数并使用副本代替正本,作为期间实例的组成部分:


Effective Java(3rd)-Item50 在需要的时候制作防御性的副本_第4张图片
image.png

   有了新的构造函数,之前的攻击将不会对Period实例产生影响。注意到防御副本在检查参数有效性(item49)前制作,对副本而不是原件进行有效性检查。虽然这看起来不自然,但却是必要的。它保护类不受其他线程参数更改的影响在漏洞窗口期间,从检查参数到复制参数的时间间隔。在计算机安全社区,这被称为检查时间/使用时间或TOCTOU攻击[Viega01].

  还要注意,我们没有使用Date的clone方法来创建防御性副本。因为Date是非final的,所以不能保证克隆方法返回一个类为java.util.Date的对象:它可以返回一个不受信任子类的实例,这个子类是专门为恶意破坏而设计的。例如,这样的子类可以在创建时在私有静态列表中记录对每个实例的引用,并允许攻击者访问这个列表。这将使攻击者可以自由控制所有实例。为了防止这种攻击,不要使用克隆方法对类型可由不受信任方子类化的参数进行防御性复制。
  虽然替换构造函数成功地防御了之前的攻击,但是仍然可以修改Period实例,因为它的访问器提供了对其可变内部结构的访问:

Effective Java(3rd)-Item50 在需要的时候制作防御性的副本_第5张图片
image.png

  要防御第二次攻击,只需修改访问器,返回可变内部字段的防御副本:

Effective Java(3rd)-Item50 在需要的时候制作防御性的副本_第6张图片
image.png

  有了新的构造函数和新的访问器,Period实际上是不可变的。无论程序员多么恶意或无能,都不可能违背period 的开始和结束不一致这一不变式(不借助语言以外的手段,如native方法和反射).这是真的,因为除了Period本身之外,任何类都无法访问Period实例中的任何可变字段。这些字段真正封装在对象中。
  在访问器中,与构造函数不同,可以使用clone方法进行防御性复制。这是因为我们知道Period的内部Date对象的类是java.util.Date,而不是某个不可信的子类。也就是说,出于第13项中列出的原因,通常最好使用构造函数或静态工厂来复制实例。

  参数的防御性复制不仅适用于不可变类。在编写方法或构造函数时,如果要在内部数据结构中存储对客户机提供的对象的引用,请考虑客户机提供的对象是否可能是可变的。如果是,请考虑在对象进入数据结构之后,您的类是否能够容忍对象中的更改。如果答案是否定的,则必须防御性地复制对象,并将副本输入到数据结构中,而不是原始结构中。举个例子,如果你正在考虑使用一个对象引用作为一个具备此元素在内部设置实例或作为一个关键的内部地图实例,您应该意识到的不变量设置或地图会损坏如果对象被修改后插入。
  在将内部组件返回给客户端之前对其进行防御性复制也是如此。无论您的类是否是不可变的,在返回对可变内部组件的引用之前,您都应该三思。很有可能,您应该返回一个防御性副本。记住,非零长度数组总是可变的。因此,在将内部数组返回给客户机之前,应该始终创建一个防御性的副本。或者,您可以返回数组的不可变视图。这两种技术都显示在项目15中。
  可以说,所有这些的真正教训是,在可能的情况下,应该使用不可变对象作为对象的组件,这样就不必担心防御性复制(item17)。在我们的Period示例中,使用Instant(或LocalDateTime或ZonedDateTime),除非您使用的是Java 8之前的版本。如果使用较早的版本,一个选项是存储Date. gettime()返回的long原语,而不是Date引用。

  可以说,所有这些的真正教训是,在可能的情况下,应该使用不可变对象作为对象的组件,这样就不必担心防御性复制(item17)。在我们的Period示例中,使用Instant(或LocalDateTime或ZonedDateTime),除非您使用的是Java 8之前的版本。如果使用较早的版本,一个选项是存储Date. gettime()返回的long原语,而不是Date引用。如果一个类信任它的调用者不修改内部组件,可能是因为类和它的客户端都是同一个包的一部分,那么就应该避免防御性复制。在这种情况下,类文档应该表明调用者不能修改受影响的参数或返回值。

  即使跨越包边界,在将可变参数集成到对象之前对其进行防御性复制也并不总是合适的。有一些方法和构造函数,它们的调用指示参数引用的对象的显式切换。当调用这样一个方法时,客户端承诺不再直接修改对象。希望拥有客户机提供的可变对象所有权的方法或构造函数必须在其文档中明确说明这一点。
  包含方法或构造函数的类,这些方法或构造函数的调用指示控制权的转移,不能保护自己免受恶意客户机的攻击。只有当一个类和它的客户机之间存在相互信任,或者对类的不变量的破坏只会对客户机造成伤害时,这样的类才是可接受的。后一种情况的一个例子是包装器类模式(item18 )。根据包装器类的性质,客户机可以在包装对象之后直接访问对象,从而破坏类的不变量,但这通常只会损害客户机。

  总而言之,如果一个类具有从客户端获取或返回给客户端的可变组件,则该类必须防御性地复制这些组件。如果复制的成本过高,并且类信任它的客户端不会不适当地修改组件,那么防御性的复制可能被概述客户端不修改受影响组件的责任的文档所取代。
本文写于2019.7.17,历时1天

你可能感兴趣的:(Effective Java(3rd)-Item50 在需要的时候制作防御性的副本)