Ch3_Methods:Item8_重写equals方法时遵循通用约定:
虽然Object类是一个具体类,但它主要还是用于扩展。因为其所有nonfinal方法(equals、
hashCode、toString、clone以及finalize)都是为重写设计的,所以这些方法都有显式的通用约定。任何重写这些方法的类都有
责任遵循这些通用约定;若未做到这一点,则其他依赖于这些约定的类就不能与之一起正常
运作。
本章讲述何时如何覆盖Object中的nonfinal方法。由于Item7已逃离了finalize,所以本章不再赘述。Comparable.compareTo虽然不是Object中的方法,但是由于它有类似的特性,所以也在本章讨论。
重写equals方法看起来很简单,但是许多重写方式会导致
错误,并且后果很严重。如果类的实例只与其自身equals,那么避免这种问题的最简单办法就是不去重写equals方法。只要满足以下任一条件,就可以这样做。
类的每个实例本质上都是唯一的。【例】例如Thread这样的表示活动实体,而非表示值的类。Object类提供的equals实现对于这些类来说恰好拥有正确的行为。
不关心类是否提供“逻辑相等”的测试功能。【例】例如java.util.Random本可以重写equals来检查两个Random实例是否生产了同样的随机数字序列,但是设计者不认为客户端会需要这样的功能。在这些情况下,从Object继承来的equals实现就已经足够了。
父类已经重写equals,并且父类的行为对于子类也是合适的。【例】例如大多数Set的实现类都从AbstractSet继承了equals方法,List的实现类从AbstractList继承了equals方法,Map的实现类从AbstractMap继承了equals方法。
类是私有的,或包级私有,并且确信其equals方法永远不会被调用。Arguably,其equals方法是应该重写成如下形式,以防止被意外调用:
[java]
@Override
public boolean equals(Object o) {
throw new AssertionError(); // Method is never called
}
那么何时应该重写Object.equals呢?当类拥有“逻辑相等”的概念,并且其父类并未重写equals以实现期望的行为,这时就需要重写Object.equals。对于值类(value
classes)通常就是这种情况,所谓值类是指表示“值”的类,例如Integer或Date。
程序员通过equals方法判断值类的实例是否逻辑相等,而不是判断
他们是否指向相同的对象。重写equals方法不仅是为了满足程序员的要求,同时也使得这些实例可以作为map的key,或者是set的元素,并拥有可预见的、期望中的行为。
有一种值类却不需要重写equals方法:实例受控的类(Item1),其每个值至多存在一个对象。【例】Enum类型就是这种值类。对于这些类,逻辑相等就等同于对象相等,所以Object.equals方法的功能就等同于逻辑equals方法。
当你重写equals方法时,必须遵守其通用约定。如下是从Object的规范中拷贝来的约定内容:
equals方法实现了等价关系(equivalence relation):
自反性(Reflexive):对于任何非null的引用x,x.equals(x)都必须返回true。
对称性(Symmetric):对于任何非null的引用x、y,当且仅当y.equals(x)为true时,x.equals(y)必须为true。
传递性(Transitive):对于任何非null的引用x、y、z,如果x.equals(y)为true、y.equals(z)为true,则x.equals(z)返回true。
一致性(Consistent):对于任何非null的引用x、y,只要equals比较操作中使用的信息未被修改,那么多次调用x.equals(y)都会一直返回true,或一直返回false。
非空性(Non-Nullity):对于任何非null的引用x,x.equals(null)必须返回false。
1)自反性(Reflexive)
第一个要求是说对象必须等于其本身。很难想象会无意中违反这一条约定。如果你违反该约定,把实例添加到集合后,集合的contains方法会告诉你该集合不包含你刚刚添加的实例。
2)对称性(Symmetric)
第二个要求是说两个对象必须对于“他们是否相等”保持一致。与第一条不同,不难想象无意违反此条要求的情形。【例】例如下面这个类,实现了一个大小写敏感的字符串。字符串的大小写在toString中保存,但在比较时却忽略了。
[java]
// Broken - violates symmetry!
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
if (s == null)
throw new
NullPointerException();
this.s = s;
}
// Broken - violates symmetry!
@Override
public boolean equals(Object o) {
if (o
instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
if (o instanceof String) // One-way interoperability!
return s.equalsIgnoreCase((String) o);
return false;
}
equals方法的意图很好,天真地试图与普通字符串进行互操作。假设有如下两个对象:
[java]
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
cis.equals(s)返回true,但是s.equals(cis)却返回false,显然违反了对称性。假设你将其放入集合中:
[java]
List<CaseInsensitiveString> list = new ArrayList<CaseInsensitiveString>();
list.add(cis);
list.contains(s)会返回什么呢?没人知道!在Sun的当前实现中,碰巧返回false,但是在其他实现中,也可能返回true或者抛出一个运行时
异常。一旦你违反equals的约定,当其他对象面对你的对象时,你就无法知道这些对象的行为会是怎么样的。
【解决办法】
为了消除这个问题,只需在equals方法中去掉对String的不良互操作即可:——为避免违反对称性,equals()参数仅考虑当前类的情况
[java]
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
3)传递性(Transitive)
同样,不难想象无意违反此条要求的情形。【例】如果子类在父类基础上增加了一个新的值组件(value component),也就是说子类增加了信息会影响equals比较。我们以一个简单的不可变的二维整数point类作为开始。
[java]
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 == null || ! (o instanceof Point)){
return false;
}
Point p = (Point) o;
return p.x == x && p.y == y;
}
}
然后扩展这个类,加入颜色的概念:
[java]
class ColorPoint extends Point{
private final Color color;
public ColorPoint(int x, int y, Color c) {
super(x, y);
this.color = c;
}
}
子类的equals应该如何编写的?如果不编写,则从Point类继承,则equals比较时会忽略颜色信息。虽然这样并不违反equals约定,但是显然是无法接受的。
【尝试1】假定仅当equals的参数是另一个ColorPoint,并且拥有相同的位置和颜色信息时才返回true。
[java]
// 与父类违反对称性!!
@Override
public boolean equals(Object o){
if(!(o instanceof ColorPoint)){
return false;
}
ColorPoint p = (ColorPoint)o;
return super.equals(p) && p.color == color;
}
Q:问题是违反了对称性!
[java]
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
p.equals(cp)返回true,而cp.equals(c)则返回false。
【尝试2】为了解决这个问题,你或许会尝试在进行ColorPoint.equals进行混合比较时忽略颜色信息:
[java]
@Override
public boolean equals(Object o){
if(! (o instanceof Point)){
return false;
}
if(!(o instanceof ColorPoint2)){ //与Point实例比较时,不考虑Color
return super.equals(o);
}
ColorPoint2 p = (ColorPoint2)o;
return super.equals(p) && p.color.equals(color);
}
Q:这种方式确实提供了对称性,但是却牺牲了传递性。
[java]
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
p1.equals(p2)、p2.equals(p3)均返回true,而p1.equals(p3)却返回false!前两个比较忽略了颜色信息,第三个比较却又考虑颜色信息。
【尝试3】
那么究竟该如何解决呢? 事实上这是OO语言中等价关系的一个基本问题。无法在扩展可实例化类的时候,增加一个值组件,同时又保证equals约定。
你可能听说过,在equals方法中使用getCalss来代替instanceof,就可以在扩展可实例化类的时候增加值组价,并同时保证equals约定。例如:
[java]
//问题:违反里氏替换原则
@Override
public boolean equals(Object o){
if(o==null || o.getClass()!= getClass()){//如果o是其子类,则永远范围false
return false;
}
Point p = (Point)o;
return p.x == x && p.y == y;
}
这样做的效果是,只有当两个对象是相同的实现类时,才会进行比较。看起来还不错,但是后果却是不可接受的。
假设我们要编写一个方法,判断某个整数点是否在单位圆上:
[java]
// Initialize UnitCircle to contain all Points on the unit circle
private static final Set<Point> unitCircle;
static {
unitCircle = new HashSet<Point>();
unitCircle.add(new Point( 1, 0));
unitCircle.add(new Point( 0, 1));
unitCircle.add(new Point(-1, 0));
unitCircle.add(new Point( 0, -1));
}
public static boolean onUnitCircle(Point p) {
return unitCircle.contains(p);
}
现在假设你扩展了Point但不增加值组件,例如在构造方法中记录一共创建了多少个实例:
[java]
public class CounterPoint extends Point {
private static final AtomicInteger counter = new AtomicInteger();
public CounterPoint(int x, int y) {
super(x, y);
counter.incrementAndGet();
}
public int numberCreated() {
return counter.get(); }
}
然后我们将CounterPoint实例传入onUnitCircle方法中,则不管CounterPoint中x值和y值是什么,永远都只会返回false。因为CounterPoint中使用的集合框架(例如HashSet),使用equals方法来判断是否包含;而CounterPoint和Point是永远不会等价的。——不满足里氏替换原则!
但是,如果在Point的equals方法中使用instanceof,同样的onUnitCircle方法就会正常工作。
【解决办法1】
虽然没有令人满意的方法可以既扩展一个可实例化类,又增加一个值组件,不过有一个规避方法。根据Item16,“组合优于继承”,我们可以在ColorPoint中增加一个private的Point域,而不是扩展Point;同时提供一个public的视图方法,返回与该color point位置相同的Point对象:
[java]
// Adds a value component without violating the equals contract
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;
}
/**
* Returns the point-view of this color point.
*/
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);
}
... // Remainder omitted
}
在
Java平台库中,有一些类扩展了可实例化类,并增加了值组件。【例】例如java.sql.Timestamp扩展了java.util.Date类并增加了nanoseconds域。Timestamp的equals方法就违反了对称性,如果Timestamp和Date被用于同一个集合中,或以其他什么方式混在一起使用,则会引起错误的行为。
Timestamp有一个免责声明,提醒程序员不要混用Date和Timestamp。虽然只要不混用他们就不会有麻烦,但是谁都不能阻止你混用他们,而结果导致的错误将会很难调试。Timestamp的这种行为是个错误,不值得效仿。
[java]
/**
* Note: This method is not symmetric with respect to the
* <code>equals(Object)</code> method in the base class.
*/
public boolean equals(java.lang.Object ts) {
if (ts instanceof Timestamp) {
return this.equals((Timestamp)ts);
} else {
return false;
}
}
注意,你可以在抽象类的子类中增加值组件,同时不违反equals约定。对于根据Item20(用类层次来替代标签类,Prefer class hierarchies to
tagged classes)而得到的类层次结构而言,这是非常重要的。【例】例如你可能有一个抽象类Shape,其中没有任何值组件;一个子类Circle,增加了一个radius域;一个子类Rectangle,增加了length和width域。上述问题就不会发生,因为不可能直接创建一个父类实例。
4)一致性(Consistent)
第四个要求是说如果两个对象相等,则他们在任何时候都必须相等,除非其中一个(或两个)对象被修改了。换句话说,可变对象在不同的时候可以和不同的对象相等,而不可变对象则不行。当你编写一个类时,仔细考虑考虑它是否应该是不可变的(Item15)?如果结论是应该不可变,则需要保证equals方法满足:相等的对象永远都相等,不等的对象永远都不等。
不论类是否可变,都不能让equals方法依赖于不可靠的资源。如果你违反了这个
限制,那就很难满足一致性要求了。【例】例如java.net.URL的equals方法依赖于对URL中主机的IP地址的比较,而将主机名转译成IP地址需要访问网络,随着时间推移,并不保证能范围相同的结果。这就会导致URL的equals方法违反约定,并且已经在实践中引起问题了。不幸的是,由于兼容性需求,这一行为无法改变。除了少数例外情况,equals方法必须对驻留在
内存中的对象进行确定性计算。
5)非空性(Non-Nullity)
最后一条要求的意思是所有对象都与null不相等。虽然很难想象什么情况下o.equals(null)会返回true,但却不难想象意外抛出NullPointException的情况,通用约定不允许出现这种情况。许多类的euqals方法使用null判断来防止这种情况:
[java]
@Override
public boolean equals(Object o){
if(o == null)
return false;
}
这种测试是不必要的。为了测试相等性,equals方法首先必须将参数强制转换为适合的类型,然后才能调用其访问器或域。在做强制转换之前,需要用instanceof方法来检查其参数是否是合适的类型:
[java]
@Override
public boolean equals(Object o) {
if (!(o instanceof MyType))
return false;
MyType mt = (MyType) o;
}
如果没有类型检查,当equals的参数传入了一个错误的类型,则equals会抛出ClassCastException,违反约定。但是如果instanceof的第一个
caozuofu.html" target="_blank">操作符是null,则一定返回false。也就是说,如果传入null,则类型检查就会返回false,不需要单独的null检查。
总结
总而言之,实现高质量的equals方法有如下诀窍:
1)使用==操作符来判断参数是否为该对象的一个引用。如果是,则返回true。这只不过是一种
性能优化,当比较操作开销很大时,就值得这么做。
2)使用instanceof操作符来判断参数是否是正确的类型。如果不是,则返回false。一般来说,正确的类型就是equals方法所在的类。偶尔情况下,是该类实现的某个
接口。如果类使用的接口优化了equals约定,允许在实现该接口的不同类间进行比较,那就使用接口。集合接口Set、List、Map、Map.Entry都有这种特性。
[java]
public interface Set<E>{
/**
* Compares the specified object with this set for equality. Returns
* <tt>true</tt> if the specified object is also a set, the two sets
* have the same size, and every member of the specified set is
* contained in this set (or equivalently, every member of this set is
* contained in the specified set).
>>>>>>>>>
This definition ensures that the
* equals method works properly across different implementations of the
* set interface.
*
* @param o object to be compared for equality with this set
* @return <tt>true</tt> if the specified object is equal to this set
*/
boolean equals(Object o);
public abstract class AbstractSet<E> implements Set<E> {
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Set))
return false;
Collection c = (Collection) o;
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
}
3)将参数强制转换为正确的类型。因为之前进行了instanceof测试,这里的强制转换能确保成功。
4)遍历类中的每个关键域,检查参数中的域与对象本身的域是否匹配。如果测试成功返回true,否则返回false。如果Step2中的类型是一个接口,则需要通过接口方法来访问参数中的域;如果是类,则也许能直接访问参数的域,这要取决于域的可见性。
对于不是float和double的基本类型,用==操作符进行比较;对于对象引用,则
递归调用equals;对于float域,使用Float.compare方法;对于double域,调用Double.compare方法。对float和double域的特殊处理是有必要的,因为存在Float.NaN、-0.0f,以及类似的double常量,可参考Float.equals方法。
[java]
//Float.equals
/*
注意,在大多数情况下,对于 Float 类的两个实例 f1 和 f2,让 f1.equals(f2) 的值为 true 的条件是当且仅当
f1.floatValue() == f2.floatValue()
的值也为 true。但是也有下列两种例外:
1) 如果 f1 和 f2 都表示 Float.NaN,那么即使 Float.NaN==Float.NaN 的值为 false,equals 方法也将返回 true。
2) 如果 f1 表示 +0.0f,而 f2 表示 -0.0f,或相反的情况,则 equal 测试返回的值是 false,即使 0.0f==-0.0f 的值为 true 也是如此。
*/
public boolean equals(Object obj) {
return (obj instanceof Float)
&& (floatToIntBits(((Float)obj).value) == floatToIntBits(value));
}
对于数组域,则对每个元素进行比较。如果数组中的每个元素都重要,可以使用JDK1.5的Arrays.equals方法。
[java]
//Arrays.equals
public static boolean equals(Object[] a, Object[] a2) {
if (a==a2)
return true;
if (a==null || a2==null)
return false;
int length = a.length;
if (a2.length != length)
return false;
for (int i=0; i<length; i++) {
Object o1 = a[i];
Object o2 = a2[i];
if (!(o1==null ? o2==null : o1.equals(o2)))
return false;
}
return true;
}
对象引用可能包含null,为避免可能发生的NullPointException,可使用如下
习惯用法来进行比较:
[java]
(field == null ? o.field == null : field.equals(o.field))
如果field和o.field总是相同的引用,则下面方法会更快:
[java]
(field == o.field || (field != null && field.equals(o.field)))
对于某些类,例如上面的CaseInsensitiveString,域的比较要比简单的相等性测试要复杂得多。如果是这种情况,你可能会希望保存该域的一个标准形式(
canonical form),equals方法在这些标准形式上进行低开销的精确比较,而不是进行高开销的非精确比较。这种方式最适合于不可变类(Item15);如果对象改变,需要连带更新其标准形式。
域比较的顺序可能会影响equals方法的性能。为了获得最佳性能,应该首先比较哪些最可能不一致的域、开销最小的域。不应该比较哪些并非对象逻辑状态的域,例如
同步操作的Lock域。不应该比较冗余域,这些域可通过关键域计算而得,但是这样做可能提高equals方法的性能。如果冗余域代表了对整个对象的综述,那么对比这些域就能够节省比较开销。【例】例如,假设有个Polygon类,并缓存了面积域,如果两个Polygon的面积不等,那就不用再去比较他们的边和顶点了。
5)当你完成equals方法后,问自己三个问题:它是否是对称的、传递的、一致的?并且不仅是自问,还要编写单元测试来检验这些特性。如果不通过,找出原因,据此来修改equals方法。当然,equals还需要满徐其他两个特性(自反性、非空性),不过这两种特性通常会自动满足。
Item9中的PhoneNumber.equals就是根据上述诀窍编写的。下面是最后的一些说明:
当重写equals时必须重写hashCode(Item9)
不要让equals过度聪明。如果只简单地测试域是否相等,则不难满足equals约定;而当过度地寻求等价关系,则容易陷入麻烦之中。例如File类不应该将指向同一文件的符号链接当做相等对象来看待。所幸File类没有这样做。
在equals方法声明中不要将Object替换为其他类型。为了防止这种情况,应该在每次重写equals时都是用@Override。