第8条: 覆盖equals时请遵守通用约定
- 以下情况,类的每个实例都只与它自身相等:
- 类的每个实例本质上都是唯一的;
- 不关心类是否提供了“逻辑相等”的测试功能;
- 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的;
- (如何理解?)类是私有的或者包级私有的,可以确定他的equals方法永远不会被调用;
在这种情况下,无疑是应该覆盖equals方法的,以防止他被意外调用:
@Override public boolean equals(Object o) {
throw new AssertionError(); // Method is never called
}
“值类”需要覆盖equals的情形:值类即表示值的类,例如Integer,Date。覆盖equals方法以希望确定它们在逻辑上是否相等的情况。
“值类”不需要覆盖equals的情形:当值类为实例受控类,即每个值至多只存在一个对象的类时,不需要覆盖equals,直接调用Object.equals即可,因为此时逻辑相等与对象相等是同一个概念。
-
equals方法实现了等价关系:
- 自反性:对于任何一个非null的引用值x,x.equals(x)必须返回true;
- 对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
- 传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
- 一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会已知地返回true,或者一致地返回false。
- 非空性:对于任何非null的引用值x,x.equals(null)必须返回false。
小结:保证高质量equals的诀窍
- 使用==操作符检查“参数是否为这个对象的引用”;
public boolean equals(Object obj) {
return (this == obj);
}
- 使用instanceof操作符检查“参数是否为正确的类型”;
public boolean equals(Object o) {
if(!(o instanceof ClassType))
return false;
...
}
- 把参数转换成正确的类型;
public boolean equals(Object o) {
if(!(o instanceof Type))
return false;
Type type = (Type)o;
...
}
- 对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配;
1)对于既不是float也不是double类型的基本类型,可以使用==操作符进行比较;
2)对于对象引用,可以递归的使用equals方法;
3)对于float域可以使用Float.compare,对于double域可以使用Double.compare,因存在Float.NaN、-0.0f以及类似的double常量;
4)对于集合类需要比对每一个元素,如Arrays.equals;
5)避免合法null域,可采用以下代码;
6)当你编写完成了equals方法之后,应该问自己三个问题:它是否对称、传递、一致?
7) 覆盖equals时总要覆盖hashCode;
8) 不要企图让equals过于智能;
9) 不要将equals声明中的Object对象替换为其他类型;
//5)的代码案例
field == null ? o.field == null : field.equals(o.field);
第9条:覆盖equals时总要覆盖hashCode
- 基本解决方法:
1) 把某个非零常数值,比如说17,保存在一个名为result的int类型的变量中;
2) 对于对象中每个关键域f(指equals方法中涉及的 每个域),完成以下步骤:
a. 为该域计算int类型的散列码c:
i. 如果该域是boolean类型,则计算(f?1:0);
ii. 如果该域是byte、char、short或者int,则计算(int)f;
iii. 如果该域是long类型,则计算(int)(f^(f>>>32));
iv. 如果该域是float类型,则计算Float.floatToIntBits(f);
v.如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤2.a.iii,为得到的long类型值计算散列值;
vi.如果该域是一个对象引用,并且该类型的equals方法通过递归的调用equals的方式来比较这个域,则同样的为这个域递归的调用hashCode。如果需要更复杂的比较,则为这个域计算一个范式,然后针对这个范式调用hashCode。如果这个域值为null,则返回0(或者其他某个常数,但通常是0);
vii.如果该域是一个数组,则要把每一个元素做单独的域来处理。也就是说,递归的应用上述规则,对每个重要的元素计算一个散列吗,然后根据步骤2.b中的做法把这些散列值组合起来。如果数组域中的每一个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法;
b. 按照下面的公式,把步骤2.a中计算得到的散列码合并到result中:result = 31 * result + c;
3) 返回result;
public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber (int areCode, int prefix, int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 9999, "line number");
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNumber = lineNumber;
}
private static void rangeCheck(int arg, int max, String name) {
if(arg < 0 || arg > max) {
throw new IllegalArgumentException(name + ":" + arg);
}
}
public boolean equals(Object o) {
...
}
public int hashCode() {
int result = 17;
result = 31 * result + areCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
}
NOTE:如果一个类是不可变的,并且计算散列码的开销比较大,应该采用散列码缓存对象内部而不是每次请求的时候重新计算散列码;
//Lazy initialized, cached hashCode
private volatile int hashCode;
public int hashCode() {
int result = hashCode;
if(result == 0){
int result = 17;
result = 31 * result + areCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
return result;
}