8 ADT和OOP中的“等价性”

在很多场景下,需要判定两个对象是否 “相等”,例如:判断某个 Collection中是否包含特定元素。 ==和equals()有和区别?如何为自定 义ADT正确实现equals()?

1 等价关系

对于像 == 或equals()这样的布尔值二元运算,等价E是一组对(x,y),对于这些对,运算返回true。
因此对于==,这些属性也可以写为:
自反:t == t,∀t∈T
对称:t == u⇒ u == t
传递的:t == u∧ u == v⇒ t == v
对于equals()方法也是如此。

2评判平等的三种方式

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

对于ADT,抽象函数(AF)解释了如何将具体表示值解释为抽象类型的值,并且我们看到了抽象函数的选择如何决定如何编写实现每个ADT操作的代码

抽象函数(AF)提供了一种在ADT上清晰定义相等操作的方法。

1.用AF来定义等价
抽象函数AF:R→ 将数据类型的具体实例映射到相应的抽象值。
为了使用AF作为等式的定义,我们说**a等于b当且仅当AF(a)=AF(b)**。
使用AF和使用关系定义等式是等效的。
等价关系产生了一个抽象函数(该关系将T划分,因此AF将每个元素映射到其划分类)。

2.用观察者来定义等价

站在外部观察者角度:对两个对象调用任何相同的操作得到相同的结果,则认为这

两个对象是等价的,反之亦然!

当观察无法区分两个对象时,它们是相等的——我们可以应用的每个操作都会对两个对象产生相同的结果。
考虑集合表达式{1,2}和{2,1}。使用集合、基数|…|和成员资格可用的观察者操作∈, 这些表达式无法区分:
•|{1,2}|=2和|{2,1}|=2
• 1 ∈ {1,2}为真,1∈ {2,1}为真
• 2 ∈ {1,2}为真,2∈ {2,1}为真
• 3 ∈ {1,2}为false,3∈ {2,1}为false
就ADT而言,“观察”是指调用对象上的操作。因此,当且仅当不能通过调用抽象数据类型的任何操作来区分这两个对象时,这两个对象才相等

在这里插入图片描述

(利用AF判断,abc,aBc,1a2b3c等价)
在这里插入图片描述

3.用等价关系判断等价(见1 等价关系)

3.==与equals

Java有两种不同的操作来测试相等性,它们具有不同的语义。

对基本数据类型,使用 == 判定相等(值相等则相等),对对象类型,使用equals()

==用于判断基本数据类型时,比较的是值; == 用于判断引用数据类型时,比较的是地址

A.运算符 == 比较引用。它测试引用相等性。如果两个引用指向内存中的相同存储,则它们是 引用相等。在快照图中,如果两个引用的箭头指向同一个对象气泡,则它们的引用为 == 。Object.equals使用==实现 判断
B.equals()操作比较对象内容–换句话说,对象相等
在自定义ADT时,需要根据对“等价”的要求,决定是否重写Object的equals()

equals应该总是使用equals()判断相等(如果判断逻辑是内存地址相等,则不需要重写Object.equals(),此时equals()等价于==;如果判断逻辑是特定的,则需要重写Object.equals(),则equals()调用的是期望的判断逻辑。无论哪种情况,使用equals()都能达到期望的目的),不要用 == 判断对象相等

4 实现equals

//equals默认实现
public class Object {
public boolean equals(object that) 
{
return this == that;
}
}

在0bject中实现的默认equals( )是在判断引用等价性,这通常不是
程序员所期望的,因此,需要重写。

1.例子:

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

//运行下面的测试代码
Duration d1 = new Duration (1, 2); 
Duration d2 = new Duration (1, 2); 
Object o2 = d2; 
d1.equals(d2) → true 
d1.equals(o2) → false

在这里插入图片描述

public class Duration extends Object {
// explicit method that we declared: 
public boolean equals(Duration that) 
{ return this.getLength() == that.getLength(); }
// implicit method inherited from Object: 
public boolean equals(Object that) 
{ return this == that; } }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xwbjE9Pg-1655125431219)(img/image-20220606100017261.png)]

