覆盖equals需要遵守的通用约定:
覆盖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,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
4、一致性:对于任何非空的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一直的返回true,或者一直的返回false。
5、对于任何非null的引用值x,x.equals(null)必须返回false。
通过系列两个类验证覆盖equals方法必修要遵守的通用约定:
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的子类ColorPoint:
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return o.equals(this);
return super.equals(o) && ((ColorPoint) o).color == color;
}
a)自反性验证:
Point p = new Point(10,15);
ColorPoint cp = new ColorPoint(10,15,Color.BLUE);
System.out.println(p.equals(p) + " ------ "+cp.equals(cp)); //true ------ true
b) 对称性验证:
Point p = new Point(10,15);
ColorPoint cp = new ColorPoint(10,15,Color.BLUE);
System.out.println(p.equals(cp) + " ------ "+cp.equals(p)); //true ------ true
c) 传递性验证:
Point p = new Point(10,15);
ColorPoint cp = new ColorPoint(10,15,Color.BLUE);
ColorPoint cp1 = new ColorPoint(10,15,Color.RED);
System.out.println(p.equals(cp) + " ------ "+cp.equals(cp1) + "------"+p.equals(cp1)); //true------ false ------ true
通过上述验证,类Point和ColorPoint重写的equals满足自反性、对称性,但是不满足传递性,因此上述重写的equals方法存在问题;
在面向对象中,我们无法在可扩展可实例化的类时,既增加新的组件,同时又保留equals约定,解决办法是使用“复合优于继承”,不在让子类扩展父类,而是在子类中加入一个可私有的父类域和一个公有的视图方法。
代码如下:
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;
}
public Point asPoint() {
return point;
}
@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);
}
@Override
public int hashCode() {
return point.hashCode() * 33 + color.hashCode();
}
}
传递性的执行结果为:
Point p = new Point(10,15);
ColorPoint cp = new ColorPoint(10,15,Color.BLUE);
ColorPoint cp1 = new ColorPoint(10,15,Color.RED);
System.out.println(p.equals(cp) + " ------ "+cp.equals(cp1) + "------"+p.equals(cp1)); //false ------ false ------ false
满足自反性、对称性和传递性,三者缺一不可。
在覆盖equals时总要覆盖hashCode方法:
原因:
为什么再覆盖equals方法时总要覆盖hashCode方法?
不这么做会违反hashCode的通用约定,导致该类无法结合所有基于散列的集合一起正常运作,类集合有hashMap、hashSet。
需要做逻辑相等判断的类,覆盖equals方法,如果还需要在散列表(HashMap、HashSet)中作为key,需要覆盖hashcode方法。
覆盖hashCode方法通用约定:
1、在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对同一对象的多次调用,hashCode方法都必须始终返回同一个值。在一个应用程序与另外一个应用程执行过程中,执行hsahCode方法所返回的值可以不一致。
2、如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中的hashCode方法都必须产生同样的整数结果。
3、如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中的hashCode方法,则不一定要求hashCode方法必须产生不同的结果。但开发者应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。
———-摘自《Effective Java》
上述通用约定的实现例如ColorPoint中的覆盖方法hashCode():
@Override
public int hashCode() {
return point.hashCode() * 33 + color.hashCode();
}
扩展-equals() + hashCode()(解析hashCode通用约定):
1.若重写了equals(Object obj)方法,则有必要重写hashCode()方法。
2.若两个对象equals(Object obj)返回true,则hashCode()有必要也返回相同的int数:
覆盖hashCode方法,但不覆盖equals方法,仍然会导致数据的不唯一性
3.若两个对象equals(Object obj)返回false,则hashCode()不一定返回不同的int数:
4.若两个对象hashCode()返回相同int数,则equals(Object obj)不一定返回true:
5.若两个对象hashCode()返回不同int数,则equals(Object obj)一定返回false:
6.同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。