第50条:在必要时进行防御性复制
内容提要
Java编程需要防御性编程以防止客户端违反类不变性。在参数验证之前应进行防御性复制以避免漏洞。Getter方法应返回可变对象的防御性副本以防止外部变更。防御性复制也适用于存储客户端提供的可变对象引用的可变类。在返回可变内部组件时,考虑返回防御性副本或不可变视图。尽可能使用不可变对象作为内部组件以避免需要防御性复制。防御性复制可能影响性能,因此可以考虑依赖文档或明确的使用合同等替代方案。使用防御性复制来保护类的完整性,除非成本不切实际或通过明确的文档建立了相互信任。
延伸解读
防御性复制与参数验证的顺序
文章强调,防御性复制应在参数验证之前进行。如果先验证后复制,攻击者可能在验证通过后修改传入的可变对象,导致类的不变性被破坏(TOCTOU攻击)。因此,在构造器中接收可变参数时,应首先创建防御性副本,再基于副本进行验证。这一顺序是确保类安全的关键细节。
Getter方法返回防御性副本的必要性
如果Getter直接返回内部可变对象,客户端可以修改该对象,从而破坏类的封装性和不变性。文章建议Getter应返回可变对象的防御性副本,例如返回new Date(start.getTime())而非直接返回start。这样外部无法通过返回的引用修改内部状态,保护了类的完整性。
防御性复制的性能权衡与替代方案
防御性复制可能带来性能开销,尤其是在频繁调用或大对象场景下。文章指出,可以考虑依赖文档或明确的使用合同来建立信任,从而避免复制。例如,在明确转移控制权(如wrapper模式)时,可以省略防御性复制。但需注意,这种替代方案要求清晰的文档和相互信任,否则可能引入安全风险。
优先使用不可变对象作为内部组件
文章建议尽可能使用不可变对象作为内部组件,这样就不需要防御性复制。因为不可变对象的状态无法被修改,天然避免了外部变更的风险。例如,使用不可变的Date替代品(如java.time中的类)可以简化代码并提升安全性。这一策略从设计层面减少了防御性复制的需求。
Q&A
什么是防御性编程,它在Java编程中有什么重要性?
防御性编程是指在编程时假设客户端可能会破坏类的不变性,确保类的完整性,防止潜在的漏洞。
在Java中,如何进行防御性复制以避免漏洞?
在参数验证之前进行防御性复制,以确保接收到的可变对象不会被外部修改,从而避免漏洞。
Getter方法应该如何处理可变对象?
Getter方法应返回可变对象的防御性副本,以防止外部对内部状态的修改。
使用不可变对象作为内部组件有什么好处?
使用不可变对象可以避免需要防御性复制,从而提高代码的安全性和性能。
防御性复制可能带来哪些性能影响?
防御性复制可能会影响性能,因此在某些情况下可以考虑依赖文档或明确的使用合同作为替代方案。
在什么情况下可以不使用防御性复制?
当成本不切实际或通过明确的文档建立了相互信任时,可以不使用防御性复制。