第六条 在改写equals的时候请遵守通用规定
有一种“值类”可以不要求改写equals方法,类型安全枚举类型,因为类型安全枚举类型保证每一个值之多只存在一个对象,所有对于这样的类而言,Object的queals方法等同于逻辑意义上的equals方法。
在改写equals方法的时候,要遵循的规范:
1,自反性。对任意的引用值x,x.equals(x)一定是true
2,对称性。对于任意的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)也一定返回true.
3,传递性。对于任意的引用值x,y和z,如果x.equals(y)==true and y.equals(z)==true,so x.equals(z)==true.
4,一致性。对于任意的引用值x和y,如果用于equals比较的对象信息没有被修改的话,那么,多次调用x.equals(y)要么一致地返回true,要么一致地返回false.
5,非空性。对于任意的非空引用值x,x.equals(null)一定返回false.
自反性:要求一个对象必须等于其自身。一个例子:你把该类的实例加入到一个集合中,则该集合的contains方法
将果断地告诉你,该集合不包含你刚刚加入的实例.
对称性:
例如:
public final class CaseInsensitiveString{
private String s;
public CaseInsensitiveString(String s){
if(s==null) throw new NullPointerException();
this.s=s;
}
public boolean equals(Object o){
if(o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
if(o instanceof String)
return s.equalsIgnoreCase((String)o);
return false;
}
}
调用:
CaseInsensitiveString cis=new CaseInsensitiveString("Polish");
String s="polish";
正如我们期望的:cis.equals(s)==true but s.equals(cis)==false
这就违反了对称性的原则.为了解决这个问题,只需把企图与String互操作的这段代码从equals方法中去掉旧可以了.这样做之后,你可以重构代码,使他变成一条返回语句:
public boolean equals(Object o){
return o instanceof CaseInsensitiveString && ((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
}
传递性---即如果一个对象等于第二个对象,并且第二个对象又等于第三个对象,则第一个对象一定等于第三个对象。
例如:
public class Point{
private final int x;
private final int y;
public Point(int x,int y){
this.x=x;
this.y=y;
}
public boolean equals(Object o){
if(!(o instanceof Point))
return false;
Point p=(Point)o;
return p.x==x&&p.y==y;
}
}
现在我们来扩展这个类,为一个点增加颜色信息:
public class ColorPoint extends Point{
private Color color;
public ColorPoint(int x,int y,Color color){
super(x,y);
this.color=color;
}
}
分析: equals方法会怎么样呢?如果你完全不提供equals方法,而是直接从Point继承过来,那么在equals座比较的时候颜色信息会被忽略掉。如果你编写一个equals方法,只有当实参是另一个有色点,并且具有同样的位置和颜色的时候,它才返回true:
public boolean equals(Object o){
if(!(o instanceof ColorPoint)) return false;
ColorPoint cp=(ColorPoint)o;
return super.equals(o) && cp.color==color;
}
分析:这个方法的问题在于,我们在比较一个普通点和一个有色点,以及反过来的情形的时候,可能会得到不同的结果。前一种比较忽略了颜色信息,而后一种比较总是返回false,因为实参的类型不正确。例如:
Point p=new Point(1,2);
ColorPoint cp=new ColorPoint(1,2,Color.RED);
然后:p.equals(cp)==true but cp.equals(p)==false
修正:让ColorPoint.equals在进行“混合比较”的时候忽略颜色信息:
public boolean equals(Object o){
if(!(o instanceof Point)) return false;
if(!(o instanceof ColorPoint)) return o.equals(this);
ColorPoint cp=(ColorPoint)o;
return super.equals(o) && cp.color==color;
}
这种方法确实提供了对称性,但是却牺牲了传递性:
ColorPoint p1=new ColorPoint(1,2,Color.RED);
Point p2=new Point(1,2);
ColorPoint p3=new ColorPoint(1,2,Color.BLUE);
此时:p1.equals(p2)==true and p2.equals(p3)==true but p1.equals(p3)==false很显然违反了传递性。前两个比较不考虑颜色信息,而第三个比较考虑了颜色信息。
结论:要想在扩展一个可实例华的类的同时,既要增加新的特征,同时还要保留equals约定,没有一个简单的办法可以做到这一点。根据"复合优于继承",这个问题还是有好的解决办法:我们不让ColorPoint扩展Point,而是在ColorPoint中加入一个私有的Point域,以及一个公有的视图方法,此方法返回一个与该有色 点在同一位置上的普通Point对象:
public class ColorPoint{
private Point point;
private Color color;
public ColorPoint(int x,int y,Color color){
point=new Point(x,y);
this.color=color;
}
public Point asPoint(){
return point;
}
public boolean equals(Object o){
if(!(o instanceof ColorPoint)) return false;
ColorPoint cp=(ColorPoint)o;
return cp.point.equals.(point) && cp.color.equals(color);
}
}
注意,你可以在一个抽象类的子类中增加新的特征,而不会违反equals约定。
一致性:
如果两个对象相等的话,那么它们必须始终保持相等,除非有一个对象被修改了。由于可变对象在不同的时候可以与不同的对象相等,而非可变对象不会这样,这个约定没有严格界定。
非空性:没什么好说的。
1,使用==操作符检查“实参是否为指向对象的一个应用”。如果是的话,则返回true。
2,使用instanceof操作符检查“实参是否为正确的类型”。如果不是的话,则返回false。
3,把实参转换到正确的类型。
4,对于该类中每一个“关键(significant)”域,检查实参中的域与当前对象中对应的域值是否匹配
if (!(this.field == null ? o.field == null : this.equals(o.field)))
//或者写成 if(!(this.field == o.field || (this.field != null && this.field.equals(o.field)))) 对于this.field和o.field通常是相同的对象引用,会更快一些。
return false;
//比较下一个field
//都比较完了
return true;
5.最后还要确认以下事情
5.1)改写equals的同时,总是(必须)要改写hashCode方法(见【第8条】),这是极容易被忽略的,有极为重要的
5.2)不要企图让equals方法过于聪明
5.3)不要使用不可靠的资源。如依赖网络连接
5.4)不要将equals声明中的Object对象替换为其他类型。
public boolean equals(MyClass) 这样的声明并不鲜见,往外使程序员数小时搞不清楚为什么不能正常工作
原因是这里是重载(overload)而并不是改写(override)(或称为覆盖、重写)
相当于给equals又增加了一个实参类型为MyClass的重载,而实参为Object的那个equals并没有被改写,依然还是从Object继承来的最初的那个equals,所总是看不到程序员想要的效果。因为类库或其他人写的代码都是调用实参为Object型的那个equals方法的(别人又如何在事前知晓你今天写的MyClass呢?)