ADT的可变与不可变
JAVA中数据类型可以分为基本数据类型(primitive types)和对象数据类型(object types)。基本数据类型用小写字母开头表示,有int、short、long、float、boolean、char、byte。而对象数据类型就可能有无数种,并且可以由程序员自行定义。
基本数据类型和对象数据类型对比简要如下:
可以看到,数据有些可变,有些不可变。此部分便聚焦于这一点。
一个抽象数据类型(ADT)是一组数据和对数据可以进行的操作的集合体。对数据进行的操作大致分为四种:
- Creator:构造器,用于生成一个新的对象。常用方法有构造函数或者工厂方法。
- Mutator:修改器,用于修改一个ADT的数据表示。
- Producer:生成器,借助原有的实例来生成一个类型相同的实例。
- Observer:观察器,用于观察一个ADT的抽象表示(而不是内部表示)。
参数区别:
对可变数据类型和不可变数据类型来讲,最大的区别就是不可变数据类型不提供mutator方法。比如String是一个不可变数据类型,它的append方法即是一个producer,返回的是一个新生成的String实例;而StringBuilder则是一个可变数据类型,它的append方法是一个mutator,改变了StringBuilder的抽象表示。
但是只是单纯的不设计mutator方法,对于实现一个不可变数据类型是不够的。我们需要防止一个不可变的数据类型从外部被破坏,也就是说不能有可以从外部改变内部表示的漏洞。
那么怎么防止这些漏洞呢?我目前的体会如下:
这里先讲一下我对于引用和内存空间的理解。由于我之前基本只有C语言相关的知识,所以就用C语言方面的概念来叙述。
JAVA里面所有的对象数据变量,其实际性质都类似于C语言中的指针,指向一块内存区域,然后变量的类型即是C语言中的指针类型,用于告诉编译器如果对内存进行操作。基本类型数据和指向对象的变量储存在栈中,对象数据储存在堆中,具体说明可以看一下这篇博客。另外JAVA中的堆栈可以直接用计算机系统中所讲的概念来理解、、、这就是先学CSAPP后学软构的安排所在嘛?
然后我们一个对一个对象变量赋值,其实就是让这个变量(指针)指向对应内存的地址,因此JAVA中相等==操作符,比较的是俩个变量的地址是否相同,故对于俩个对象来说,比较是否相等应该用equals方法。但是这里有一个“例外”,对于int的包装类Integer,如果其值在-128~127的话,是可以用==来比较大小的。这其中原因是,如果我们用到Integer对象,其会提前创建这个范围的Integer对象,当我们用到这些对象时,并不是新创建对象,而是返回对应值的对象的地址,因此可以用==号判定相等。而对于基本数据类型,其值直接分配在栈中,故也可以用==来判定栈中储存的值是否相等。
那么我们需要区分不可变数据类型和不可变引用。不可变引用即这个分配在栈中的变量,其指向的内存区域是不可变的,用修饰词final限制,直观来说就是栈帧中这个变量对应的位置存放的地址数据是不可以改变的。在snapshot diagram中体现为双线箭头。而不可变数据类型,意味这其在堆中分配的数据信息是不可变的,在我们设计方法时限制,直观来讲就是堆中对应区域的数据不可以改变。snapshot diagram中体现为双线椭圆。因此一个变量如果是final的,其始终指向一块固定的地址,但是指向的空间里的数据不一定是不可变的;而一个不可变数据类型,虽然其储存空间中的数据不会变,但是指向其的变量可以改为指向另一块内存。
针对上面的体会,我总结了几条构造不可变数据类型的经验:
- 使用private final修饰属性。private修饰词是为了防止外部可以直接看到内部表示,这也同样是保证表示独立性的一个重要原则。final是为了使所用的引用不可变,防止引用的地址空间更改。
- 使用防御式拷贝。对于有些observer的实现,直接返回我们的内部field会更方便。但是,如果我们直接返回我们的内部变量值,那么外部就获取了指向对象的地址,如同前文所说,可以进而更改地址对应区域存放的数据,从而破坏了不可变性质。这也是所谓的“表示泄露”问题。
- 内部表示尽量使用基本数据类型或者不可变数据类型。不同地方的基本数据类型在栈中的不同区域分配空间,因此外部的修改不会影响内部表示,在JAVA角度来看是不可变的。而不可变数据类型,即使外部知道了其的地址,也不能对其进行改变,这样也保证了我们的SafetyFromRepExposure。
那么,防御式拷贝怎样实现呢?核心思想就是新分配一块内存,然后其存放的值跟我们要返回的实际值是相同的。但是,单纯的浅拷贝是不够的,我们需要深拷贝!
回想我上面所说的,对象数据变量实际上是一个指针,因而如果我们的不可变数据类型A中的属性有些是对象类型的,命名为B,A堆中内存区域存放B的是指向B数据存放地址的指针。如果我们只是单纯的new了一个新对象C,使用A内部的field对这个新对象赋值,那么A、C俩个对象中属性B的地址值其实还是相同的,如果对B指向的内存区域进行修改,A中B指向的数据同样会改变,这样会破坏不可变性。因此要对A进行深层次拷贝,比如要重新new一个和B数据相同的对象D,然后赋值给C。那么,深拷贝到何地步就可以了呢?我的理解是当所有需要拷贝的属性值是不可变数据类型或者基本数据类型时,就可以停止拷贝,而是用原对象的属性值了,因为这俩种数据是不怕暴露地址或者值的,外部无法进行修改。
另外,JAVA标准库中也提供了常用可变对象的不可变视图产生方法。比如可以调用Collections.unmodifiableSet方法来产生Set数据类型的一个不可变的视图,当外部对其尝试调用mutator修改时,会抛出UnsupportedOperatonException异常。此外对于List、Map等也有类似方法。
不仅在返回值需要使用防御式拷贝,在构造函数中也同样需要防御式拷贝,这样可以防止在生成对象后,外部修改传入参数的数据来间接修改对象内部的数据。
下面是我做LAB3时写的一个不可变对象的实现代码。仅供参考。
由于VoteItem已知时不可变的,故这里就直接addAll了,不然需要拷贝一份。
//immutable
/**
* 一张选票,记录了对所有候选人的打分。
* 只能对一个候选人打一次分,若打多次则为非法选票
* 该选票为匿名
* @param <C> 候选人的种类
*/
public class Vote<C> {
// 缺省为“匿名”投票,即不需要管理投票人的信息
// 一个投票人对所有候选对象的投票项集合
private final Set<VoteItem<C>> voteItems = new HashSet<>();
// 投票时间
private final Calendar date = Calendar.getInstance();
// Rep Invariants
// voteItems集合不能为空,对每一个candidate只能有一个投票项,
// 因此集合中任俩个voteItem的candidate不能相同。
// Abstract Function
// AF(voteItem)=Set(voteItems)
// Safety from Rep Exposure
// 所有field都是private final的,然后返回值使用了不可变集合,提供一个只读的视图
private void checkRep() {
assert this.voteItems.size()!=0;
Set<C> candidates=new HashSet<>();
for(VoteItem<C> voteItem:this.voteItems){
assert candidates.add(voteItem.getCandidate());
}
}
/**
* 创建一个选票对象
* @param voteItems 输入的集合中,对每一个candidate只能有一个评分,
* 即集合中任俩个voteItem的candidate不能相同
*
*/
public Vote(Set<VoteItem<C>> voteItems) {
this.voteItems.addAll(voteItems);
}
/**
* 查询该选票中包含的所有投票项
* 提供一个不可变视图,不可以修改
* @return 所有投票项
*/
public Set<VoteItem<C>> getVoteItems() {
return Collections.unmodifiableSet(this.voteItems);
}
/**
* 一个特定候选人是否包含本选票中
*
* @param candidate 待查询的候选人
* @return 若包含该候选人的投票项,则返回true,否则false
*/
public boolean candidateIncluded(C candidate) {
for(VoteItem<C> voteItem:this.voteItems)
if(voteItem.getCandidate().equals(candidate))
return true;
return false;
}
@Override
public int hashCode() {
int result = voteItems.hashCode();
result = 31 * result + date.hashCode();
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Vote<?> vote = (Vote<?>) o;
if (!voteItems.equals(vote.voteItems)) return false;
return date.equals(vote.date);
}
}