第二章节
这一章关注的是所有对象的通用方法,并且指出了覆盖这些方法所需要的约定和细节
8)覆盖equals时请遵守通用约定
equals约定
我们首先需要我们覆盖equals时所期望的结果:
1. 类的每个实例本质上都是唯一的。
2. 不关心类是否提供了“逻辑相等”的测试功能。
3. 父类已经覆盖了equals,从父类继承过来的行为对于子类也是合适的。
4. 类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。
我们根据上面这四个期望结果,大概可以推导出什么情况下需要覆盖equals
- 父类并没有覆盖equals
- 这个类具有自己特有的“逻辑相等”的概念
我们构想了一个坐标类,符合上面这两条需要覆盖equals的条件。
//com.insanus.Coordinate.java
package com.insanus;
public class Coordinate {
private int x;
private int y;
private int z;
public static class CoordinateBuilder {
private int x;
private int y;
private int z;
CoordinateBuilder(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
public Coordinate getInstance() {
return new Coordinate(this);
}
}
private Coordinate(CoordinateBuilder i) {
this.x = i.x;
this.y = i.y;
this.z = i.z;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (!(obj instanceof Coordinate)||obj == null) {
return false;
} else {
return x == ((Coordinate) obj).getX() && y == ((Coordinate) obj).getY() && z == ((Coordinate) obj).getZ();
}
}
public static void main(String[] args) {
Coordinate t1 = new CoordinateBuilder(20, 21, 22).getInstance();
Coordinate t2 = new CoordinateBuilder(20, 21, 22).getInstance();
Coordinate t3 = new CoordinateBuilder(20, 21, 22).getInstance();
//等价关系
System.out.println("对称性:" + (t1.equals(t2) && t2.equals(t1)));
System.out.println("自反性:" + (t1.equals(t1)));
System.out.println("非空性:" + (!t1.equals(null)));
System.out.format("传递性: 如果存在t1==t2(%s)且t1==t3(%s),则有t2==t3(%s)\n", t1.equals(t2), t1.equals(t3), t2.equals(t3));
}
}/* Output:
对称性:true
自反性:true
非空性:true
传递性: 如果存在t1==t2(true)且t1==t3(true),则有t2==t3(true)
*///:~
我们设计了一个构建器方便日后的扩展,并且按照约定,满足了通用约定
实现高质量equals方法的诀窍
我们并由此得出了实现高质量equals方法的诀窍:
1. 使用==操作符检查“参数是否为这个对象的引用”
2. 使用instance操作符检查“参数是否为正确的类型”
3. 把参数转换成正确的类型
4. 对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配。
5. 当你编写完成equals方法之后,应该问自己三个问题:它是否是对称,传递的,一致的?
* 覆盖equals时总要覆盖hashCode
* 不要企图让equals方法过于智能
* 不要将equals声明中的Object对象替换成其他的类型
9)覆盖equals时总要覆盖hashCode
我们先看看Object对hashCode的约定
hashCode的约定
- 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
- 如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
- 如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能
根据这几条约定,我们为刚才的Coordinate覆盖了hashCode。
我们先添加了一个属性
private volatile int result
volatile
延后定义
@Override
public int hashCode() {
if (result == 0) {
result = 17;
result = 31 * result + x;
result = 31 * result + y;
result = 31 * result + z;
}
return result;
}
编写这种散列函数是个研究课题,最好留给数学家和理论方面的计算机科学家。日常使用利用这种方法就足够了。
10)始终要覆盖toString
我们来看看Object源码中是如何实现toString
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
很明显,我们需要覆盖toString以便用户能够更好理解。
我们为Coordinate覆盖了toString
@Override
public String toString() {
return String.format("x: %d, y: %d, z: %d", x, y, z);
}
11)谨慎地覆盖clone
首先我们来聊聊Cloneable这个接口,Cloneable接口的目的是作为对象的一个minxi接口。如果一个类没有实现该接口,就会返回CloneNotSupportedException异常
clone通用约定
创建并返回此对象的一个副本。“副本”的准确含义可能依赖于对象的类。这样做的目的是,对于任何对象 x,表达式:
x.clone() != x
为 true,表达式:
x.clone().getClass() == x.getClass()
也为 true,但这些并非必须要满足的要求。一般情况下:
x.clone().equals(x)
为 true,但这并非必须要满足的要求
而clone的过程是没有调用构造器,换句话说,clone方法对于只是对于域的软拷贝。clone又是递归的调用父类的clone,所以你要设计一个良好的clone方法时,你还要确保父类都拥有一个设计良好的clone。
更好的解决办法
我们可以提供一个拷贝构造器
public Yum(Yum yum)
或者是拷贝工厂
public static Yum newInstance(Yum yum);
12)考虑实现Comparable接口
Comparable
这个接口的唯一方法是compareTo,而实现了Comparable接口的对象数组排序只需要如此Arrays.sort(a)
compareTo和equals的约定都相差无几,这里举出几个不同点:
1. 无需进行类型检查
2. compareTo比较是顺序比较,不是等同性比较
compareTo约定
比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
实现类必须确保对于所有的 x 和 y 都存在 sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) 的关系。(这意味着如果 y.compareTo(x) 抛出一个异常,则 x.compareTo(y) 也要抛出一个异常。)
实现类还必须确保关系是可传递的:(x.compareTo(y)>0 && y.compareTo(z)>0) 意味着 x.compareTo(z)>0。
最后,实现者必须确保 x.compareTo(y)==0 意味着对于所有的 z,都存在 sgn(x.compareTo(z)) == sgn(y.compareTo(z))。 强烈推荐 (x.compareTo(y)==0) == (x.equals(y)) 这种做法,但并不是 严格要求这样做。一般来说,任何实现 Comparable 接口和违背此条件的类都应该清楚地指出这一事实。推荐如此阐述:“注意:此类具有与 equals 不一致的自然排序。”
在前面的描述中,符号 sgn(expression) 指定 signum 数学函数,该函数根据 expression 的值是负数、零还是正数,分别返回 -1、0 或 1 中的一个值。
我们给出改写过的Coordinate
//com.insanus.Coordinate.java
package com.insanus;
public class Coordinate implements Comparable<Coordinate>{
private int x;
private int y;
private int z;
public static class CoordinateBuilder {
private int x;
private int y;
private int z;
CoordinateBuilder(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
public Coordinate getInstance() {
return new Coordinate(this);
}
}
private Coordinate(CoordinateBuilder i) {
this.x = i.x;
this.y = i.y;
this.z = i.z;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
public double getModel() {
return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2));
}
@Override
public int compareTo(Coordinate o) {
return Double.compare(getModel(), o.getModel());
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (!(obj instanceof Coordinate)||obj == null) {
return false;
} else {
return x == ((Coordinate) obj).getX() && y == ((Coordinate) obj).getY() && z == ((Coordinate) obj).getZ();
}
}
public static void main(String[] args) {
Coordinate a = new CoordinateBuilder(12, 13, 14).getInstance();
Coordinate b = new CoordinateBuilder(12, 13, 14).getInstance();
System.out.format("等价关系测试\n自反性(%s)\n存在a.equals(b)为true时,a.compareTo(b) == 0 (%s)\n", a.compareTo(a) == 0, (a.equals(b) == true) == (a.compareTo(b) == 0));
//a.compareTo(null);
//抛出NullPointerException
}
}/* Output:
等价关系测试
自反性(true)
存在a.equals(b)为true时,a.compareTo(b) == 0 (true)
*///:~