这是一个不平凡的问题。基本上,您必须考虑通过 getter 或调用另一个类的 setter 给任何其他类的类的任何内部状态。例如,如果您这样做:
Date now = new Date();
someObject.setDate(now);
// another use of "now" that expects its value to not have changed
那么你可能有两个问题:
someObject
可能会改变 " now
" 的值,这意味着上面的方法在以后使用该变量时可能具有与预期不同的值,并且
- 如果在将 "
now
" 传递给someObject
您之后更改了它的值,并且如果someObject
没有进行防御性副本,那么您已经更改了someObject
.
您应该避免这两种情况,或者您应该记录您对允许或禁止什么的期望,具体取决于代码的客户是谁。另一种情况是当一个类有一个Map
并且你为它自己提供一个getter Map
。如果Map
是对象内部状态的一部分,并且对象希望完全管理 的内容Map
,那么你永远不应该泄露Map
出去。如果您必须为地图提供 getter,则返回Collections.unmodifiableMap(myMap)
而不是myMap
. 由于潜在的成本,您可能不想在此处制作克隆或防御性副本。通过返回您的 Map 以使其无法修改,您可以保护您的内部状态不被另一个类修改。
由于许多原因,clone()
往往不是正确的解决方案。一些更好的解决方案是:
- 对于吸气剂:
- 与其返回 a
Map
,不如仅Iterator
将 s 返回到 thekeySet
或 theMap.Entry
或任何允许客户端代码执行其需要执行的操作。换句话说,返回本质上是您内部状态的只读视图的内容,或者
- 返回包装在不可变包装器中的可变状态对象,类似于
Collections.unmodifiableMap()
- 与其返回 a ,不如
Map
提供一个get
方法,该方法接受一个键并从映射中返回相应的值。如果所有客户都想Map
从中获取价值,那么不要给客户Map
本身;相反,提供一个包装Map
'get()
方法的 getter。
- 对于构造函数:
- 在对象构造函数中使用复制构造函数来复制传入的任何可变内容。
- 尽可能设计为将不可变数量作为构造函数参数,而不是可变数量。例如,有时将 long 返回的
new Date().getTime()
值比Date
对象更有意义。
- 尽可能多地利用您的状态
final
,但请记住,final
对象仍然可以是可变的,并且final
数组仍然可以修改。
在所有情况下,如果有关于谁“拥有”可变状态的问题,请将其记录在 getter 或 setter 或构造函数上。在某处记录它。
这是一个糟糕代码的简单示例:
import java.util.Date;
public class Test {
public static void main(String[] args) {
Date now = new Date();
Thread t1 = new Thread(new MyRunnable(now, 500));
t1.start();
try { Thread.sleep(250); } catch (InterruptedException e) { }
now.setTime(new Date().getTime()); // BAD! Mutating our Date!
Thread t2 = new Thread(new MyRunnable(now, 500));
t2.start();
}
static public class MyRunnable implements Runnable {
private final Date date;
private final int count;
public MyRunnable(final Date date, final int count) {
this.date = date;
this.count = count;
}
public void run() {
try { Thread.sleep(count); } catch (InterruptedException e) { }
long time = new Date().getTime() - date.getTime();
System.out.println("Runtime = " + time);
}
}
}
您应该看到每个可运行文件都休眠了 500 毫秒,但是您得到了错误的时间信息。如果您更改构造函数以制作防御性副本:
public MyRunnable(final Date date, final int count) {
this.date = new Date(date.getTime());
this.count = count;
}
然后你得到正确的时间信息。这是一个简单的例子。您不想调试复杂的示例。
注意:未能正确管理状态的常见ConcurrentModificationException
结果是迭代集合时。
你应该防御性地编码吗?如果你能保证同样的专家程序员团队永远是编写和维护你的项目的人,他们会不断地工作,这样他们就可以记住项目的细节,同样的人会为它工作项目的生命周期,并且项目永远不会变得“大”,那么也许你可以不这样做。但是除了极少数情况外,防御性编程的成本并不高——而且收益很大。另外:防御性编码是一个好习惯。您不想鼓励养成将可变数据传递到不应该有它的地方的坏习惯。这将有一天咬你。当然,所有这些都取决于项目所需的正常运行时间。