Effective Java 第三版——50. 必要时进行防御性拷贝
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(或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实例进行修改,因为它的访问器提供了对其可变内部结构的访问:
// 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项)。 根据包装类的性质,客户端可以通过在包装后直接访问对象来破坏类的不变性,但这通常只会损害客户端。
总之,如果一个类有从它的客户端获取或返回的可变组件,那么这个类必须防御性地拷贝这些组件。如果拷贝的成本太高,并且类信任它的客户端不会不适当地修改组件,则可以用文档替换防御性拷贝,该文档概述了客户端不得修改受影响组件的责任。
Effective Java 第三版——50. 必要时进行防御性拷贝的更多相关文章
- 《Effective Java 第三版》目录汇总
经过反复不断的拖延和坚持,所有条目已经翻译完成,供大家分享学习.时间有限,个别地方翻译得比较仓促,希望有疑虑的地方指出批评改正. 第一章简介 忽略 第二章 创建和销毁对象 1. 考虑使用静态工厂方法替 ...
- 《Effective Java 第三版》新条目介绍
版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...
- Effective Java 第三版——45. 明智审慎地使用Stream
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective Java 第三版——1. 考虑使用静态工厂方法替代构造方法
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective Java 第三版——3. 使用私有构造方法或枚类实现Singleton属性
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective Java 第三版——6. 避免创建不必要的对象
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective Java 第三版——7. 消除过期的对象引用
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective Java 第三版——8. 避免使用Finalizer和Cleaner机制
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective Java 第三版——9. 使用try-with-resources语句替代try-finally语句
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
随机推荐
- jQuery动态设置样式(style、css)
一.jQuery设置css样式 <div style="background-color:#ffffff;padding-left:10px;">测试jQuery动态获 ...
- 011 处理模型数据时@ModelAttribute的使用
一:说明 1.使用场景 在下面的场景中: 使用new对象时,最后的效果是两个被赋值了,但是还有一个值是空值的,这个不符合只更新两个值,另一个值不变的情况. 正确的做法: 三个值得数据不是new出来的, ...
- SqlServer 添加用户 添加角色 分配权限
转载自:https://www.cnblogs.com/accumulater/p/6158387.html --创建一个简单的登录,登录名为:newlogin:登录密码:123456:默认数据库 ...
- 《Gradle权威指南》--Android Gradle高级自定义
No1: 指定共享库 <uses-library android:name="com.google.android.maps" android:required=" ...
- hdu 1002 A + B Problem II【大数加法】
题目链接>>>>>> 题目大意:手动模拟大数加法,从而进行两个大数的加法运算 #include <stdio.h> #include <strin ...
- Shell学习之Shell特性(一)
Shell学习之Shell特性 目录 命令和文件自动补齐功能 命令历史记忆功能 history.上下键.!number.!string.!$.!! 别名功能 alias.unalias cp.~use ...
- Spring Boot 项目实战(二)集成 Logback
一.前言 上篇介绍了 Spring Boot Maven 多模块项目的搭建方法以及 MyBatis 的集成.通常在调试接口或者排查问题时我们主要借助于日志,一个设计合理的日志文件配置能大大降低我们的排 ...
- 在docker中运行mysql实例
Docker是一种新兴的虚拟化技术,能够一定程度上的代替传统虚拟机.下图是容器跟虚拟机的对比 对docker有个大致了解,学习docker断断续续,虽说学习不能急于求成,但断断续续学的话,浪费的碎片化 ...
- 推荐一个spring cloud 学习路线,绝对合理化
最近没有时间所有没用给大家更新spring cloud 系列学习,在这先给大家奉献上我学习spring cloud 的路线 当然第一步先学习springboot然后: spring cloud eur ...
- Easydarwin加FFMPEG实现HLS流视频点播
前言 最近有点迷茫,所以将自己用过的东西写个Demo记录一下,复习复习. 具体实现: Easydarwin 一个开源的好用的流媒体平台框架. FFMPEG 一个视频音频处理神器,就是用起来有点麻烦, ...