在令人惊讶的书“Java the Good Parts”中,作者给出了一些代码,这些代码在其getter方法中返回一个对象的副本(与一个封装良好的字段一起使用)然后声明:
此方法尝试返回私有数据的副本 而不是对私人数据的引用,通常是一个好主意
为什么会这样?我认为封装的目标是确保没有人能够真正改变私人成员。那我为什么要写这样的东西
private someType fieldName = new someType();
...
同时定义它的getter(假设存在某种复制构造函数)
someType getSomething()
{
return new someType(fieldName);
}
根据我现在所知,我的意思是在你们弹出之前是:
到目前为止,这是有意义的,它服务垃圾收集,因为这种方法不维护对实际对象的引用。从内部类的角度来看也是可以理解的,任何方法都可以通过引用来改变任何可访问的字段。
但我并不怀疑这样做的两个原因是什么才真正超越了这个问题。
答案 0 :(得分:19)
当类型可变时,常常对返回副本很有用,这样客户端就无法从根本上修改数据,至少在没有告诉你的情况下。考虑:
public class Person {
private Date dateOfBirth;
public Date getDateOfBirth() {
return dateOfBirth;
}
public void setDateOfBirth(Date dateOfBirth) {
// Do some validation, e.g. that it's after 1800
this.dateOfBirth = dateOfBirth;
}
}
看起来没问题吧?但是怎么样:
Person person = new Person();
person.setDateOfBirth(new Date()); // Now... fine.
// Aha! Modify the Date to a very long time ago. Now anyone else
// using the Person will be messed up...
person.getDateOfBirth().setTime(Long.MIN_VALUE);
如果getDateOfBirth
返回副本,则调用者对返回值引用的Date
对象所做的任何更改都将与其他任何人无关。 Person
对象仍然有效,因为它只有一个有效的日期。当然,这应该记录下来,以便编写上述代码的人期望它不会影响Person
对象,因为返回了一个副本。
更好的解决方案比所有这些复制都支持不可变类型,但是当你有一个不可变对象的引用时,你可以随意分享它,知道没有人可以改变你脚下的状态。
答案 1 :(得分:7)
我们的想法是,getter允许您查看对象的状态而无法对其进行修改(因为您将修改副本而不是原始文件)。
如果你打电话:
someType property = someObj.getSomething();
然后
property.setSomeSubProperty(someValue);
这只会更改someType
的副本,而不会更改someType
中存储的原始someObj
。
如果包含getSomething()
方法的类是可变的,则它可能具有setSomething(someType value)
方法,并且使用该方法将是修改该属性的可接受方法。
答案 2 :(得分:2)
已经提出了很好的答案,但让我想出其他的例子和参考资料
解释的最佳来源是Josh Bloch的 Effective Java 。关于不变性和防御性副本至少有两章。
简而言之: 在Java中,您通过引用传递所有内容(我知道它过于简单,但这不是重点)并且许多类都是可变的。因此,私有字段与外部对象的直接分配并不十分安全,因为下面的值可以在任何时间点从对象外部改变,从而打破封装。
访问器方法是打破封装的本质。在最常见的实现中,您只需将该字段设置为public,如上所述,您允许任何人更改底层对象(如果允许的话)。最好的例子是集合恕我直言。如果您返回任何默认Java集合,任何人都可以添加内容,删除元素甚至清除它。如果你的逻辑取决于状态,或者你正在编写多线程应用程序,那么这是获得竞争条件的最简单方法,这是我们真正不想要的。
所以好的做法是
每个人都有一些与他们相关的费用。复制/克隆需要时间和内存。视图不是完全安全的,因为底层实现可能在任何时间点发生变化,不可变对象不允许修改,并且难以在遗留系统中实现等等。由您来寻找平衡,但我们总是很乐意提供帮助: )
最后一点,在传递可变参数的构造函数/ setter中执行防御性复制也是一种很好的做法,原因完全相同。如果有人将元素添加到我们在构造函数中最终得到的集合中,那将是非常愚蠢的,因为我们不保留状态,这是我们显然想要的。所以,再次,在构造函数中,如果你没有控制传入的内容,就不要做一个简单的初始化(即使你这样做,也可能是复制的好主意)
我更喜欢集合作为一个例子,因为它们更容易推理如何复制/如何更改,但是在其他答案中提到的StringBuilder和Date,确实表明它不仅仅是集合的问题。所以最好的答案是:记住,final是你最好的朋友。从一开始就经常使用它,永远不要相信可变的陌生人!
答案 3 :(得分:0)
由于没有“私人数据”这样的东西,实际上只有您无法访问的数据和无法更改的数据。
假设fieldName被定义为StringBuilder fieldName。您无法对StringBuilder做任何事情,以防止有权访问它的人修改它。另一方面,如果它被定义为字符串fieldName,那么(没有一些真正邪恶的反映)就没有其他人改变它的可能性。
因此,它是不可变性的昂贵替代品。更好的是使用仅允许访问您想要提供的属性和操作的包装器。