本篇文章继续搬运MIT上ADT部分相关知识
Reading 11: Abstraction Functions & Rep Invariants
不变量
继续讨论实现好的抽象数据类型的方法,最后一条,可能也是最重要的一条,好的抽象数据类型的特性就是它保护自身的不变量。不变量是一种在程序每个可能的运行时刻始终保持正确的特性。不可修改性是我们已经遇到过的很重要的不变量:一旦创建,一个不可修改的对象应该在它的生命周期内始终代表相同的值。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类型是不可修改的?
第一个威胁到不可修改性的事实:没有什么能够阻挡下面的代码运行:
Tweet t = new Tweet("justinbieber",
"Thanks to all those beliebers out there inspiring me every day",
new Date());
t.author = "rbmllr";
这是一个典型的表现泄露的例子,意味着类型外的代码可以直接修改该类型的表现。表现泄露不止威胁到不变性,也威胁到了表现独立性。我们不能在不影响客户端直接访问这些成员的情况下不改变Tweet的实现。
幸运的是,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为参数,并返回相同的tweet,但时间延后了一小时。
问题出在哪儿呢?getTimestamp方法返回了Date的引用,t.timestamp 和d都是相同的可修改对象的引用,所以我们可以通过d.setHours方法修改日期。
我们可以修复这种表现泄露通过防御性拷贝的方式:返回可修改对象的复制来避免引用泄露:
public Date getTimestamp() {
return new Date(timestamp.getTime());
}
我们做了防御性拷贝之后,仍然存在表现泄露!考虑如下的客户端代码:
/** @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;
}
如上代码使用一个Date对象代表一天24小时,为每个小时都产生了一条tweet。但请注意每一个Tweet的构造器都保存了相同的Date引用,所以24个Tweet对象最后都表示了同一个时间
明智的方法是通过防御性拷贝初始化类:
public Tweet(String author, String text, Date timestamp) {
this.author = author;
this.text = text;
this.timestamp = new Date(timestamp.getTime());
}
为可修改类型提供不可修改包装器
JAVA容器类提供了有趣的折中方法:不可修改包装器
**Collections.unmodifiableList()**以List作为参数,用一个看起来像List的类包装它,使修改者不可使用:set(),add(),remove()等等