@Override 
public boolean equals(Object that) 
{ return that instanceof Duration && this.sameValue((Duration)that); }
// returns true iff this and that represent the same abstract value 
private boolean sameValue(Duration that) { return this.getLength() == that.getLength(); }

2.instanceof

判断某个对象是不是特定类型(或其子类型),属于动态类型检查,不是静态类型检查

除了用于实现equals()方法,尽可能避免使用instanceof和getClass() !

利用多态来代替instanceof

instanceof相当于判断当前对象A能不能转换成为类型B,java里面上转型是安全的.

即用父类类型的引用替代子类类型的引用,去指向子类型,用接口类型的引用指向具体实现这个接口的类型

记忆: 父类、超类对象 instanceof 父类或子类

img

5.对象契约

1.重写equals规则

1.重写equals()方法时,必须遵守其一般约定
2.equals必须定义一个等价关系,即一个自反、对称和传递的关系;
3.equals必须一致:如果没有修改对象上equals比较中使用的信息,则对该方法的重复调用必须产生相同的结果;
5.对于非null引用x,x.equals(null)应返回false;(如果不是,则违反了对称性)

6.hashCode()必须为equals方法认为相等的两个对象生成相同的结果。

2.equals方法实现了一个等价关系:
自反:对于任何非空引用值x,x。equals(x)必须返回true。
对称:对于任何非空引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
可传递:对于任何非空的引用值x、y、z,如果x.equals(y)返回true和y。equals(z)返回true,然后x。equals(z)mus返回true。
一致性:对于任何非空引用值x和y,x.equals(y)的多次调用始终返回true或false,前提是没有修改对象上equals比较中使用的信息。
equals是所有对象上的全局等价关系,用是否为等价关系判断equals是否正确。

3.破坏等价关系

注:下面的相等操作实际违反了传递性

如果x.equals(null)返回true,equals将违反对称性。而代码里的that instanceof Duration可以让x.equals(null)返回false

public class Duration 
{ private final int mins; private final int secs;
// Rep invariant: 
// mins >= 0, secs >= 0 
// Abstraction function: 
// AF(min, secs) = the 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; }

@Override 
public boolean equals(Object that) 
{ return that instanceof Duration && this.sameValue((Duration)that); }
private static final int CLOCK_SKEW = 5; // seconds
// returns true iff this and that represent the same abstract value within a clock- skew tolerance 
private boolean sameValue(Duration that) 
{ return Math.abs(this.getLength() - that.getLength()) <= CLOCK_SKEW; } }

//假设创建这些Duration对象
Duration d_0_60 = new Duration(0, 60); 
Duration d_1_00 = new Duration(1, 0); 
Duration d_0_57 = new Duration(0, 57); 
Duration d_1_03 = new Duration(1, 3);

在这里插入图片描述

4.破坏哈希表

​ 两个常见的聚合类型HashSet 和HashMap 就用到了哈希表的数据结构,并且依赖hashCode保存集合中的对象以及产生合适的键(key)。
​ 一个哈希表表示的是一种映射:从键值映射到值的抽象数据类型。哈希表提供了常数级别的查找,所以它通常比数组或者列表的性能要好。键不一定是有序的,也不一定有 什么特别的属性,除了类型必须提供equals 和hashCode两个方法。
​ 哈希表是怎么工作的呢?它包含了一个初始化的数组,其大小是我们设计好的。当一个键值对准备插入时,我们通过hashcode计算这个键,产生一个索引,它在我们数组大小的范围内(例如取模运算)。最后我们将值插入到数组索引对应的位置。
哈希表的一个基本不变量就是键必须在hashcode规定的范围内。
​ Hashcode最好被设计为键计算后的索引应该平滑、均匀的分布在所有范围内。但是偶尔冲突也会发生,例如两个键计算出了同样的索引。因此哈希表通常存储的是一个键值对的列表而非一个单个的值,这通常被称为哈希桶(hash bucket)。而在Java中, 键值对就是一个有 着两个域的对象。当插入时,你只要向计算出的索引位置插入一个键值对。当查找时,你先根据键哈希出对应的索引,然后在索引对应的位置找到键值对列表,最后在这个列表中查找你的键。
​ 现在你应该知道了为什么object的规格说明要求相等的对象必须有同样的hashcode。如果两个相等的对象hashcode不同,那么它们在聚合类存储的时候位置也就不一样一-如果你存入了一个对象,然后查找一个相等的对象,就可能在错误的索引处进行查找,也就会得到错误的结果。
object默认的hashCode()实现和默认的equals() 保持一致:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UfxmSsAa-1655125431220)(img/image-20220606105718498.png)]

