覆盖equals时请遵守通用约定
equals方法起来似乎很简单,但是许多覆盖方式会导致很多错误,在此就不多说"覆盖(重写)"的含义。
在java中使用equals方法通常用来比较两个对象是否相等,如果你还在困扰equals和==的区别或者不知道为什么要覆盖equals方法,请先了解清楚再往下看。
什么时候不需要覆盖equals方法?
1.类的每个实例本质都是唯一的,例如Thread类,Object提供的equals方法对这些类来说正好适用。
2.不关系类是否提供了"逻辑相等"的测试功能。
3.超类已经覆盖了equals,从超类继承过来的行为对于子类也同样合适。
4.类是私有的或包是私有的,可以确定他的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.一致性:对于任何非null的引用值x,y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或一致地返回false.
5.对于非null的引用值x,x.equals(null)必须返回false.
看了上述概念觉得就像是数学公式一样枯燥,不如看看代码如何实现。- -
1.自反性(对象必须等同于自身)
String s = "helloworld";
System.out.println(s.equals(s));
运行结果:显而易见为true
2.对称性(任何两个对象对于 (他们是否相等吗?)的问题都必须保持一致)
public class CaseInsensitiveString {
private String s;
public CaseInsensitiveString(String s) {
if (s == null)
throw new NullPointerException();
this.s = s;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
if (obj instanceof String)
return s.equalsIgnoreCase((String) obj);
return false;
}
public static void main(String[] args) {
CaseInsensitiveString s1 = new CaseInsensitiveString("Helloworld");
String s2 = "helloworld";
System.out.println("s1 == s2 ? " + s1.equals(s2));
System.out.println("s2 == s1 ? " + s2.equals(s1));
}
}
运行结果:s1 == s2 ? true s2 == s1 ? false
解释:通过equals方法判断忽略大小写的字符串是否相等,出现这样的结果是因为:我们在进行s1.equals(s2)的时候,是因为s1是CaseInsensitiveString类型的,它会执行到上面equals的代码,而s2是String类型的,s2.equals(s1)的比较自然是String中的equals方法(此方法并没有比较字符串相等的方法)
改进后:
public class CaseInsensitiveString {
private String s;
public CaseInsensitiveString(String s) {
if (s == null)
throw new NullPointerException();
this.s = s;
}
@Override
public boolean equals(Object obj) {
return obj instanceof CaseInsensitiveString && ((CaseInsensitiveString) obj).s.equalsIgnoreCase(s);
}
public static void main(String[] args) {
CaseInsensitiveString s1 = new CaseInsensitiveString("Ss");
CaseInsensitiveString s3 = new CaseInsensitiveString("SS");
System.out.println("s1 == s3 ? " + s1.equals(s3));
System.out.println("s3 == s1 ? " + s3.equals(s1));
}
}
运行结果:s1 == s3 ? true s3 == s1 ? true
3.传递性
现在假设我们有一个类Point和一个Point的子类ColorPoint分别如下:
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 obj) {
if (!(obj instanceof Point))
return false;
Point p = (Point) obj;
return p.x == x && p.y == y;
}
}
ColorPoint继承Point,不过比Point类多一个颜色属性。当我们把ColorPoint与Point对象的引用和Point与ColorPoint进行比较
import java.awt.Color;
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
public static void main(String[] args) {
Point p1 = new Point(1, 2);
ColorPoint cp1 = new ColorPoint(1, 2, Color.BLACK);
System.out.println("p1 == cp1 ? " + p1.equals(cp1));
System.out.println("cp1 == p1 ? " + cp1.equals(p1));
}
}
运行结果:p1 == cp1 ? true cp1 == p1 ? true
为什么会遇到这样的情况?明明不同类型,执行结果为true。
仔细看过发现ColorPoint没有提供equals方法,直接继承了Point类,使用的当然是Point的equals方法,很明显颜色信息被忽略掉了。
改进ColorPoint类增加equals方法
import java.awt.Color;
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 obj) {
if (!(obj instanceof Point))
return false;
if (!(obj instanceof ColorPoint))
return obj.equals(this);
return super.equals(obj) && ((ColorPoint) obj).color == color;
}
public static void main(String[] args) {
ColorPoint p1 = new ColorPoint(1, 2, Color.BLACK);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
System.out.println("p1 == p2 ? " + p1.equals(p2));
System.out.println("p2 == p3 ? " + p2.equals(p3));
System.out.println("p1 == p3 ? " + p1.equals(p3));
}
}
运行结果:p1 == p2 ? true p2 == p3 ? true p1 == p3 ? false
原因:因为第三种情况比较了颜色信息。
总结:我们无法在扩展可实例化的类的同时,既增加新的值得组件,又同时保留equals约定
改进:复合优先于继承的规则
public class ColorPoint{
enum Color{
RED,BLACK,WHITE
}
private final Color color;
private final Point point;
public ColorPoint(int x, int y, Color color) {
this.color = color;
point = new Point(x, y);
}
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);
}
public static void main(String[] args){
ColorPoint p1 = new ColorPoint(1, 2 , Color.BLACK);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2 , Color.WHITE);
System.out.println("p1.equals(p2): " + p1.equals(p2));
System.out.println("p2 equals(p1): " + p2.equals(p1));
System.out.println("p2.equals(p3): " + p2.equals(p3));
System.out.println("p1.equals(p3): " + p1.equals(p3));
}
}
运行结果:p1.equals(p2): false p2 equals(p1): false p2.equals(p3): false p1.equals(p3): false
一致性:如果两个对象相等,它们就必须始终相等,除非它们中有一个对象(或两个都)被修改了。
非空性:所有的对象都必须不等于null。因此,我们在覆盖equals方法时,必须先检查其正确类型。
覆盖equals方法的建议:
1.使用==操作符检查 ”参数是否为这个对象的引用“。
2.使用instanceof操作符检查 ”参数是否为正确的类型“。
3.把参数转换为正确的类型。
4.对于该类中的”关键域“,检查参数中的域是否与对象中对应的域相匹配。
5.当完成equals方法后,要检查是否满足四个特性,最好测试检查。
6.覆盖equals时总要覆盖hashCode
7.不要企图让equals方法过于智能
8.不要将equals声明中的Object对象替换为其他的类型。