Effective Java 第8条:覆盖equals时要遵守通用约定


转载 : http://architecture3.riaos.com/?p=3092399

覆盖equals方法貌似很简单,但是有许多覆盖方式会导致错误,所以们应该做到要么不覆盖equals方法,要么覆盖时就要遵守通用约定。

一、什么情况下可以不覆盖equals方法

如果不覆盖equals方法,那该类的每个实例都只与它自身相等,而有时候这就是我们需要的。

1、类的每个实例本质上都是唯一的

对于代表活动实体如Thread而不是值(Value)的类来说确实如此,Object提供的equals实现对于这些类来说正是正确的行为。

2、不关心类是否提供逻辑相等的测试功能

有些类我们只关注它提供的功能,而不是类实例之间是否相等。比如java.util.Random类,客户一般用它生成随机数,基本不会无聊的检测两个Random生成的随机数是否相同,所以对于这些类覆盖equals方法的意义不大。

3、超类已经覆盖equals,从超类继承过来的行为对于子类也是合适的

这些在集合框架中比较常见,比如大多数的Set实现都从AbstractSet继承equals实现,List实现从AbstractList继承equals实现,Map从AbstractMap继承equals实现。

4、类是私有的或包级私有的,可以确定它的equals方法永远不会被调用

在这种情况下,最好覆盖equals方法,以防它被意外调用,可以在覆盖equals方法中抛出异常。

二、覆盖equals方法的情景

1、如果类具有自己特有的“逻辑相等”概念(不同于对象等同概念),而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法,这就是典型的”值类”情形。

三、equals通用约定(等价关系)

1、自反性(reflexive):对于任何非null的引用值x,x.equals(x)必须返回true。

2、对称性(symmetric):对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。

3、传递性(transitive):对于任何非null的引用值x,y,z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)也必须返回true。

4、一致性(consistent):对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false。

5、对于任何非null的引用值x,x.equals(null)必须返回false。

三、违反通用约定的例子

1、违反对称性

实现一个区分大小写的字符串,字符串由toString方法保存,但在equals比较操作中忽略大小写:

public final class CaseInsensitiveString{
	private final String s;
	public CaseInsensitiveString(String s){
	if(s == null){
	throw new NullPointerException();
}
this.s = s;
}
@Override public boolean equals(Object o){
	if(o  instanceof  CaseInsensitiveString){
	return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
}
//就是这违反了对称性,CaseInsensitiveString知道String类,但反过来就不成了,谁知道你丫是啥东东啊,这样String.equals(cis)肯定返回false
if(o  instanceof  String){
	return s. equalsIgnoreCase((String)o);
}
return false;
}
}

测试数据:
CaseInsensitiveString cis = new CaseInsensitiveString(“Polish”);
String s = “polish”;

当调用cis.equals(s)时调用的是CaseInsensitiveString类的equals方法返回true,但是s.equals(cis)调用的是String类的equals方法返回false,String类是不知道cis为何物的,它也不知道如何和cis进行比较,必然返回false,这就不满足对称性了。

解决这个错误的方法是将equals方法进行如下的重构:

@Override public boolean equals(Object o){
//专一,只与CaseInsensitiveString类自己的实例比较,互操作是没前途的
return o instanceof CaseInsensitiveString 
       && ((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
}

2、违反传递性

Point类:

public class Point{
	private final int x;
	private final int y;
	public Point(int x, int y){
	this.x = x;
	this.y = y;
}
@Override public boolean equals(Object o){
	if(!(o instanceof  Point)){
	return false;
}
Point p = (Point)o;
return p.x == x && p.y == y;
}
}

现在对Point类做扩展,为其添加颜色信息:

public class ColorPoint extends Point{
	private final Color color;
	public ColorPoint(int x, int y, Color color){
	super(x, y);
	this.color = color;
}
}

此时如果对ColorPoint调用equals方法,由于其没有覆盖equals方法,因此是直接调用从超类继承过来的equals方法,在比较过程中将忽略 颜色信息 ,这样做虽然没有违反equals约定,但是不符合逻辑相等的期望,因此为ColorPoint提供如下的equals方法:

@Override public Boolean equals(Object o){
	if(!(o instanceof  ColorPoint)){
	return false;
}
return super.equals(o) && ((ColorPoint)o).color == color;
}

测试数据:

Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
当p.equals(cp)时调用Point的equals方法,没有颜色信息因此返回true,当cp.equals(p)是调用ColorPoint的equals方法时则返回false,不满足对称性,可以通过如下重构equals的方法修复这个问题:

@Override public Boolean equals(Object o){
	if(!(o instanceof  Point)){
	return false;
}
	if(!(o instanceof  ColorPoint)){//不带颜色的Point,使用Point的equals方法比较
	return o.equals(this);
}
return super.equals(o) && ((ColorPoint)o).color == color;
}
上述方法保证了对称性,我们可以使用以下测试数据来进行测试:

ColorPoint p1 = new ColorPoint(1, 2, Color.BLUE);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.RED);

当p1.equals(p2)时返回true,p2.equals(p3)时返回true,p1.equals(p3)是返回false,不满足传递性。

这个问题是面向对象语言中关于等价关系的一个基本问题:无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals的约定。

解决这个问题的方法是:面向对象编程中,组合优先于继承,现在的ColorPoint类不再继承Point类,而是通过一个引用组合它,重构之后的代码如下:

public class ColorPoint{
	private final Point point;
	private final Color color;
	public ColorPoint(int x, int y, Color color){
	if(Color == null){
	throw new NullPointerException();
}
point = new Point(x, y);
this.color = color;
}
   @Override public Boolean equals(Object o){
	 if(!(o instanceof  ColorPoint)){
	return false;
 }
 ColorPoint cp = (ColorPoint)o;
 return cp.point.equals(point) && cp.color.equals(color);
  }
}

JDK中java.sql.Timestamp对java.util.Date就违反对称性约定,可以查看JDK文档中的免责声明。所以不建议大家将两者混用。

3、违反一致性

在equals方法中不要依赖不可靠的资源,例如java.net.URL的equals方法依赖与对URL中主机IP地址的比较,但是网络中主机IP地址有可能是变化的,因此java.net.URL的equals方法难以保证一致性原则。

4、违反空值性

一般我们使用一个显示的空测试来避免抛出空指针异常:

@Override public boolean equals(Object o){
	if(o == null){
	return false;
}
……
}
其实,在equals方法中,最终是要将待比较对象转换为当前类的实例,以调用它的方法或访问它的属性, 这样必须先经过instanceof测试,而如果instanceof的第一个参数为null,则不管第二个参数是那种类型都会返回false,这样可以很好地避免空指针异常并且不需要单独的null检测

@Override public boolean equals(Object o){
	if(!(o instanceof  MyType)){
	return false;
}
MyType mt = (MyType)o;
……
}

四、实现高质量equals方法的诀窍

1、使用==操作符检查参数是否为这个对象的引用。

2、使用instanceof操作符检查参数是否为正确的类型。

3、把参数转换成正确的类型。

4、对于要比较类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配。

5、编写完equals方法后需要测试是否满足对称性、传递性和一致性。

五、最佳编程实践

1、覆盖equals时总要覆盖hashCode

2、不要企图让equals方法过于智能

3、不要将equals声明中的Object对象替换为其他的类型,因为替换后只是重载Object.equals(Object o)而不是覆盖。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值