等价关系
ADT是对数据的抽象,体现为一组对数据的操作
对于抽象数据类型,抽象函数(AF)解释了如何将具体的表示值解释为抽象类型的值,我们看到了抽象函数的选择如何决定如何编写实现每个ADT操作的代码。
抽象函数(AF)提供了一种清晰地定义ADT上的等价操作的方法。
数据类型的等价性:
现实中的每个对象实体都是独特的,因此,两个物理对象永远不会真正“平等”;它们只有一定的相似程度。然而,在人类语言世界中,在数学概念世界中,你可以为同一件东西有多个名称。所以很自然会问当两个表达式代表相同的东西: 1+2,√9和3是相同理想数学值的替代表达式。
等价关系:自反、对称、传递
对于像==或equals()
这样的布尔值二进制操作,等价值E是该操作返回true的成对(x,y)的集合。
不变类型的等价性
使用AF来定义等价关系:
为了使用f作为等式的定义,我们假设a等于b当且仅当f (a)=f (b)。即AF映射到同样的结果,则等价。
由抽象函数引起的关系是一种等价关系。
使用观察来定义等价关系:
我们谈论抽象价值之间平等的另一种方式是,客户端可以观察到它们的情况。
使用观察方法。当两个对象不能通过观察来区分时,我们可以说它们是相等的——我们可以应用的每一个操作都会对两个对象产生相同的结果。
就ADT而言,“观察”是指调用对对象的操作。因此,当且仅当它们不能通过调用抽象数据类型的任何操作来区分时,两个对象才相等。
== vs. equals()
Java有两个不同的操作来测试相等性,它们具有不同的语义。
-
引用等价性。
==
操作符比较引用。如果两个引用指向内存中的相同存储器,则它们是==
。在快照图中,如果两个引用的箭头指向同一个对象气泡,则这两个引用为==
。 -
对象等价性。
equals()
操作会比较对象的内容——换句话说,就是对象的相等性。
在自定义ADT时,需要重写Object的equals()
。
当我们定义一个新的数据类型时,我们的责任是决定对象相等对于数据类型的值意味着什么,并适当地实现equals()
操作。
== 操作符 vs. equals()方法
==
对于基本数据类型,使用==
判定相等- 对于对象类型,使用
equals()
判定相等
如果用==
,是在判断两个对象身份标识 ID是否相等(指向内存里的同一段空间)。
对于对象类型,我们需要重写父类的equals()
方法。
如果您想覆盖一个方法:
- 确保签名匹配
- 使用
@Override
- 执行复制和粘贴spec(或让IDE为您执行)
实现equals()
不变类型的平等
equals()
方法由Object定义,其默认实现如下所示:
在Object中实现的缺省equals()是在判断引用等价性,这通常不是程序员所期望的,因此需要重写方法。
在重写时需要注意方法签名中参数列表是一个Object类型,不应该更改原来的参数列表。
使用 @Override
,Java编译器将检查超类中是否实际存在具有相同签名的方法,如果您在签名中犯了错误,则会给您一个编译器错误。
标准的写法(例):
@Override
public boolean equals(Object o) {
if (!(o instanceof PhoneNumber)) // Does null check
return false;
PhoneNumber pn = (PhoneNumber) o;
return pn.lineNumber == lineNumber
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
instanceof
instanceof
操作符将测试对象是否为特定类型的实例。- 使用
instanceof
操作符是动态类型检查,而不是静态类型检查。 - 一般来说,在面向对象编程中使用实例是一件坏事。除了实现
equals()
之外,任何地方都不允许它。- 这一禁令还包括检查对象的运行时类型的其他方法。例如,getClass()也不允许使用。(违反OOP原则)
建议用(类型)多态取代instanceof
。
对象的“契约”
对象中equals()
的契约
当重写equals()
方法时,必须遵守其契约:
- 满足自反、传递、对称的等价关系
- 除非对象被修改了,否则调用多次
equals()
应同样的结果 - “相等”的对象,其
hashCode()
的结果必须一致
对于任何非空引用值x,x.equals(null)
必须返回false。
equals是一个在所有对象上的全局等价关系,用“是否为等价关系”检验你的equals()
是否正确。
对象中hashCode()
的契约
每当在应用程序执行过程中对同一对象进行多次调用时,hashCode方法必须一致地返回相同的整数,只要不修改在该对象的平等比较中使用的信息。
如果两个对象根据 equals(Object)
方法相等,那么对这两个对象分别调用hashCode()
方法必须产生相同的整数结果。
等价的对象必须有相同的hashCode。
不相等的对象,也可以映射为同样的hashCode,但性能会变差。
- 相等的对象必须有相等的哈希代码
- 如果重写
equals()
,必须也重写hashCode()
- 如果重写
-
不相等的对象应该有不同的哈希代码
- 在构造它时考虑到所有的值字段
-
除非对象突变,哈希代码必须不改变
对象的默认hashCode()
实现:
重写hashCode()
确保满足契约的一个简单而粗暴的方法是,hashCode总是返回一些常量值,因此每个对象的哈希代码都是相同的。
这满足对象契约,但它会产生灾难性的性能影响,因为每个键都将存储在同一个插槽中,并且每个查找都将退化为沿着一个长列表的线性搜索。
标准方法是为用于确定相等式的对象的每个组件计算哈希代码(通常通过调用每个组件的hash code方法),然后将这些组件组合起来,加入一些算术运算
Java的最新版本现在有了一个实用程序方法对象。Objects.hash()
,它使实现涉及多个字段的哈希代码变得更容易。
请注意,如果您根本不重写hashCode()
,那么您将从Object中得到一个,它是基于对象的地址的。但是需要注意保证两个equal的objects,一定要有同样的hashcode.
标准的写法(例):
使用字段构造哈希值:
@Override
public int hashCode() {
int result = 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() {
short[] hashArray = {areaCode, prefix, lineNumber};
return Arrays.hashCode(hashArray);
}
可变类型的等价性
平等:当两个物体不能通过观察来区分时,它们是相等的。
对于可变对象,有两种方法来解释这一点:
- 观察等价性:在不改变状态的情况下,两个可变对象是否看起来一致
- 行为等价性:调用对象的任何方法都展示出一致的结果
注意:对于不可变对象,观察性和行为性相等是相同的,因为没有任何mutator方法。
对可变类型来说,往往倾向于实现严格的观察等价性。
-
Java对其大多数可变数据类型(如集合)使用观察性相等,但其他可变数据类(如StringBuilder)使用行为相等。
-
如果两个不同的List对象包含相同的元素序列,则
equals()
表示它们相等。
但在有些时候,观察等价性可能导致bug,甚至可能破坏RI:
例:当把List存入HashSet中后,改变List的内容,从而改变了List的哈希值,导致判断HashSet中是否包含List的返回为False。
如果某个mutable的对象包含在HashSet集合类中,当其发生改变后,集合类的行为不确定 ,务必小心!
在JDK中,不同的mutable类使用不同的等价性标准:
-
观察等价性
- Date类的
equals()
- List类的
equals()
- Date类的
-
行为等价性
- StringBuilder类的
equals()
- StringBuilder类的
对可变类型,实现行为等价性即可。也就是说,只有指向同样内存空间的objects,才是相等的。所以对可变类型来说,无需重写这两个函数,直接继承Object的两个方法即可。
如果一定要判断两个可变对象看起来是否一致,最好定义一个新的方法。
总结:
- 不可变类型:重写
equals()
和hashCode()
- 可变类型:不该重写
equals()
和hashCode()
clone() in Object
clone()
将创建并返回此对象的一个副本。“复制”的确切含义可能取决于对象的类别。
一般的意图是,对于任何对象x:
从这些Clone()
的契约中无法确保是深度复制!
自动包装和等价性
原始类型及其对象类型的等价物,例如,int和Integer。
如果创建两个具有相同值的Integer对象,它们将满足equals()
,但是不满足==
。
但是强制转换为int后,就可以满足==
。
Integer x = new Integer(3);
Integer y = new Integer(3);
System.out.println(x.equals(y)); //true
System.out.println(x == y); //false
System.out.println((int)x == (int)y); //true
但是,对于数字在-128到127间,则相等!(数字存于缓冲区,直接从缓冲区中取出,两次地址相同)
例:
Integer x = new Integer(2);
Integer y = new Integer(2);
System.out.println(x==y); //false
Integer x = 2;
Integer y = 2;
System.out.println(x==y); //true