程序中多次调用同一对象的 hashCode方法,都要返回相同值

但从应用程序的一次执行到同一应用程序的另一次执行,返回值不需要保持一致。

等价对象,hashcode值必须一致。

不相等的对象,也可映射为相同hashcode值,但性能会变差

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6 可变类型的相等

​ 之前我们已经对不可变对象的相等性进行了讨论,那么可变类型对象会是怎样呢?
回忆之前我们对于相等的定义,即它们不能被使用者观察出来不同。而对于可变对象来说,它们多了一种新的可能:通过在观察前调用改造者,我们可以改变其内部的状态,从而观察出不同的结果。
所以让我们重新定义两种相等:
●观察相等:两个索引在不改变各自对象状态的前提下不能被区分。例如,只调用观察者、生产者、创建者。它测试的是这两个索引在当前程序状态下“看起来"相等
●行为相等:两个所以在任何代码的情况下都不能被区分,即使有一个对象调用了改造者。它测试的是两个对象是否会在未来所有的状态下“行为”相等

对于不可变对象,观察相等和行为相等是完全等价的,因为它们没有改造者改变对象内部的状态。
对于可变对象,Java通常实现的是观察相等。例如两个不同的List对象包含相同的序列元素,那么equals()操作就会返回真。

但是使用观察相等会带来隐秘的bug,并且也会让我们很容易的破坏聚合类型的表示不变量RI。假设我们现在有1个List,然后我们将其存入1个Set :

List<String> list = new ArrayList<>(); 
list.add("a"); 
Set<List<String>> set = new HashSet<List<String>>(); 
set.add(list);

在这里插入图片描述

我们可以检查这个集合是否包含我们存入的列表:
set . contains(list)→true
但是如果我们修改这个存入的列表:
list . add( “goodbye”);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B81qRtT5-1655125431222)(img/image-20220606113148503.png)]

它似乎就不在集合中了!
set . contains(list)→false!
事实上,更糟糕的是:当我们(用迭代器)循环遍历这个集合时,我们依然会发现集合存在,但是contains()还是说它不存在!
for (List l : set)

{
set . contains(l)→false!
}
如果1个集合的迭代器和contains()都互相冲突的时候,显然这个集合已经被破坏了。

发生了什么?我们知道 List是一个可变对象,而在Java对可变对象的实现中,改造操作通常都会影响equals()和hashCode() 的结果。所以列表第一次放入 HashSet 的时候,它是存储在这时hashCode() 对应的索引位置。但是后来列表发生了改变,计算 hashCode()会得到不一 样的结果,但是HashSet 对此并不知道,所以我们调用contains时候就会找不到列表。
当equals() 和hashCode()被改动影响的时候,我们就破坏了哈希表利用对象作为键的不变量。

下面是java.util. Set规格说明中的一-段话:

注意:当可变对象作为集合的元素时要特别小心。如果对象内容改变后会影响相等比较而且对象是集合的元素,那么集合的行为是不确定的。

不幸的是,Java库坚持它对可变类型的equals() 的实现,即==聚合类,比如collection使用观察相等,不过也有一些可变类型(例如StringBuilder )使用的是行为相等。==

总结:

对可变类型,实现行为等价性即可
也就是说,只有指向同样内存空间的objects,才是相等的。
所以对可变类型来说,无需重写这两个函数,直接继承object的两个方法即可。
如果一定要判断两个可变对象看起来是否一致,最好定义一个新的方法。

