不变量(Invariant)
首先,看下面这个例子
public class Tweet {
public String author;
public String text;
public Date timestamp;
/**
* Make a Tweet.
* @param author Twitter user who wrote the tweet
* @param text text of the tweet
* @param timestamp date/time when the tweet was sent
*/
public Tweet(String author, String text, Date timestamp) {
this.author = author;
this.text = text;
this.timestamp = timestamp;
}
}
这是一个Tweet类,最开始的三行,就是这个类的字段(representation)
如果一个类是immutable的,那么它的representation在创建后就不能被改变,mutable的则可以被改变
但是,即使是mutable类型的类,也应该通过调用它本身的方法来改变representation。
一个类,保持它自身的representation不受调用者的操作(除了它本身的方法)而改变的性质,就是类的不变性
以上面的Tweet 类为例,因为它的representation都是public的,所以,tweet的调用者可以轻易地知道,一个Tweet 使用了两个String类型和一个Date类型来存储数据。
因此,所有人都可以对Tweet 进行修改,比如:
Tweet t = new Tweet("justinbieber",
"Thanks to all those beliebers out there inspiring me every day",
new Date());
t.author = "rbmllr";
如果是A创建了t,但是B对t进行了修改(A不知情),那么,如果A打印出t的author,结果会是rbmllr而非justinbieber,这是一个很疑惑的情况。
因为t的representation在A没有进行任何操作的情况下,自己改变了。
因此,Tweet没有保证自身的不变性。
这是一个表示泄漏(representation exposure)的典型例子,也就是在类之外的代码可以直接修改类的representation
为了保证类的不变性,防止表示泄漏,我们可以有以下手段:
-
把representation 都改为private,这样,只有类内部的代码才能对representation 进行操作。如果外部代码想修改representation ,也只能通过类自身的方法,让类内部的代码来进行操作,比如setter方法
public String setAuthor(String author) { this.author = author; }
-
对一些字段使用
final
。
final关键字可以保证字段不能被“重新指派”
反应到图上,就是类的字段对一个对象的箭头不能被替换成指向另一个对象的箭头
好,对Tweet类进行改进:
public class Tweet {
private final String author;
private final String text;
private final Date timestamp;
public Tweet(String author, String text, Date timestamp) {
this.author = author;
this.text = text;
this.timestamp = timestamp;
}
/** @return Twitter user who wrote the tweet */
public String getAuthor() {
return author;
}
/** @return text of the tweet */
public String getText() {
return text;
}
/** @return date/time when the tweet was sent */
public Date getTimestamp() {
return timestamp;
}
}
因为对author和text都加上了final
,这使得一个Tweet类的author和text在被确定之后,就无法被改变。
但是,还有一个问题,final
虽然确保了下图的箭头不会指向别的对象,但不能确保它所固定指向的这个对象,不会发生改变
这里,timestamp是Date型的,mutable,所以,尽管timestamp的箭头固定指向了右下角的对象,仍不能保证这个对象本身不会发生变化。
当然,这里author和text都是mutable型的,所以加上final后一定不会被改变。
看下面这段代码:
/** @return a tweet that retweets t, one hour later*/
public static Tweet retweetLater(Tweet t) {
Date d = t.getTimestamp();
d.setHours(d.getHours()+1);
return new Tweet("rbmllr", t.getText(), d);
}
我们新建了一个d,这个d和t.timestamp一样,都指向上图中右下角的对象。
因此,如果我们对d进行修改,实际上,是对那个对象进行了修改,所以不光d的值加上了1,t.timestamp也会在其它调用者不知情的情况下,加上了1
这种同时有多个引用(也就是图中的箭头)指向同一个对象的情况,叫做aliases,它是问题的关键
对于aliases的防范,我们需要进行防御式拷贝(defensive copy)
注意,防御式拷贝要发生在两个地方,一是返回mutable类型的值时,二是将自身的representation赋值为传入的mutable类型时。
如下面两段代码:
public Date getTimestamp() {
return new Date(timestamp.getTime());
}
public Tweet(String author, String text, Date timestamp) {
this.author = author;
this.text = text;
this.timestamp = new Date(timestamp.getTime());//defensive copy
}