从《Effective Java》中进行防御性复制

20

我正在阅读 Joshua Bloch 的 "Effective Java",第39条建议进行防御性拷贝,我有一些问题。我经常使用以下结构:

MyObject.getSomeRef().setSomething(somevalue);

这个缩写代表:

SomeRef s = MyClass.getSomeRef();
s.setSomething();
MyObject.setSomeRef(s);

它总是有效的,但我猜如果我的getSomeRef()返回一个副本,那么我的快捷方式将不起作用,如何知道是否隐藏了MyObject的实现以确定是否可以安全使用快捷方式?

5个回答

29

你正在违反面向对象编程的两个规则:

  • 不要和陌生人交谈
  • 封装性

请注意,这些规则只是规则,有时可以甚至必须打破这些规则。

但是,如果某些数据由对象拥有,并且该对象应该保证其拥有的对象具有某些不变量,则不应将其可变的内部数据结构暴露给外部。因此需要进行防御性复制。

另一个经常使用的习惯用法是返回可变数据结构的不可修改视图:

public List<Foo> getFoos() {
    return Collections.unmodifiableList(this.foos);
}

这个惯用语或者说是防御性复制惯用语非常重要,例如如果你必须确保每次对列表的修改都经过对象:

public void addFoo(Foo foo) {
    this.foos.add(foo);
    someListener.fooAsBeenAdded(foo);
}

如果您不进行防御性拷贝或返回一个不可修改的列表视图,调用者可以直接向列表中添加一个foo,而监听器将不会被调用。


但是如果您像此示例中所做的那样实现getFoos(),那么调用者如何向集合添加元素呢? - Inquisitive
2
通过调用 addFoo() 方法。 - JB Nizet

11

文档是您应该了解的方式。 MyObject 应记录其公开的内容是否可以或应该用于修改 MyObject 本身。 您只应按类明确授权的方法修改对象。

例如,这里是 List 中两个方法的 Javadocs,其中一个方法的结果不能用于更改 List,而另一个方法的结果可以更改 List

toArray():

返回的数组将是“安全的”,因为此列表未保留对其的任何引用。 (换句话说,即使此列表由数组支持,此方法也必须分配新数组)。因此,调用者可以自由地修改返回的数组。

subList():

返回的列表由此列表支持,因此返回列表中的非结构性更改反映在此列表中,反之亦然。 返回的列表支持此列表支持的所有可选列表操作。

我认为,文档中的沉默意味着您不应使用它来改变对象(仅将其用于只读目的)。


3
防御性拷贝是一个不错的主意,但你需要理解在什么情况下以及何时使用它。如果你正在操作的对象网络是内部的,并且它不打算支持多线程,那么防御性拷贝就被误用了。
另一方面,如果这是一个将公开的对象网络,那么你有违反迪米特法则的风险。如果是这样,请考虑在myObject上公开一个操纵器API。
顺便说一下,你的代码示例让getSomeRef看起来像是一个静态API。我建议你为任何返回某个单例副本的静态API命名(例如copyOfSomething())。同样适用于静态工厂方法。

1
调用getSomeRef()两次并比较它们的引用,如果它们不同,则函数返回副本,否则返回相同的实例。
if(MyObject.getSomeRef() == MyObject.getSomeRef()){
     // same instance
}else{
     // copied instance
}

1
虽然在逻辑上是正确的,但这并没有帮助@Loner。他基本上问的是他是否应该避免使用简写形式。这个解决方案涉及到比简写和展开形式结合起来更多的代码。 - Dilum Ranatunga
@DilumRanatunga 太棒了。我只是假设他需要找出函数是否返回相同的实例。我不知怎么忽略了主要问题。我从这个错误中学到了更多,比回答其他问题还要多。为此点赞。 - JProgrammer

1
我建议定义一个readableThing接口或类,并从中派生mutableThingimmutableThing接口。属性getter应该基于返回项与列表的关系返回其中之一的接口:
  1. 如果可以安全地修改事物以使更改存储到底层列表中,则应返回mutableThing。
  2. 如果对象的接收者不能使用它来修改集合,但有可能将来的集合操作会影响该对象,则应返回readableThing。
  3. 如果可以保证所讨论的对象永远不会改变,则应返回immutableThing。
  4. 如果方法的预期结果是调用方具有可变的事物,该事物使用来自集合的数据进行初始化,但未连接到集合,则建议编写接受可变事物并适当设置其字段的方法。请注意,这种用法将清楚地向任何阅读代码的人表明该对象未连接到集合。您还可以使用helper GetMutableCopyOfThing方法。

很遗憾,Java本身并没有更好地声明谁“拥有”各种对象的能力。在垃圾回收框架出现之前,必须跟踪所有对象的所有者,无论它们是否可变,这是非常烦人的。由于不可变对象通常没有自然所有者,因此跟踪不可变对象的所有权是一个主要的麻烦。然而,通常情况下,任何具有可变状态的对象Foo都应该有一个确切的所有者,该所有者将Foo的可变方面视为其自身状态的一部分。例如,ArrayList是保存列表项的数组的所有者。如果不跟踪谁拥有它们(或至少它们的可变方面),那么使用可变对象编写无错误程序的可能性很小。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接