在这里插入图片描述

equals和hashcode总结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BEG4bCGo-1655125431223)(img/image-20220606114341192.png)]

练习:
在这里插入图片描述
在这里插入图片描述

1.以下那些选项在运行过后为真?1,3,4,5,7,8

①b1. count(“a”) == 1 ② b1. count(“b”) == 1
③b2.count(“a”) == 1 ④b2. count(“b”) == 1
⑤b3. count(“a”) == 1 ⑥b3.count(“b”) == 1
⑦b4. count(“a”) == 1 ⑧b4. count(“b”) == 1

2.如果Bag 实现的是行为相等(即Bag为可变类型),以下哪些表达式为真? **(要考虑引用相等)**2,6

3.如果Bag 是Java API的一部分,即它可能实现的是观察相等,以下哪些表达式为真**(需要通过 Observer(count())判断)**? 2,5,6

①b1. equals(b2)
②b1. equals(b3)
③b1. equals(b4)
④b2. equals(b3)
⑤b2. equals(b4)
⑥b3. equals(b1)

clone()创建并返回此对象的副本。
复制的确切含义可能取决于对象的类别。
一般目的是,对于任何对象x:

x.clone()!=x;

x.clone.getClass()==x.getClass

x.clone().eqals(x)

从这些合同里无法确保为深拷贝

浅拷贝与深拷贝

0.直接赋值的方式没有生产新的对象,只是生新增了一个对象引用

1.浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存(分支)。浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。

对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性复制一份给新的对象。因为是两份不同的数据,所以对其中一的对象的成员变量值进行修改,不会影响另一个对象拷贝得到的数据。

对于数据类型是引用类型的成员变量,比如说成员变量是某个数组,某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用指(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。

2.深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象,是“值”而不是“引用”(不是分支)

拷贝第一层级的对象属性或数组元素
递归拷贝所有层级的对象属性和数组元素
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。

在这里插入图片描述

总结:

赋值语句(=):p1与p2的equals()相等,hashCode()相等

浅拷贝:p1与p2的equals()不相等,hashCode()不相等

深拷贝:p1与p2的equals()不相等,hashCode()不相等

这三者中,赋值语句与深拷贝很容易理解,对于浅拷贝,新旧对象共享的是同一块内存,注意:也是有新对象产生的,这就造成hashCode()不相同。(这里容易产生误解,两个对象如果hashCode()相同,两个对象不一定相同,因为有hash冲突存在。)。)

Object类是类结构的根类,其中有一个方法为protected Object clone() throw CloneNotSupportedException,这个方法就是进行的浅拷贝。有了这个浅拷贝的模板,就可以通过调用clone()方法来实现对象的浅拷贝,但是需要注意:

  1. Object类虽然有这个方法,但是这个方法是受保护的(被projected修饰),所以是无法直接使用。
  2. 使用clone方法的类必须实现Cloneable接口,否则会抛出异常CloneNotSupportedException。
  3. 对于以上两点,解决方法是,在使用clone方法的类中重写了clone()方法,通过super.clone()调用Object类中的原clone方法

7 自动装箱与相等

强调:Integer是类,int是基本数据类型!!

在这里插入图片描述

(注意,如果是Integer x=3; Integer y=3; x == y ->return true 对于Integer 必须数值在-128-127直接才能保证上述是true ,这是因为自动装箱规范要求 byte<= 127、char<=127、-128<=short <=127、-128<=int <=127都被包装到固定的对象中(缓存)。如下图所示

注意int没这个限制。

在这里插入图片描述

像下面这个例子,显然new开辟了2个新的空间,自然地址不相同

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4RRKSN7c-1655125431227)(img/image-20220607200138011.png)]

关于“包装类型的equals比较的是值",可以从上面的Integer类的源码看出来。

Object.equals显然比较的是地址,也就是等价于 ==

在这里插入图片描述
在这里插入图片描述

->false,因为Integer间的 == 是引用相等,130超过了我们上述说的范围-128-127

在这里插入图片描述
在这里插入图片描述

->true,因为int间的== 是对象相等

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值