为什么在不可变类中的getter中制作防御性副本?

这个问题是关于良好的编程实践和避免潜在的漏洞。
我读了Joshua Bloch的Effective Java,这就是我想知道的:
为什么我应该考虑在我的不可变类中使用getter方法制作防御性副本而不使用mutator?
第二:除了私人,我为什么要让我的领域最终 ? 这只是关于性能(不是安全性)吗?

我相信这是certificate这一陈述的理由:

 public class Immutable { private final String name; private Date dateOfBirth; public Immutable(String name, Date dateOfBirth) { this.name = name; this.dateOfBirth = dateOfBirth; } public String getName() { return name; } public Date getDateOfBirth() { return dateOfBirth; } } 

getName()很好,因为它也返回不可变对象。 但是getDateOfBirth()方法可以破坏不变性,因为客户端代码可以修改返回的对象,因此也修改了Immutable对象:

 Immutable imm = new Immutable("John", new Date()); imm.getName(); //safe Date dateOfBirth = imm.getDateOfBirth(); //hundreds of lines later dateOfBirth.setTime(0); //we just modified `imm` object 

返回不可变对象和基元是安全的(因为它们是按值返回的)。 但是,您需要制作可变对象的防御副本,例如Date

 public Date getDateOfBirth() { return new Date(dateOfBirth.getTime()); } 

并将集合包装在不可变的视图中(如果它们是可变的),例如,请参阅Collections.unmodifiableList()

 public List getSomeIds() { return Collections.unmodifiableList(someIds); } 

Java中的对象引用是混杂的; 如果将对象引用传递给方法,则该方法无法知道其他人可能具有对同一对象的引用,或者同样可能尝试对其进行操作。 同样,如果一个方法返回一个对象引用,那么就不知道收件人可以用它做什么。 因此,如果一个类型允许任何引用变异的人,那么持有对这样一个对象的引用的实体可以确保它不会被变异的唯一方法就是保持对私有对象的引用,这个私有对象从未有过,永远不会得到,参考。

这里显示的防御性复制风格的替代方案(每次请求信息时构造一个新的对象实例)是让对象的信息检索方法接受一个可变对象,并用检索到的信息填充该对象。 这种方法要求在调用方法来检索信息之前,要求信息的代码构造一个(可能是空白的)对象来接受信息,但是如果有人将例如使用循环来检索,简要检查,并丢弃100个信息,构造一个可以在每次循环中重复使用的对象可能更快,而不是构造100个新对象,每个对象只是简单地使用。

虽然封闭类可能是不可变的,但其getter方法返回的引用可能是可变的,允许调用者修改不可变的传递状态。 例:

 public class MyImmutable { private final StringBuilder foo = new StringBuilder("bar"); public StringBuilder getFoo() { return foo; // BAD! } } 

您应该使用private进行封装(防止类依赖于您的类的实现细节)。 你应该使用final来确保你不要错误地修改字段,如果它不是要修改的(是的,它可能有助于性能)。

只有当您在getter中返回的对象是可变的时,才应该进行防御性复制,因为客户端可能会改变您的对象状态。

关于最后一个问题,并不是绝对必须使字段最终,而是将它们作为最终授权,一旦创建对象就无法修改。

事实上,如果您需要在创建对象后修改某些字段,这也很好,但您必须确保客户端代码无法区分对象状态已被更改。 你必须做的是不可变的是对象的外部可见状态,而不是内部状态。

例如,String类在创建时不计算它的哈希码,它在第一次需要时计算它,然后将其缓存在私有可变字段上。

我假设你的类被声明为final或只有私有构造函数,否则它的子类可能以不可预测的方式改变你的非final字段……

一些示例代码澄清:

 public final class Question { //final class to assure that inheritor could not // make it mutable private int textLenCache = -1; //we alter it after creation, only if needed private final String text; private Date createdOn; public Immutable(String text, Date createdOn) { Date copy=new Date(createdOn.getTime() ) //defensive copy on object creation //Ensure your class invariants, eg both params must be non null //note that you must check on defensive copied object, otherwise client //could alter them after you check. if (text==null) throw new IllegalArgumentException("text"); if (copy==null) throw new IllegalArgumentException("createdOn"); this.text= text; //no need to do defensive copy an immutable object this.createdOn= copy; } public int getTextLen() { if (textLenCache == -1) textLenCache=text.length(); //client can't see our changed state, //this is fine and your class is still //immutable return textLenCache; } public Date getCreatedOn() { return new Date(createdOn.getTime()); //Date is mutable, so defend! } } 

编辑

需要构造函数可变参数的防御性副本有两个原因:

  1. 客户端代码可以在创建对象后更改参数的状态。 您需要复制参数以避免这种可能性。 例如,假设String对象构造函数String(char [] value)使用了您提供的char数组而不复制它:您将能够通过更改构造函数中提供的char数组来更改String内容。

  2. 您希望确保可变对象状态在您检查约束的时间与您在字段中复制它的时间之间不会发生变化。 对于此原因,您始终必须检查参数的本地副本上的约束。

我稍微修改了Tomasz Nurkiewicz的回答,以certificate为什么使dateOfBirth final不能防止它被客户端类更改:

  public class Immutable { private final String name; private final Date dateOfBirth; public Immutable(String name, Date dateOfBirth) { this.name = name; this.dateOfBirth = dateOfBirth; } public String getName() { return name; } public Date getDateOfBirth() { return dateOfBirth; } public static void main(String[] args) { //create an object Immutable imm = new Immutable("John", new Date()); System.out.println(imm.getName() + " " + imm.getDateOfBirth()); //try and modify object's intenal String name = imm.getName(); //safe because Integer is immutable name = "George"; //has no effect on imm Date dateOfBirth = imm.getDateOfBirth(); dateOfBirth.setTime(0); //we just modified `imm` object System.out.println(imm.getName() + " " + imm.getDateOfBirth()); } } **Output:** John Wed Jan 13 11:23:49 IST 2016 John Thu Jan 01 02:00:00 IST 1970 

1如果你没有制作防御性副本,你可以让你的对象被发出,一旦发出,你的类不再是不可变的,调用者可以自由地改变对象。

 public class Immutable { private List something; public List getSomething(){ return something; // everything goes for a toss, once caller has this, it can be changed } } 

2如果您的字段只是私有而非最终字段,则意味着您可以重新初始化字段,但如果您的字段是最终字段,则只会初始化一次而不是多次,并且您可以实现不变性。

为什么我应该考虑在我的不可变类中使用getter方法制作防御性副本而不使用mutator?

这仅在返回的对象不是不可变的情况下才有用。

像这样的深层不变性对于防止不可变类所持有的任何对象的更改很有用。

考虑您有一个对象缓存。 每当从缓存中检索对象并进行更改时,您都有可能同时修改缓存中的值。

除了私人之外,我为什么要让我的领域成为最终的?

只需帮助您实现不变性并防止意外或有意义地在设置后更改值(例如,通过子类化)。