这个问题是关于良好的编程实践和避免潜在的漏洞
我读了Joshua Bloch的Effective Java,这就是我想知道的:
我为什么要考虑在我的不可变类中使用getter方法制作防御性副本而不使用mutator呢?
第二:除了私人之外,我为什么要将我的字段 final 设为?这只是关于性能(而不是安全性)吗?
答案 0 :(得分:39)
我相信这种说法证明了这一点:
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);
}
答案 1 :(得分:3)
Java中的对象引用是混杂的;如果将对象引用传递给方法,则该方法无法知道其他人可能具有对该同一对象的引用,或者同样也可能尝试使用该对象。同样,如果一个方法返回一个对象引用,那么就不知道收件人可以对它做什么。因此,如果一个类型允许任何引用它的人改变它,那么持有对这样一个对象的引用的实体可以确保它不会被改变的唯一方法是保持对私有对象的引用,这对外部从未有过,永远不会得到,参考。
此处显示的防御性复制风格的替代方案(每次请求信息时构造新的对象实例)是让对象的信息检索方法接受可变对象并使用检索到的信息填充该对象。这种方法要求在调用方法检索信息之前,要求信息的代码构造一个(可能是空白的)对象来接受该信息,但是使用循环来检索,简要检查并丢弃100条信息,构造一个可以在每次循环中重复使用的对象可能更快,而不是构造100个新对象,每个对象只能短暂使用。 / p>
答案 2 :(得分:1)
虽然封闭类可能是不可变的,但其getter方法返回的引用可能是可变的,允许调用者修改不可变的传递状态。例如:
public class MyImmutable
{
private final StringBuilder foo = new StringBuilder("bar");
public StringBuilder getFoo()
{
return foo; // BAD!
}
}
您应该使用 private 进行封装(防止类依赖于您的类的实现细节)。您应该使用 final 来确保如果不打算修改字段,则不要错误地修改字段(是的,可能帮助提高性能)。< / p>
答案 3 :(得分:1)
只有当您在getter中返回的对象是可变的时,才应该进行防御性复制,因为客户端可能会改变您的对象状态。
关于最后一个问题,并不是绝对必须使字段成为最终字段,而是将它们作为创建对象后无法修改的最终授权。
事实上,如果您需要在创建对象后修改某些字段,这也没关系,但您必须确保客户端代码无法区分对象状态已被更改。你必须做的是不可变的是对象的外部可见状态,而不是内部状态。
E.g。 String类在创建时不计算它的哈希码,它在第一次需要时计算它,然后将它缓存在私有可变字段上。
我假设您的类被声明为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!
}
}
修改强>:
构造函数可变参数的防御性副本需要有两个原因:
客户端代码可以在创建对象后更改参数的状态。您需要复制参数以避免这种可能性。例如。想想如果String对象构造函数 String(char[] value)使用了您提供的char数组而不复制它:您可以通过更改构造函数中提供的char数组来更改String内容。
您希望确保可变对象状态在您检查其约束的时间与您在字段中复制它的时间之间不会发生变化。对于此原因,您始终必须检查参数的本地副本上的约束。
答案 4 :(得分:1)
我略微修改了Tomasz Nurkiewicz的回答,以证明为什么使dateOfBirth最终不会保护它不被客户端类改变:
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
答案 5 :(得分:0)
为什么我要考虑在我的不可变类中使用getter方法制作防御性副本而不使用mutator?
仅当返回的对象不是不可变的时才有用。
像这样的 深度不变性对于防止不可变类所持有的任何对象的更改很有用。考虑您有一个对象缓存。每当从缓存中检索对象并进行更改时,您都有可能同时修改缓存中的值。
除了私人以外,为什么我要将我的字段设为最终字段?
简单地帮助您实现不变性并防止一旦设置(例如,通过子类化)意外或有意义地改变这些值。
答案 6 :(得分: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如果您的字段只是私有而非最终字段,则表示您可以重新初始化该字段,但如果您的字段是最终字段,则只会初始化一次而不是多次,并且您可以实现不变性。