0 正确的equals方法
public class MyClass { // 主要属性1 private int primaryAttr1; // 主要属性2 private int primaryAttr2; // 可选属性 private int optionalAttr; // 延迟加载,缓存散列码 private volatile int hashCode = 0; @Override public int hashCode() { if(hashCode == 0) { int result = 17; result = 37*result + primaryAttr1; result = 37*result + primaryAttr2; hashCode = result; } return hashCode; } @Override public boolean equals(Object obj) { if(obj == this) { return true; } if(!(obj instanceof MyClass)) { return false; } MyClass myClass = (MyClass) obj; return myClass.primaryAttr1 == primaryAttr1 && myClass.primaryAttr2 == primaryAttr2; } }
1 在改写equals时要遵守通用约定
1.1 不必改写equals的情况:
1)一个类的每个实例本质上都是惟一的。
2)不关心一个类是否提供了“逻辑相等”的测试功能。
3)超类已经改写的equals,从超类继承的行为对于子类也是合适的。
4)一个类是私有的,或者是包级私有的,并且可以确定它的equals方法永远也不会被调用。
1.2 需要改写Object.equals的情况:
当一个类有自己特有的“逻辑相等”概念,而且父类也没有改写equals以实现期望的行为。通常适合于value class的情形。
改写equals也使得这个类的实例可以被用作map的key或者set的元素,并使map和set表现出预期的行为。
1.3 改写equals要遵守的通用约定(equals方法实现了等价关系):
1)自反性:x.equals(x)一定返回true
2)对称性:x.equals(y)返回true当且仅当y.equals(x)
3)传递性:x.equals(y)且y.equals(z),则x.equals(z)为true
4)一致性:若x.equals(y)返回true,则不改变x,y时多次调用x.equals(y)都返回true
5)对于任意的非空引用值x,x.equals(null)一定返回false。
当重写完equals方法后,应该检查是否满足对称性、传递性、一致性。(自反性、null通常会自行满足)
下面是一个示例:
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; } //... }
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 boolean equals(Object o) { if(!(o instanceof ColorPoint)) return false; ColorPoint cp = (ColorPoint)o; return super.equals(o) && cp.color == color; } //... }
import java.awt.Color; public class Test { public static void main(String arg[]) { System.out.println("检验对称性:"); Point p = new Point(1, 2); ColorPoint cp = new ColorPoint(1, 2, Color.RED); System.out.println(p.equals(cp)); System.out.println(cp.equals(p)); System.out.println("检验传递性:"); ColorPoint p1 = new ColorPoint(1, 2, Color.RED); Point p2 = new Point(1, 2); ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE); System.out.println(p1.equals(p2)); System.out.println(p2.equals(p3)); System.out.println(p1.equals(p3)); } }
此时的输出为:
检验对称性: true false 检验传递性: false true false
这表示上述equals方法的写法违背了对称性,但没有违背传递性。之所以不满足对称性,是由于Point的equals方法在接收ColorPoint类型的参数时,会将其当做Point(忽略color属性)进行比较,而ColorPoint的equals方法接收Point类型参数时,判断其不是ColorPoint类型就直接返回false。因此,可以对equals方法作如下修改:
/* 满足对称性,但牺牲了传递性 */ public boolean equals(Object o) { if(!(o instanceof Point))// 不是Point类型 return false; if(!(o instanceof ColorPoint))// 是Point类型,但不是ColorPoint类型 return o.equals(this); // 是ColorPoint类型 ColorPoint cp = (ColorPoint)o; return super.equals(o) && cp.color == color; }
此时的执行输出为:
检验对称性: true true 检验传递性: true true false
这表示满足对称性,但牺牲了传递性。由于p1与p2,p2与p3的比较都没有考虑color属性,但是p1与p3比较则考虑了color属性。要在扩展(即继承)一个可实例化的类的同时,既要增加新的属性,同时还要保留equals约定,没有一个简单的方法可以做到。
复合优先于继承。遵照这条原则,这个问题可以有很好的解决方案,不再让ColorPoint继承Point,而是在ColorPoint类中加入一个私有的Point域,以及一个公有的view方法,此方法返回对应的Point对象。
import java.awt.Color; public class ColorPoint { private Point point; private final Color color; public ColorPoint(int x, int y, Color color) { point = new Point(x, y); this.color = color; } // 返回ColorPoint的Point视图(view) 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); } // ... }
Java平台中有一些类是可实例化类(非抽象类)的子类,且加入了新属性。例如,java.sql.Timestamp继承自java.util.Date,并增加了nanoseconds域,Timestamp的equals违反了对称性,当Timestamp和Date用于集合中时可能出现问题。
注意,可以在一个抽象类的子类中增加新的属性,而不会违反equals约定。因为不可能创建父类的实例,也就不会出现上述问题。
2 不要将equals声明中的Object对象替换为其他类型
public boolean equals(MyClass obj) { ... }
上述代码,使用了具体的类MyClass作为参数,这会导致错误。原因在于,这个方法并没有重写(override)Object.equals方法,而是重载(overload)了它。某些情况下,这个具体化的equals方法会提高一些性能,但这样极有可能造成不易察觉的错误。
3 改写equals时同时改写hashCode
3.1 java.lang.Object的规范中,hashCode约定:
1)在一个应用程序执行期间,如果一个对象的equals方法
2)相等对象(equals返回true)必须具有相等的散列码
3)不相等对象(equals返回false)调用hashCode方法,不要求必须产生不同的值;但是产生不同的值有可能提高散列表的性能。
若改写equals时,未同时改写hashCode方法,则可能导致不满足第二条。
3.2 改写hashCode的常用方法:
参考资料:
《Effective Java》(2nd Edition) 第7条、第8条、第14条、第20条、第37条等