Equality in ADT and OOP
Equivalence Relation
现实中的对象实体是独特的,无法完全相等,但又相似性。
软件中,什么情况下两个事物认为是等价的、可以相互替代的,即为等价性问题。
在集合论中,等价关系是满足自反、对称、传递的二元关系,此定义可以作用在上述的等价性问题上。
Three ways to regard equality
ADT是对数据的抽象,体现为一组对数据的操作,而AF抽象函数是内部表示R到抽象表示A的映射。基于AF定义ADT的等价操作为:如果AF映射到同样的结果,则等价,即AF(a)=AF(b),则有a等价于b。
在外部观察者角度中,对两个对象调用任何相同的操作,都会得到相同的结果,则认为两个对象是等价的,反之亦然。
public class Duration{
private final int mins;
private final int secs;
// rep invariant:
/*
* mins>=0
* secs>=0
*/
// abstraciton 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类,现在有如下4个声明,则d1、d2和d4是等价的。
Duration d1 = new Duration (1,2);
Duration d2 = new Duration (1,3);
Duration d3 = new Duration (0,62);//最后体现为mins=0,secs=62,getLength=62;
Duration d4 = new Duration (1,2);
再看看下面的LetterSet的例子:
/** Immutable type representing a subset of the letters a-z,ignoring case */
class LetterSet{
private String s;
//AF:
/*
* AF(s)=the subset of letters {a...z} that are found in s(ignoring alphabetic case and non-letters)
*/
//RI:
/*
* true
*/
/** Make a LetterSet consisting of the letters found in chars
*(ignoring alphabetic case and non-letters).*/
public LetterSet(String chars){
s=chars;
}
/** @return the size of this set */
public int size() { ... }
/** @param letter must be a letter 'a'...'z' or 'A'...'Z'
* @return true iff this set contains letter, ignoring alphabetic case */
public boolean contains(char letter) { ... }
/** @return the length of the string that this set was constructed from */
public int length() { ... }
/** @return the union of this and that */
public LetterSet union(LetterSet that) { ... }
/** @return true if and only if all the letters in this set are lowercase */
public boolean isAllLowercase() { ... }
/** @return the first letter in s */
public char first() { ... }
}
下面有五个实例化对象,可以进行练习:
new LetterSet("abc");
new LetterSet("aBc");
new LetterSet("");
new LetterSet("bbbbbbbc");
new LetterSet("1a2b3c");
== vs. equals()
两个=为引用等价性,即内存地址是否相同(注意,Python中是is);equals为对象等价性,未重写equals则默认是内存地址是否相同,自定义ADT时根据对等价的需要,决定是否重写Object的equals()(Python中则为两个=)
对于基本数据类型,使用两个=进行判定相等,对对象类型则使用equals进行判定相等。
Implementing equals()
在Object中实现的缺省equals()是判定引用等价性,根据实际需要常常需要@Override重写
public class Duration extends Object{
// explicit method that we declared:
public boolean equals(Duration that){
return this.getLength()==that.getLength();
}
// imlicit method inherited from object:
public boolean equals (Object that){
return this == that;
}
}
注意第一个方法是重载(参数类型不同,编译器进行选择,为静态检查),第二个方法是重写
Duration d1 = new Duration (1,2);
Duration d2 = new Duration (1,2);
Object o2 = d2;
d1.equals(d2);//true
d1.equals(o2);//false
在运行时即使o2和d2是一个对象,但是在编译时已经决定了二者调用的equals方法不同。
instanceof是判断某个对象是否为特定类型(或其子类型),属于动态类型检查,在equals之外的方法要尽量避免使用instanceof和getClass()方法。
The Object contract
对于equals方法,需要始终满足如下条件:
1.满足等价关系,即自反、对称、传递;
2.除非对象被修改,否则调用多次equals应是相同的结果;
3.对于非空的x,x.equals(null)始终为false;
4.等价的对象,hashCode()结果必须相同;
上述的性质可以概括为自反性Reflexice、对称性Symmetric、传递性Transitive、一致性Consistent、非空性Non-null。
哈希表实现了key-value之间的映射,表中包含一个数字,键值对中的key被映射为hashcode,对应到数组的index,hashcode决定了数据被存储到数组的哪个位置。哈希表的RI中基本要求就是key在slot中的位置由hashcode确定。
等价的对象必须要有相同的hashCode,但是不等价的对象也可以映射为同样的hashCode,但是相对的性能会下降。因此,建议不等价的对象要有不同的hashCode。并且,重写equals方法的同时也要对应的重写hashCode方法。
另外,除非是可变类型,否则hashCode在其他方法调用中也不应该被改变。
对于hashCode的重写,可以结合计算中用到的所有信息的hashCode组合出新的hashCode。
下面是重写的hashCode样例:
pubic final class PhoneNumber{
private final short areaCode;
private final short prefix;
private final short lineNumber;
@Override
public int hashCode(){
int reuslt = 17;//Nonzero is good
result = 31 * result + areaCode;//Constant must be odd
result = 31 * result + prefix;//" " " "
result = 31 * result + lineNumber;//" " " "
return result;
}
@Override
public int hashCode(){//Less efficient,but otherwise equally good
short[] hashArray = {areaCode, prefix, lineNumber};
return Arrays.hashCode(hashArray);
}
}
Equality of Mutable Types
观察等价性,在不改变状态的情况下,两个可变对象是否看起来一致。
行为等价性,调用对象的任何方法都展示出一致的结果。
对于不可变对象,观察等价性和行为等价性是完全一样的,因为不可变对象没有mutator方法,不可变对象用引用等价性和对象等价性判断。
对可变对象来说,倾向于实现严格的观察等价性,Java对其大部分可变数据类型(如Collections)使用观察等价性,如两个List中包含相同顺序的元素,则equals()返回true,部分可变类型用行为等价性。
在某些时候观察等价性可能导致bug,甚至会破坏RI。
例如,有List和Set如下,开始检查set中是否包含list,结果为true,对list使用Observers方法之后,再判断set中包含list,则结果为false。
List<String> list = new ArrrayList<>();
list.add("a");
Set<List<String>> set = new HashSet<List<String>>();
set.add(list);
set.contains(list);//true
list.add("b");
set.contains(list);//false,由于在上一行改变了list,但是set中的list并没有相应改变
当List的序列被改变,对象的hashcode变了,但是HashSet未更新其在bucket的位置,查找时新hashcode的位置找不到对应元素。
当equals和hashcode的结果可能被可变影响时,hash table的RI会遭到破坏。
因此,如果某个mutable对象包含在Set集合中,当其发生改变后,集合类的行为不确定。
在JDK中,不同mutable类使用不同的等价性标准。
例如,Date类的equals为观察等价性,当前仅当getTime方法返回相同的long类型的value;List类的equals为观察等价性,当且仅当具体对象也是list,两个list有相同的大小,每对符合的元素在两个list中都等价;String类的equals继承自Object类,属于行为等价性。
对于可变类型,实现行为等价性即可,即只有指向同样内存空间的objects才相等,无需重写equals和hashCode方法,若要判断两个可变对象看起来是否一致,最好定义一个新的方法。
对于不可变类型,必须重写equals和hashCode方法。
Class Bag<E>{
/** make an empty bag */
public Bag<E>()
/** modify this bag by adding an occurrence of e, and return this bag */
public Bag<E> add(E e)
/** modify this bag by removing an occurrence of e (if any), and return this bag */
public Bag<E> remove(E e)
/** return number of times e occurs in this bag */
public int count(E e)
对于上述可变的Bag类,有下列实例化对象
Bag<String> b1 = new Bag<>().add("a").add("b");
Bag<String> b2 = new Bag<>().add("a").add("b");
Bag<String> b3 = b1.remove("b");
Bag<String> b4 = new Bag<>().add("b").add("a");
可变类型,行为等价性,需要引用相等。
b1.equals(b2);//false
b1.equals(b3);//true
b1.equals(b4);//false
b2.equals(b3);//false
b2.equals(b4);//false
b3.equals(b1);//true
对应的快照图如下图所示:
如果实现的是观察等价性,则需要通过Observer(count())判断。
clone()创造并返回一个此对象的副本,对于任意的对象x,有如下的表达式为true:
x.clone()!=x,x.clone().getClass()==x.getClass(),x.clone().equals(x)
Autoboxing and Equality
原始类型和它们的对象类型是等价的,例如int和Integer。
Integer x = new Integer(3);
Integer y = new Integer(3);
x.equals(y);//true
x==y;//false
(int)x==(int)y;//true