Java程序构造©Day6
April 2nd, 2018
标签(空格分隔): Java程序构造
Equality in ADT and OOP 复写equals()
和hashCode()
- 站在观察者角度 利用AF 定义对象之间的等价关系
- 引用等价性 对象等价性
- 可变数据类型的观察等价性和行为等价性
- 正确实现等价判断
什么是等价性
对于等价性在这里主要是指对象之间的等价性 包括ADT
要基于抽象函数AF来找到等价操作
等价性判断依据
- 可以使用AF 如果AF映射出的结果相同 那么两者等价 即两者具有自反 对称 可传递的性质
- 可以基于观察者的角度给出判断
这里给出一个例子
判断以下对象的等价性
对于任何的两个对象 AF映射出的结果如果相等 那么两者等价
显然 d1 d3 d4等价
== 和 equals()
在java中 == 和 equals是一样的
referential | object | |
---|---|---|
Java | == | equals() |
Obj C | == | isEqual: |
C# | == | Equals() |
Python | is | == |
Javascrip | == | n/a |
Javaequals()
的判断适用于对象 如果使用==
那么判断两者的身份ID是否相同
不可变类型的等价性
重写equals()
Java的等价性判断定义在Object中 但是实现很简陋 多数情况下不适用与自定义类型 所以需要@Override
我们定义一个类型
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
虽然d2 o2指向的对象可能非常相像 但是 我们在上边的定义中重载了equals 那么这个类型中应该有两个判断
public boolean equals(Duration that){
...
}
public boolean equals(Object c){
return this == that;
}
根据传入的类型 传入的o2是一个Object类型的对象 那么Java会自动调用后者进行判断 当然不相等
所以我们采用以下的方式进行重写
@Override
public boolean equals(Object that){
if(!(that instanceof Duration)) return false;
Duration thatDuration = (Duration) that;
return thatDuration.getLength(); == this.getLength();
}
我们推荐的重写方法
public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
@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;
}
...
}
注意 instacnceof()
最好只在类型判断相等时使用 更不要使用getClass()
重写hashCode()
刚刚讲到的等价性判断 涉及到自反 传递 对称
- 自反 对任何非null类
x.equals(x)
必为真 - 对称 对任何非null类
x.equals(y)
为真当且仅当y.equals(x)
- 传递 对任何非null类 如果
x.equals(y)
为真且y.equals(z)
为真 那么x.equals(z)
必为真 - 对任何非null类
x.equals(null)
必为假
在不保证这几类的情况下 equals()存在问题
如果两个object调用了equals() 那么在调用后会调用各自的hashCode()来产生一个数 两者必须相等
所以我们有几个原则
- 相等的对象 重写
equals()
必须重写hashCode()
- 不相等的对象 必须有不相等的hashCode
- hashCode不变 除非对象发生变化
在java中的equals和hashCode的原写法
public class Object{
public boolean equals(Object that){
return this == that;
}
public int hashCode(){
return /* return the memory address of this object */
}
...
}
所以 对于之前给出的Duration的例子是不正确的 破坏了hashCode的原则
我们要保证对于等价的对象 hashCode必须相等
我们有hashCode重写的推荐写法
public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
@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 boolean hashCode(){
return Arrays.hashCode(areaCode, prefix, lineNumber);
}
可变类型等价性
对于等价类型 我们从两点出发 行为等价性 和 观察等价性
行为等价性是对其中AF映射的结果相等 那么判断相等
观察等价性是在不改变状态的情况下 其中的任何类型 都相等
一般对于可变类型 我们倾向于严格的观察等价性 但是可能出现漏洞
List<String> list = new ArrayList<>();
list.add("a");
Set<List<String>> set = new HashSet<>();
set.add(list);
set.contains(list);//true
list.add("b");
set.contains(list);//false
这涉及到equals和hashCode的
对于容器类型 我们偏向于使用观察等价性
对于其他类型 我们倾向于行为等价性
- 对可变类型 实现行为等价性即可
- 只有指向相等的地址 才能相等
- 可变类型无需重写两个方法 直接继承
- 如果一定要判断 新写一个方法