HIT软件构造学习笔记7-Equality in ADT and OOP

Equivalence Relation

Equality operation on an ADT

ADT是对数据的抽象, 体现为一组对数据的操作

抽象函数AF:内部表示->抽象表示

基于抽象函数AF定义ADT的等价操作

  • 等价关系

我们定义下面一种关系E,使之满足:

 同时满足上述自反传递对称的关系我们称之为等价关系

举一个例子来说,我们经常使用的逻辑比较符==.equals()方法就是一种等价关系。因为

 满足自反对称传递,因此为等价关系。

Three ways to regard equality

ADT是对数据的抽象,通过已操作为特点的数据的抽象而不是表示方法的抽象。

Using AF to define the equality

对于一个抽象数据类型而言,抽象函数AF解释了具体的表示值是怎么对应映射到抽象值的。所以我们可以根据抽象函数AF来定义ADT的等价操作。具体的定义为:如果AF映射到同一个抽象空间的话,则等价

Using observation to define the equality

另一种定义等价性的方式就是通过observer来判断。在使用observer时,我们说如果当两个对象让我们看不出来行为上的区别,那么他们就是等价的

因为对于ADT来说,observation意味着观察被调用对象的操作,因此当对两个对象调用任何相同的操作都得到相同的结果的时候,这两个对象就是等价的。

Example

 针对上图中的类我们定义一些操作,代码如下

