最近在软件构造课里学习了Java中ADT的等价性,希望能够通过写博客这种方式来总结一下,加强理解。
等价关系
之前在集合论与图论中学过关系,等价关系需要满足三个性质,自反性、对称性、传递性。
对于一个集合T,这个集合的某一个关系可以定义为其笛卡尔积的子集,即关系R
⊆
\subseteq
⊆T
×
\times
×T。下面看看等价关系的三个性质。
自反性
∀
\forall
∀t
∈
\in
∈T, t R t.
对称性
∀
\forall
∀u,v
∈
\in
∈T, if u R v, then v R u.
传递性
∀
\forall
∀u,v,w
∈
\in
∈T, if u R v, and v R w, then u R w.
对象的等价性
注:这里的对象指的都是不可变(immutable)的,如果是可变的,直接用Object类里的函数即可。
老师上课举的例子我觉得对我的启示比较大,两个对象是否等价,需要比较的不应该是它们内部的属性,而是属性经过AF(abstraction function)映射后是结果,即AF(rep1)是否等于AF(rep2)。其中rep1和req2分别是待比较的两个对象的属性列表。
例子如下:
public class Duration {
private final mins;
private final secs;
// rep invariant:
// mins >= 0, secs >= 0
// abstraction function:
// represents a span of time of mins minutes and secs seconds
/** Make a duration lasting for m minutes and s seconds. */
public Duration(int m,int s) {
mins=m;
secs=s;
}
/** @return length of this duration in seconds */
public long getLength() {
return mins*60+secs;
}
}
这是一个表示时间的类,如果要比较两个Duration对象的等价性,判断两个属性是否相等显然是不合理的,比如
Duration d1=new Duration(1,2);
Duration d2=new Duration(0,62);
这两个对象虽然内部的属性不同,但表示的意义都是62秒,所以应该等价。这里就可以用函数getLength()来判断了,如果返回值相等,则等价。
在这里需要约束class里面的方法。对于两个等价的对象,执行同一观察器(observer)函数,返回的结果也要一样。例如有一个class的结构如下:
class LetterSet {
private String s;
//Abstraction function:
// AF(s) = the subset of the letters {a...z} that are found in s
// (ignoring alphabetic case and non-letters)
/** @return true if and only if all the letters in this set are lowercase */
public boolean isAllLowercase() {...}
}
这个isAllLowercase函数就是错误的。比如"abc"和"ABC",有AF,都映射到集合{a,b,c},但这个函数的返回值不同。
equals函数
首先看一下Object类中的equals函数。
public class Object() {
...
public boolean equals(Object that) {
return this == that;
}
}
对于这个缺省的equals函数,我的理解是判断两个对象是否指向同一个内存空间(不太确定),所以一般来说,对于自己编写的类,需要以上文的原则重写这个函数。
重写函数的代码基本如下:
@Override
public boolean equals(Object o) {
if (!(o instanceof Duration)) return false;
Duration that = (Duration) o;
return this.getLength() == that.getLength();
}
注意,这里的参数必须是Object型。这是因为这是对父类函数的重写(override),参数列表必须相同,如果参数类型不同,那是重载(overload)。比如说我们把equals函数写成下面的格式:public boolean equals(Duration),则如果调用函数传入的参数类型是Duration,当然默认调用是我们写的这个函数,但如果参数是其他的类型,因为类型不匹配,所以会调用父类的equals函数,发生错误。
hashCode函数
这个函数的功能是将对象映射到一个常数。看一下Object类中的hashCode函数。
public class Object {
...
public boolean equals(Object that) {return this == that; }
public int hashCode() {return /* the memory address of this */;}
}
这个函数的默认实现是返回它的内存地址。重写这个函数的规则和equals相似,都是由AF决定的。如果两个对象等价,则它们的hashCode返回值一定相等。对于上面的例子Duration这个类,重写的一种方法为:
@Override
public int hashCode() {
return (int) getLength();
}
可变类型的等价性
这里引出了两个概念,观察等价性和行为等价性。
观察等价性
在不改变状态的情况下,两个mutable对象是否看起来一致。
行为等价性
调用对象的任何方法都展示出一致的结果。
对于不可变类型来说,这两种等价性是等价的,不可变类型没有变化器(mutator)方法。
对于可变类型,往往使用观察等价性来判断,但有时用观察等价性会出现bug。
当我们编写一个可变类型时,实现行为等价性即可,equals函数和hashCode函数,不需要重写,直接继承Object的两个方法就行了。当然如果一定要判断两个可变类型“看起来”是否一致,可以定义一个新的方法来判断。
对equals和hashCode的总结
对于不可变类型
必须重写这两个方法,保证行为等价性,行为等价性等价于观察等价性。
对于可变类型
不需要重写,保证行为等价性。
最后,本人新手,恳请斧正。