Java 返回私有数据的副本而不是引用

Java 返回私有数据的副本而不是引用,java,reference,Java,Reference,在令人震惊的《Java的好部分》一书中,作者给出了一些代码,这些代码在其getter方法中返回一个对象的副本(与封装良好的字段一起使用),然后声明: 此方法尝试返回私有数据的副本 一般来说,这是一个好主意,而不是引用那个私有数据 为什么会这样?我认为封装的目标是确保没有人能够真正改变私有成员。那我为什么要写这样的东西呢 private someType fieldName = new someType(); ... 在这样定义其getter时(假设存在某种复制构造函数) 据我所知,我的意思

在令人震惊的《Java的好部分》一书中,作者给出了一些代码,这些代码在其getter方法中返回一个对象的副本(与封装良好的字段一起使用),然后声明:

此方法尝试返回私有数据的副本 一般来说,这是一个好主意,而不是引用那个私有数据

为什么会这样?我认为封装的目标是确保没有人能够真正改变私有成员。那我为什么要写这样的东西呢

private someType fieldName = new someType(); 

...
在这样定义其getter时(假设存在某种复制构造函数)

据我所知,我的意思是在你们进来之前:

到目前为止,这是有意义的,因为它服务于垃圾收集,因为这种方法不维护对实际对象的引用

从内部类的角度来看,它也是可以理解的,从内部类的角度来看,任何方法都可以更改任何可通过引用访问的字段


但我不怀疑这样做的两个原因是真正超越这个问题的

其思想是getter允许您查看对象的状态,而无需修改它(因为您将修改的是副本,而不是原始副本)

如果你打电话:

someType property = someObj.getSomething();
然后

property.setSomeSubProperty(someValue);
这只会更改
someType
的副本,而不会更改存储在
someObj
中的原始
someType


如果包含
getSomething()
方法的类是可变的,那么它可能有一个
setSomething(someType值)
方法,使用该方法将是可以接受的修改该属性的方法。

当类型是可变的时,返回一个副本通常很有用,这样客户端就无法从根本上修改您的数据,至少没有告诉你。考虑:

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
对象


然而,比所有这些复制更好的解决方案是支持不可变类型——当你有一个对不可变对象的引用时,你可以尽可能广泛地共享它,知道没有人可以在你脚下改变它的状态。

因为没有“私有数据”这样的东西,事实上,只有你无法获取的数据和你无法更改的数据

假设fieldName定义为StringBuilder fieldName。对于StringBuilder,您无法阻止有权访问它的人修改它。另一方面,如果它被定义为stringfieldname,那么(没有一些真正邪恶的反射)就不可能有其他人更改它


因此,它是不变性的昂贵替代品。更好的方法是使用一个只允许访问您想要提供的属性和操作的包装器。

已经有了很好的答案,但让我来介绍其他示例和参考

最好的解释来源是Josh Bloch的有效Java。至少有两章是关于不变性和防御副本的

简而言之: 在Java中,通过引用传递所有内容(我知道它过于简单,但这不是重点),并且许多类是可变的。因此,使用外部对象直接指定私有字段并不真正安全,因为下面的值可以在任何时间点从对象外部更改,从而破坏封装

访问器方法是打破封装的本质。在最常见的实现中,您只需将字段公开,并且如上所述,您允许任何人更改基础对象(如果他们允许的话)。最好的例子是IMHO。若您返回任何默认Java集合,任何人都可以向其中添加内容、删除元素甚至清除它。如果您的逻辑取决于状态,或者您正在编写多线程应用程序,那么这是获得竞争条件的最简单方法,这是我们真正不希望的

因此,一个好的做法是

  • 返回对象的深度副本(例如Guava副本收集方法)
  • 返回对象的视图(例如,Collections类及其方法)
  • 使用不可变对象(最简单的)
  • 克隆还是其他时髦的生意
每一个都有一些与之相关的成本。复制/克隆需要时间和内存。视图不是完全安全的,因为底层实现可能会在任何时间点发生变化,不可变对象不允许修改,并且很难在遗留系统中实现,等等,这取决于您找到平衡点,但我们总是乐于提供帮助:)

最后,出于完全相同的原因,在传入的可变参数的构造函数/设置器中进行防御性复制也是一种很好的做法。若有人将元素添加到集合中,那个么我们在构造函数中最终确定了元素,那个将是非常愚蠢的,因为我们并没有保留状态,这显然是我们想要的。所以,在构造函数中,如果您不控制传入的内容,请不要只进行简单的初始化(即使您控制传入的内容,复制也是一个好主意)


我更喜欢以集合为例,因为它们更容易解释如何复制/如何更改,但其他答案中提到的StringBuilder和日期确实表明,这不是集合的唯一问题。所以最好的答案是:记住,决赛是你最好的朋友。从一开始就经常使用它,永远不要相信易变的陌生人

我认为垃圾收集方面是有争议的:它可能会更快地清除特定的引用,但它也会导致GC更频繁地发生。主要目的可能是免疫
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);