《Effective java》读书笔记3——覆盖equals方法的通用约定

满足下列四个条件之一的就不需要覆盖equals方法:

(1).类的每个实例本质上都是唯一的,如枚举等。

(2).不关心类是否提供了“逻辑相等”的测试功能。

(3).超类已经覆盖了equals方法,从超类集成过来的行为对于子类也是合适的。

(4).类是私有的或者包访问权限的,可以确定它的equals方法永远不会被调用。

当类具有自己特有的“逻辑相等”概念(不同于对象等同的概念),而且超类还没有覆盖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)总会一致地返回相同的结果。

(5).非空性(Non-nullity):对于任何非null的引用值x,x.equals(null)必须返回false。

这些约定看起来很简单,但是在实际编程中很容易违反这些约定,一旦违反这些约定,程序运行就好不正常,接下来通过例子展示违反约定的情况以及如何避免这些错误。

违反对称性的例子:

实现一个区分大小写的字符串,字符串由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);
}
if(o  instanceof  String){
	return s. equalsIgnoreCase((String)o);
}
return false;
}
}

我们使用如下的测试数据来测试这个equals方法:

CaseInsensitiveString cis = new CaseInsensitiveString(“Polish”);
String s = “polish”;

当调用cis.equals(s)时调用的是CaseInsensitiveString类的equals方法返回true,但是s.equals(cis)调用的是String类的equals方法返回false,不满足对称性。

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

@Override public boolean equals(Object o){
return o instanceof CaseInsensitiveString 
       && ((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
}

违反传递性的例子:

一个基本的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;
}

使用如下的测试数据测试equals方法:

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.Data进行了扩展,并增加了nanosecond域,Timestamp的equals方法实现确实违反了对称性原则,JDK文档中的免责声明如下:

java.sql.Timestamp由java.util.Date和单独的毫微秒值组成。只有整数秒才会存储在 java.util.Date组件中。小数秒(毫微秒)是独立存在的。传递不是 java.sql.Timestamp实例的对象时,Timestamp.equals(Object) 方法永远不会返回 true,因为日期的毫微秒组件是未知的。因此,相对于 java.util.Date.equals(Object)方法而言,Timestamp.equals(Object) 方法是不对称的。此外,hashcode 方法使用底层 java.util.Date 实现并因此在其计算中不包括毫微秒。

鉴于 Timestamp类和上述 java.util.Date类之间的不同,建议代码一般不要将 Timestamp值视为 java.util.Date的实例。Timestamp 和 java.util.Date 之间的继承关系实际上指的是实现继承,而不是类型继承。

所以当把Timestamp 类和Date对象用于同一个集合对象或者其他混合使用时,可能会引起难以调试的问题,在编程中建议大家不要将二者混用。

避免违反一致性原则:

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

避免违反空值性原则:

这个原则最容易避免,很多人喜欢使用下面方式避免空值:

@Override public boolean equals(Object o){
	if(o == null){
	return false;
}
……
}
这种做法其实没有必要,我们通常使用下面的例子:

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

在instanceof类型检查时,传入null和传入不同类型参数一样会返回false,保证了空值性原则。

实现高质量equals方法的诀窍:

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

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

(3).把参数转换成正确的类型。

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

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

覆盖equals方法时特别要注意:不要将equals生命中的Object对象替换为其他类型,如:

public boolean equals(MyClass o){
	……
}

上述方法并没有覆盖Object类的equals方法,只是重载了Object类的equals方法并对其进行强类型匹配而已,在和Object的equals方法返回相同结果的情况下没有太大的影响,如果返回结果不同,则往往引起不符合期望的程序行为,并且这种问题非常的隐蔽和难以调试。

避免这种问题的做法很简单,在equals方法前加上@Override注解如下:

@Override public boolean equals(Object o){
	……
}

此时如果方法输入参数不是Object类型就会产生编译时错误。

覆盖hashCode方法时记得要覆盖equals方法

JDK的Object类约定在覆盖equals方法时必须要覆盖hashCode方法,因为如果忘记覆盖hashCode方法可能会导致使用hash算法的容器运行异常。我个人认为应该是覆盖hashCode方法的时候记得要覆盖equals方法,且看JDK的Object类对hashCode方法的约定:

(1).在应用程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,则对同一个对象多次调用hashCode方法,必须始终如一返回同一个整数值。

(2).如果两个对象根据equals方法比较是相等的,则这两个对象的hashCode方法返回值也必须相等。

(3).如果两个对象根据equals方法比较是不相等的,则这两个对象的hashCode方法返回值有可能相等。

上述约定的第三条就是编程中常说的hash碰撞,既hash码相等但是对象不一定相等,当产生hash碰撞时,需要调用equals方法比较是否对象相等,如果不等就需要对对象进行再次hash计算生成新的hashCode。

所以为了防止使用hash算法的集合容器等在运行中产生hash碰撞,必须在覆盖hashCode方法的同时覆盖equals方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值