18

This question is about good programming practices and avoiding potential holes.
I read Joshua Bloch's Effective Java and here is what I wonder:
Why should I consider making defensive copies in getter methods in my immutable class with no mutators in it?
And second: why should I make my fields final in addition to private ? Is this only about performance (not security) ?

4

7 回答 7

41

我相信这种情况可以证明这种说法是合理的:

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<Integer> getSomeIds() {
    return Collections.unmodifiableList(someIds);
}
于 2012-06-08T15:37:05.030 回答
4

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

此处显示的防御性复制样式的替代方法(每次请求信息时都构造一个新对象实例)是让对象的信息检索方法接受一个可变对象并用检索到的信息填充该对象。这种方法要求请求信息的代码在调用方法检索信息之前构造一个(可能是空白的)对象来接受信息,但如果使用循环来检索、简要检查并丢弃 100 个根据信息,构造一个每次循环都可以重用的对象可能比构造 100 个新对象更快,每个对象都只会短暂使用。

于 2012-06-12T16:41:23.457 回答
1

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

关于最后一个问题,没有必要将字段设为 final,而是将它们设为最终授权,一旦创建对象就无法修改。

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

例如,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, e.g. 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. 您希望确保可变对象状态在您检查它的约束和将它复制到您的字段中的时间之间不会发生变化。为此,您始终必须检查参数本地副本的约束。

于 2012-06-08T15:45:08.573 回答
1

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

public class MyImmutable
{
  private final StringBuilder foo = new StringBuilder("bar");

  public StringBuilder getFoo()
  {
    return foo; // BAD!
  }
}

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

于 2012-06-08T15:38:05.270 回答
1

我稍微修改了 Tomasz Nurkiewicz 的答案,以说明为什么将 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
于 2016-01-13T09:36:48.643 回答
0

为什么我应该考虑在我的不可变类的 getter 方法中制作防御性副本,而其中没有修改器?

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

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

考虑你有一个对象缓存。每当从缓存中检索并更改对象时,您都会冒着修改缓存中值的风险。

除了私有之外,我为什么还要将我的字段设为最终字段?

只是为了帮助您实现不变性并防止值在设置后意外或交付更改(例如,通过子类化)。

于 2012-06-08T15:38:25.103 回答
0

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

public class Immutable {
   private List<Object> something;

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

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

于 2012-06-08T15:39:11.510 回答