大纲
- 站在观察者角度,利用AF,定义不可变对象之间的等价关系
- 引用等价性和对象等价性
- 可变数据类型的观察等价性和行为等价性
- 理解Object的契约,正确实现等价关系判定
在很多场景下,需要判定两个对象是否“相等”,例如:判断某个Collection中是否包含特定元素。
1.等价关系
ADT是对数据的抽象,体现为一组对数据的操作
抽象函数AF:内部表示 抽象表示
基于抽象函数AF定义ADT的等价操作
2 Equality of Immutable Types
利用AF定义等价性:
- a equals b if and only if f(a)=f(b). AF映射到同样的结果,则等价
用等价关系引入抽象函数,用抽象函数定义的关系是等价关系
用观察法定义等价性
我们可以应用的每个操作都会对这两个对象产生相同的结果。
站在外部观察者角度:对两个对象调用任何相同的操作,都会得到相同的结果,则认为这两个对象是等价的。反之亦然!
3.== vs equal
= = 引用等价性
equals 对象等价性
在自定义时,需重写equals
= = 对基本数据类型,使用==判定相等
对对象类型,使用equals()
如果用 = =,是在判断两个对象
身份标识 ID是否相等(指向内存里的同一段空间)
4 Implementing equals()
equal()方法由对象定义,其默认实现是这样的
在Object中实现的缺省equals()是在判断引用等价性
Java编译器使用参数的编译时类型在重载操作之间进行选择。静态类型检查
一个更好地实现equals的方法:
instanceof:
- 运算符instanceof测试对象是否为特定类型的实例。
- 使用的instanceof是动态类型检查,而不是静态类型检查。
- 一般来说,在面向对象编程中使用instanceof是一种问题。除了实现平等之外,应该在任何地方都不允许使用它。
使用多态性来避免使用instanceof
5 The Object contract
等价关系:自反、传递、对称
除非对象被修改了,否则调用多次equals应同样的结果
如果x不为空,x.equals(null)应返回false
“相等”的对象,其hashCode()的结果必须一致
equals方法实现了等价性关系
Reflexive – every object is equal to itself
Symmetric – if a.equals(b) then b.equals(a)
Transitive – if a.equals(b) and b.equals©, then a.equals©
Consistent– equal objects stay equal unless mutated
“Non-null” – a.equals(null) returns false
这些因素结合在一起确保equals是所有对象的全局等价关系
用“是否为等价关系”检验你的equals()是否正确
违反等价关系:
违反哈希表:
哈希表是映射的一种表示形式:将键映射到值的抽象数据类型。
哈希表的rep不变量包含一个基本约束,即键位于由其哈希代码确定的槽中。
- hashcode的设计使得密钥将均匀地分布在索引上。
- 但有时会发生冲突,两个键放在同一索引中。
- 因此,哈希表不是在索引中保存单个值,而是实际保存一个键/值对列表,通常称为 哈希桶 。
- 一个键/值对在Java中实现为一个包含两个字段的对象。
- 插入时,将一对添加到由哈希代码确定的数组槽中的列表中。
在应用程序执行期间,每当对同一对象多次调用它时,hashCode方法必须始终返回相同的整数,假设不修改在该对象的等比较中使用的信息。
从一个应用程序的一个执行到同一应用程序的另一个执行,这个整数不需要保持一致。
== 等价的对象必须有相同的hashCode ==
程序员应该知道,为不相等的对象生成不同的整数结果可能会提高哈希表的性能,不相等的对象,也可以映射为同样的hashCode,但性能会变差。
为什么对象契约要求相同的对象具有相同的哈希码?
如果两个相等的对象具有不同的哈希码,则它们可能被放置在不同的插槽中。
因此,如果尝试使用与插入值相同的键来查找值,则查找可能会失败。
对象的默认hashCode()实现与其默认值等于()相一致:
Overriding hashCode()
一个简单而有效的方法来确保契约得到满足,就是hashCode总是返回一些常量值,所以每个对象的hash码都是相同的。
这满足了对象契约,但会对性能产生灾难性的影响,因为每个键都将存储在同一个槽中,并且每个查找都将退化为沿长列表的线性搜索。
标准是为用于确定相等性的对象的每个组件计算一个哈希码(通常通过调用每个组件的hashCode方法),然后将它们组合起来,抛出一些算术运算。
. Java的最新版本现在有了一个实用方法Objects.hash(),使得实现包含多个字段的哈希代码变得更容易。
Always override hashCode() when you override equals()
除非你能保证你的ADT不会被放入到Hash类型的集合类中 。
hashCode Override实例
public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
@Override
public int hashCode() {
int result=17;
result+=31*result+areaCode;//constant must be odd
result==31*result +prefix;
result+=31*result+lineNumber;
return result;
}
...
}
替代的hashCode覆盖:
public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
@Override
public int hashCode() {
short[] hashArray = {areaCode, prefix, lineNumber};
return Arrays.hashCode(hashArray);
}
...
}
6 Equality of Mutable Types
观察等价性:在不改变状态的情况下,两个mutable对象是否看起来一致
行为等价性:调用对象的任何方法都展示出一致的结果。
对可变类型来说,往往倾向于实现严格的观察等价性。
但在有些时候,观察等价性可能导致bug,甚至可能破坏RI。
如果将可变对象用作集合元素,则必须非常小心。
如果某个mutable的对象包含在Set。
List<String> list=new ArrayList<>();
list.add("a");
Set<List<String>> set=new HashSet<>();
set.add(list);
System.out.println(set.contains(list));
list.add("goodbye");
System.out.println(set.contains(list));
for(List<String> l:set)
{
System.out.println(set.contains(l));
}
结果为:
true
false
false
集合类中,当其发生改变后,集合类的行为不确定 务必小心。
在JDK中,不同的mutable类使用不同的等价性标准。
对可变类型,实现行为等价性即可。
也就是说,只有指向同样内存空间的objects,才是相等的。
所以对可变类型来说,无需重写这两个函数,直接继承Object的两个方法即可。
如果一定要判断两个可变对象看起来是否一致,最好定义一个新的方法。e.g. similar()。
7 Autoboxing and Equality
x==y false 引用不相等
int(x)==int(y) True
Map<String, Integer> a = new HashMap<>();
Map<String, Integer> b = new HashMap<>();
a.put("c", 1);
b.put("c", 1);
System.out.println (a.get("c")== b.get("c"));
Integer x = new Integer(2);
Integer y = new Integer(2);
System.out.println(x==y);
Integer x1 = 2;
Integer y1 = 2;
System.out.println(x1==y1);
结果:
true
false
true