/** @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")

看我们定义的方法都属于observer,我们拿.contains()方法做一下解释。这里我们说的等价性是指,通过观察器得到的结果相同的,就比如字符串"abc"和字符串"aBc"通过contains()方法得到的结果是一样的;而且这两个字符串在AF定义的等价性上也是相同的,所以我们说.contains()方法定义的观察丁家兴和AF定义的等价性有着相同的选择

再看.size()方法,如果两个实例的.size()方法相同的话,AF的映射结果是有可能不同的。就比如字符串"abc"和字符串"def",他们的.size()方法返回值都是3,但是AF映射出的Set确实不同的,因此我们说.size()方法单独是无法判断观察等价性的但是结合.contains()方法就可以判断了

从集合论的角度来描述一下吧,可能让你更容易理解一点。我们以椭圆来表示一个方法所包含的等价对象的多少:如果一个椭圆B被另一个椭圆A包含,那么A和B就不是等价的,因为属于大椭圆的元素不一定属于小椭圆;如果一个椭圆A包含一个椭圆B,那么A和B就是等价的因为属于椭圆B的元素一定属于椭圆A。这两句话是有着很大区别的!我们以Venn图来演示并解释:

 在上面我们讨论过,.size()方法的观察等价性和AF定义的等价性是不一样的,表现为.size()方法的圈更大,包含了更多AF中没有的元素;而.contain()方法观察等价性筛选出来的等价元素和AF选出来的是一毛一样的,因此圈重合;那么.length()方法定义的观察等价和AF有不重合的地方,因此也不同。

 == vs. equals()

在Java中,这两种等价性测试都是支持的,但是他们有着不同的语义:

  • ==:比较的是引用等价性,也就是说如果两个对象指向同一个引用的值,那么它们通过==判断就是等价的。
  • .equals()方法:.equals()方法判断的是对象等价性。需要注意的是,在自定义ADT时,需要根据对等价的要求,决定是否重写Object的.equals()方法。

针对这两种判断在使用的时候我们进行一下简单的总结:对于基本数据类型我们使用==判断相等对于对象类型,我们使用.equals()方法判断相等。再对两个对象等价性进行判断的时候,我们应该总是使用.equals()方法,如果我们想要判断两个对象的逻辑是内存地址是否相等,那么则可以不用重写Object.equals()方法,此时它等价于==;其他情况下如果我们判断特定的逻辑,则需要重写Object.equals()方法。
 

Implementing equals()

在Object类中,缺省的equals()方法判断的是引用等价性,而这通常不是我们所期望的,因此需要重写。我们看一下下面一段代码示例:

public class Duration{
		...
		// Problematic definition of equals()
		public boolean equals(Duration that){
				return this.getLength() == that.Length();
		}
}

你可能会想这是一种重写,但是实际上它是一种重载。为什么呢?因为这个类中的.equals()方法和他继承的Object类中的.equals()方法有着不同的参数列表,因此这种写法是一种重载。

我们的client可以这样使用:

Duration d1 = new Duration(1, 2);
Duration d2 = new Duration(1, 2);
Object o2 = d2;
assertEquals(true, d1.equals(d2));
assertEquals(false, d1.equasl(o2));

我们关注assert的这两行,为什么会是这样的结果呢?我们再传入参数o2的时候,传入的是一个Object类型的参数,因此我们在调用的时候实际上调用的是父类中的.equals()方法,比较的是引用等价性,那么显然,两者引用是不相等的。关于重载的问题我们在上一章已经做了详细的讨论,相信你已经理解了;如果不明白的话可以跳回看一下上一章的6.2中的示例。

 如果我们想要重写.equals()方法,最好的做法是在要重写的方法前面加上@Override,请求编译器帮助你检查两个方法的签名是否一致。给一个重写的例子:

@Override
public boolean equals(Object thatObject){
		if (!(thatObject instanceof Duration)) return false;
		Duration tahtDuration = (Duration) thatObject;
		return this.getLength() == thatDuration.getLength();
}

我们还可以进行优化,关于数值比较的部分我们可以通过委派的方式让另一个方法去做这件事,从而使代码等价清晰简洁

equals()重写套路

首先将传入的参数和null进行比较,如果传进来一个空指针那么返回值肯定是false;接下来判断如果传入类型是本类的一个字类型,那么也返回false;最后就是进行判断逻辑的实现了。

  • instanceof关键字

instanceof关键字实现的功能是判断某个对象是不是特定的类型或者他的子类型,使用instanceof是一种动态类型检查,而非静态类型检查。它的实现逻辑是,尝试进行一次向运算符右侧类型的强制转换,如果成功了就返回true;失败了就返回false

但是我们在写代码的过程中,频繁的使用instanceofgetClass()是一种不好的编程习惯,我们除了在重写.equals()方法的时候,尽量不使用这两种形式。

The Object contract(对象契约)

在上面的讨论中我们队与等价关系的定义阐述的已经比较清晰了,等价关系是自反传递对称的。在讨论这个关系的时候有几个点需要注意一下:

  • 除非对象被修改过,否则多次调用.equals()方法得到的结果应该是一样的
  • 定义相等的对象,其.hashCode()方法返回的结果必须是一样的

因为这几个性质,可以通过是否为等价关系来检验你写的.equals()方法是否正确。你可能会想,如果我定义一个等价关系,定义的方法是两个对象的.hashCode()方法返回值的差值在一定范围内就判定为等价,这样可以吗?答案是否定的

举一个简单的例子,如果两个数的差值小于5,那我认为两个Integer相等的话,就会出现0和3等价,3和6等价,但是0和6并不等价的情况。导致这问题的原因是它破坏了哈希表

哈希表实现了键-值之间的映射,它的实现方式是使用一个数组,根据对象的hashcode来决定它将被放在哪里。因此,哈希表在工作的时候,他不是简简单单维护一个值,而是掌握了一组值,我们称之为哈希桶

但是哈希桶的大小是有限的,两个不同对象的哈希值可能相同,这时就发生了冲突,进一步会通过散列技术来解决冲突,而你可以使用.equals()方法来进一步判断两个对象的等价性。如下图所示:

 

hashCode()方法注意事项

因为我们要求等价的对象的.hashCode()返回值相同,因此就是需要我们使用重写.hashCode()方法。那么怎么么样进行重写呢?

最简单的方法就是我们让所有等价的对象得hashCode相同就可以了,非常好实现但是会降低hashTable的效率。另一种比较常用的有效的方法是.euqlas()方法中使用到的计算信息进行组合,拼出新的hashCode

 

Equality of Mutable Types

关于可变类型的等价性我们从下面两个角度来进行考虑,也是前面提及过的两个角度:

  • 观察等价性:再不改变状态的情况下,如果两个对象通过Observers看到的东西没有区别则满足观察等价性
  • 行为等价性:调用两个可变对象的方法,如果通过方法看不出来区别就满足行为等价性

而对于可变对象来说,我们一般更注意他的观察等价性,在Java语言中也是这么进行处理的。例如两个List中包含相同顺序的元素则.equals()方法返回true,但是不排除个别可变类型探究其行为等价性的情况。

但是在有些时候,观察等价性可能导致bug的出现,甚至可能破坏RI,我们看下面的例子:

List<String> list = new ArrayLList<>();
List.add("a");

Set<List<String>> set = new HashSet<List<String>>();
set.add(list);
assertEquals(true, set.contains(list));
list.add("GoodBye!");
assertEquals(false, set.contains(list));

我们发现在对list进行了操作之后set中竟然找不到我们之前存入的list了!这是为什么呢?原因是我们进行的对可变类型的改变影响了equals()和hashCode()的结果我们在进行操作的时候,对象的hashCode发生了改变,但是HashSet却没有更新其在哈希桶中的位置,所以我们在查找的时候找不到元素。

所以!如果某个mutable的对象包含在Set集合中,当其发生改变之后,集合类的行为不确定,请务必小心!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值