在很多场景下,需要判定两个对象是否 “相等” ,例如:判断某个 Collection中是否包含特定元素。 ==和equals()有和区别?如何为自定 义ADT正确实现equals()?
什么是等价性?为什么要讨论等价性?
ADT上的相等操作
ADT是通过创建以操作为特征的类型而不是其表示的数据抽象。
对于抽象数据类型,抽象函数(AF)解释了如何将具体表示值解释为抽象类型的值,并且我们看到了抽象函数的选择如何决定如何编写实现每个ADT操作的代码。
抽象函数(AF)提供了一种方法来清晰地定义ADT上的相等操作。
数据类型中值的相等性?
在物质世界中,每个物体都是不同的 - 即使两个雪花的区别仅仅是它们在太空中的位置,在某种程度上,即使是两个雪花也是不同的。
所以两个实体对象永远不会真正“相等”。 他们只有相似的程度。
然而,在人类语言的世界中,在数学概念的世界中,对同一事物可以有多个名称。
- 当两个表达式表示相同的事物时,很自然地:1 + 2,√9和3是同一个理想数学值的替代表达式。
三种等价性的方式
使用AF或使用关系
使用抽象函数。 回想一下抽象函数f:R→A将数据类型的具体实例映射到它们相应的抽象值。 为了使用f作为等价性的定义,我们说当且仅当f(a)= f(b)时等于b。
使用关系。 等价关系是E⊆T x T,即:
- 自反:E(t,t)∀t∈T
- 对称:E(t,u)⇒E(u,t)
- 传递:E(t,u)∧E(u,v)⇒E(t,v)
- 用E作为等价性的定义,当且仅当E(a,b)时,我们会说a等于b。
等价关系:自反,对称,传递
这两个概念是等价的。
- 等价关系导致抽象函数(关系分区T,因此f将每个元素映射到其分区类)。
- 抽象函数引发的关系是等价关系。
使用观察
我们可以谈论抽象价值之间的等价性的第三种方式就是外部人(客户)可以观察他们的情况
使用观察。 我们可以说,当两个对象无法通过观察进行区分时,这两个对象是相同的 - 我们可以应用的每个操作对两个对象都产生相同的结果。站在外部观察者角度
就ADT而言,“观察”意味着调用对象的操作。 所以当且仅当通过调用抽象数据类型的任何操作不能区分它们时,两个对象是相等的。
==与equals()
Java有两种不同的操作,用于测试相等性,具有不同的语义。
- ==运算符比较引用。
它测试引用等价性。 如果它们指向内存中的相同存储,则两个引用是==。 就快照图而言,如果它们的箭头指向相同的对象气泡,则两个引用是==。
- equals()操作比较对象内容
换句话说,对象等价性。
必须为每个抽象数据类型适当地定义equals操作。在自定义ADT时,需要重写对象的equals()方法
- 当我们定义一个新的数据类型时,我们有责任决定数据类型值的对象相等是什么意思,并适当地实现equals()操作。
==运算符与equals方法
对于基本数据类型,您必须使用==对基本数据类型,使用==判定相等
对于对象引用类型对象类型,使用equals()
- ==运算符提供身份语义如果用==,是在判断两个对象身份标识ID是否相等(指向内存里的同一段空间)
- 完全由Object.equals实现
- 即使Object.equals已被覆盖,这很少是你想要的!
- 你应该(几乎)总是使用.equals
重写方法的提示
如果你想覆盖一个方法:
- 确保签名匹配
- 使用@Override编译器有你的背部
- 复制粘贴声明(或让IDE为你做)
不可变类型的等价性
equals()方法由Object定义,其默认含义与引用相等相同。在对象中实现的缺省equals()方法是在判断引用等价性
对于不可变的数据类型,这几乎总是错误的。
我们必须重写equals()方法,将其替换为我们自己的实现。
重写与重载
在方法签名中犯一个错误很容易,并且当您打算覆盖它时重载一个方法。
只要你的意图是在你的超类中重写一个方法,就应该使用Java的批注@Override。
通过这个注解,Java编译器将检查超类中是否存在具有相同签名的方法,如果签名中出现错误,则会给出编译器错误。
instanceof
instanceof运算符测试对象是否是特定类型的实例。
使用instanceof是动态类型检查,而不是静态类型检查。
一般来说,在面向对象编程中使用instanceof是一种陋习。 除了实施等价性之外,任何地方都应该禁止。
这种禁止还包括其他检查对象运行时类型的方法。
Equivalence Relation(等价关系)
ADT是对数据的抽象,体现为一组对数据的操作
AF为抽象函数:内部表示--> 抽象表示
我们可以根据抽象函数AF来定义ADT的等价操作:检查抽象函数是否满足等价关系:自反、对称、 传递。
Equality of Immutable Types
a和b是等价的,如果对于抽象函数f(x)有:f(a) =f(b)
站在观察者的角度:对于两个对象,调用任何相同的操作,都会得到相同的结果,则认为这两个对象是等价的。
例:对于集合{1,2}和{2,1},有操作 cardinality |...|和membership ∈
对于ADT中所有的方法,我们调用任何方法都不能区别。(如果除了构造方法没有别的方法了,那么我们就不可以通过这一条来判断等价)
例:只有一个构造方法,那么我们就需要通过AF来判断是否等价
Alphabetic case(字母大小写)、non-letters(非字母)
== 和 equal()
==:对于基本数据类型,判断的是值是否相同。对于引用数据类型,判断的是地址是否相同(是否指向内存中同一块地址)
equal():判断的是对象的等价性。当我们自定义ADT时候,需要重写equals方法。
重写需要@Override注解,不要重载!
例:
为什么d1.equals(d2)为
true而d1.equals(o2)为false呐?
因为在Duration中,实际上有两个equals方法
一个为我们上面新加的,另一个为Object类中的equals方法(所有的类均为Obejct的子类)
由于静态类型检查,d1.equals(d2)调用的是我们上面新添加的方法
d1.equals(o2)会因为o2为Object类而调用Object类中的equals方法,实则还是判断地址是否相同。
instanceof关键字:java中的一个双目运算符,可以判断一个对象是否为一个类(或接口、抽象类、父类)的实例,为动态检查
Obj instanceof Class :判断Obj是不是xxx的一个实例
使用:1.声明一个Class类的对象,判断obj是否为Calss的实例对象
2.声明一个Class接口实现类的对象,判断obj是否为Class接口实现类的实例对象
ArrayList arrayList = new ArrayList();
arrayList instanceof List; //为true
3.obj为Class的直接或者间接子类
我们有一个父类Person和继承Person的类Man
注意:obj必须为引用类型,不能为基本类型,否则编译就不会通过
平常我们应该避免使用instanceof,除了再重写equals方法时候。
The Object contract
对于equals(),我们必须遵守:
(1).必须符合等价关系
(2).除非对象被修改,否则多次调用equals应该得到同样的结果
(3).“相等的对象”其hashcode()结果也必须一致
Equality of Mutable Types
1.观察等价性:在不改变状态的情况下,两个mutable对象是否看起来一致
2.行为等价性:调用对象的任何方法都展现出一致的结果
往往我们更倾向于实现严格的观察等价性。
对于immutable,上述两者是等价的,因为没有 mutator方法
注意:如果某个mutable的对象包含在HashSet集合类中,当其发生改变后,集合类的行为不确定。
例:这是为什么?
一开始,我们的List集合里面只有一个"a",其hashCode我们假设为a
那么会根据其哈希值将其放入一个哈希桶。当list集合添加了一个“b”,那么他的哈希值会改变,不再是a了,但set集合不会因此改变list的哈希桶位置,所以查找就永远不会找到它了。
总结:对于immutable类型,我们要重写equals和hashcode方法
对于mutable类型,我们不应该重写equals和hashcode,但是java在其集合中并没有遵循这一规则,导致我们上述问题的出现。
Autoboxing and equality
但是x == y 为false(注意Integer的缓存机制-128-127不适用new Integer的情况)
例:输出结果为false
首先put的时候,会把int的130转换为Integer的130,注意这里因为130不在缓存的范围内,所以a和b放入的不是一个Integer对象,所以两者地址不同,返回false
上述这种情况就是true,因为126在缓存机制的范围之内,所以都指向了同一片内存地址
总结
等价性是实现抽象数据类型(ADT)的一部分。
- 等价性应该是一种等价关系(反身,对称,传递)。
- 相等和散列码必须相互一致,以便使用散列表(如HashSet和HashMap)的数据结构能够正常工作。
- 抽象函数是不变数据类型中等式的基础。
- 引用等价性是可变数据类型中等价性的基础; 这是确保随时间的一致性并避免破坏散列表的不变式的唯一方法。
减少错误保证安全
- 使用集合数据类型(如集合和地图)需要正确实现相等和散列码。 编写测试也是非常理想的。 由于Java中的每个对象都继承了Object实现,所以不可变类型必须重写它们。
容易明白
- 读过我们规范的客户和其他程序员会希望我们的类型实现适当的等价性操作,如果我们不这样做,会感到惊讶和困惑。
准备好改变
- 为不可变类型正确实施的等价性将参考等价性与抽象价值的等价性分开,从客户身上隐藏我们是否共享价值的决定。 选择可变类型的行为而不是观察等价性有助于避免意外的别名错误。