表示不变量
不变量
产生好的ADT设计,其中最重要的一点就是它会保护/保留自己的不变量。 不变量是一种属性,它在程序运行的时候总是一种状态,而不变性就是其中的一种:一旦一个不变类型的对象被创建,它总是代表一个不变的值。当一个ADT能够确保它内部的不变量恒定不变(不受使用者/外部影响),我们就说这个ADT保护/保留自己的不变量.
当一个ADT保护/保留自己的不变量时,对代码的分析会变得更简单。例如,你能够依赖字符串不变性的特点,在分析的时候跳过那些关于字符串的代码;或者当你尝试基于字符串建立其他的不变量的时候,也会变得更简单。与此相对,对于可变的对象,你将不得不对每一处使用它的代码处进行审查。
不变性
在这篇阅读的后面,我们会看到许多关于不变量的例子,现在我们先看一看不变性:
/**
* This immutable data type represents a tweet from Twitter.
*/
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对象是不可变的(一旦被创建,author, message, 和 date都不能被改变)?
第一个威胁就是使用者可以直接访问Tweet内部的数据,例如:
Tweet t = new Tweet("justinbieber",
"Thanks to all those beliebers out there inspiring me every day",
new Date());
t.author = "rbmllr";
这就是一个表示暴露(Rep exposure)的例子,就是说类外部的代码可以直接修改类内部存储的数据。上面的表示暴露不仅影响到了不变量,也影响到了表示独立性,如果我们改变类内部数据的表示方法,使用者也会受到影响。
幸运地是,Java给我们提供了处理这样的表示暴露的方法:
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;
}
}
其中, private 表示这个区域只能由同类进行访问;而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);
}
retweetLater 希望接受一个Tweet对象然后修改Date后返回一个新的Tweet对象。
问题出在哪里呢?其中的 getTimestamp 调用返回一个一样的Date对象,它会被 t.t.timestamp 和 d 同时索引。所以当我们调用 d.setHours()后,t也会受到影响,如下图所示:
这样,Tweet的不变性就被破坏了。这里的问题就在于Tweet将自己内部对于可变对象的索引“泄露”了出来,因此整个对象都变成可变的了,使用者在使用时也容易造成隐秘的bug。
我们可以通过防御性复制来弥补这个问题:在返回的时候复制一个新的对象而不会返回原对象的索引。
public Date getTimestamp() {
return new Date(timestamp.getTime());
}
可变类型通常都有一个专门用来复制的构造者,你可以通过它产生一个一模一样的复制对象。在上面的例子中,Date的复制构造者就接受了一个timestamp值,然后产生了一个新的对象。另一个复制可变对象的方法是使用clone() ,但是它没有被很多类支持(译者注:标准库里面只有5%支持),在Java中,使用clone()可能会带来一些麻烦。你可以在 Josh Bloch, Effective Java, item 11, 或者 Copy Constructor vs. Cloning获得更多有关这方面的信息。
现在我们已经通过防御性复制解决了 getTimestamp返回值的问题,但是我们还没有完成任务!思考这个使用者的代码:
/** @return a list of 24 inspiring tweets, one per hour today */
public static List<Tweet> tweetEveryHourToday () {
List<Tweet> list = new ArrayList<Tweet>();
Date date = new Date();
for (int i = 0; i < 24; i++) {
date.setHours(i);
list.add(new Tweet("rbmllr", "keep it up! you can do it", date));
}
return list;
}
这个代码尝试创建24个Tweet对象,每一个对象代表一个小时,如下图所示:
但是,Tweet的不变性再次被打破了,因为每一个Tweet创建时对Date对象的索引都是一样的。所以我们应该对创建者也进行防御性编程:
public Tweet(String author, String text, Date timestamp) {
this.author = author;
this.text = text;
this.timestamp = new Date(timestamp.getTime());
}