这是一个不太简单的问题。基本上,您需要考虑通过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
并且您为
Map
本身提供了一个 getter 时。如果
Map
是对象的内部状态的一部分,并且该对象希望完全管理
Map
的内容,则永远不要让
Map
出去。如果您必须为地图提供一个 getter,则应返回
Collections.unmodifiableMap(myMap)
而不是
myMap
。在这里,由于潜在成本,您可能不想进行克隆或防御性拷贝。通过返回包装后无法修改的 Map,您可以保护内部状态不被另一个类修改。
由于许多原因,
clone()
通常不是正确的解决方案。一些更好的解决方案是:
- 对于getter:
- 不要返回Map,而是返回只针对keySet或Map.Entry或允许客户端代码执行所需操作的东西的迭代器。换句话说,返回的内容应该是你内部状态的只读视图;或
- 返回可变状态对象,但用类似于Collections.unmodifiableMap()的不可变包装进行封装。
- 提供一个get方法,它接受一个键并从地图中返回相应的值,而不是返回Map。如果所有客户端都要做的就是从Map中获取值,那么不要将Map本身放给客户端;相反,提供一个包装Map的get方法。
- 对于构造函数:
- 在对象构造函数中使用复制构造函数来复制任何可变的传入对象。
- 尽可能使用不可变数量作为构造函数参数,而不是可变数量。例如,有时候使用
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());
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
(并发修改异常)在迭代集合时是常见的结果。
您应该进行防御性编码吗?如果您可以保证同一小组专业程序员始终是撰写和维护项目的人,他们将持续工作以保留项目细节的记忆,相同的人将在项目的生命周期内工作,并且该项目永远不会变得“大”,那么也许您可以不采取防御性措施。但是,除了极少数情况外,防御性编程的成本并不高--而且好处是巨大的。此外:防御性编码是一个好习惯。您不希望鼓励传递可变数据的不良习惯。这样做总有一天会对您造成困扰。当然,所有这些都取决于您的项目所需的正常运行时间。