Effective Java 2.0_中英文对照_Item 8

文章作者:Tyan
博客:noahsnail.com | CSDN | 简书

CHAPTER3 Methods Common to All Objects

ALTHOUGH Object is a concrete class, it is designed primarily for extension. All of its nonfinal methods (equals, hashCode, toString, clone, and finalize) have explicit general contracts because they are designed to be overridden. It is the responsibility of any class overriding these methods to obey their general contracts; failure to do so will prevent other classes that depend on the contracts (such as HashMap and HashSet) from functioning properly in conjunction with the class.

虽然Object是一个具体的类,但设计它的主要目的是为了扩展。它的所有非final方法(equalshashCodetoStringclonefinalize)都有明确的通用约定,因为设计它们的目的是为了重写。任何类都应该遵循通用约定重写这些方法;不这样做的话,依赖这些约定的其它类(例如HashMapHashSet)将无法结合这个类正确运行。

This chapter tells you when and how to override the nonfinal Object methods. The finalize method is omitted from this chapter because it was discussed in Item 7. While not an Object method, Comparable.compareTo is discussed in this chapter because it has a similar character.

会告本章诉你什么时候,怎样重写这些非final的Object方法。本章会忽略finalize方法,因为它在Item 7中已经讨论过了。虽然不是一个Object方法,但是这章仍会讨论Comparable.compareTo,因为它有一个类似的特性。

Item 8: Obey the general contract when overriding equals

Overriding the equals method seems simple, but there are many ways to get it wrong, and consequences can be dire. The easiest way to avoid problems is not to override the equals method, in which case each instance of the class is equal only to itself. This is the right thing to do if any of the following conditions apply:

重写equals方法看似简单,但许多方式都会导致错误,结果是非常可怕的。避免这些问题的最简单方式是不要重写equals方法,在这种情况下类的每个实例只等价于它本身。如果符合以下任何条件,这样做就是正确的:

  • Each instance of the class is inherently unique. This is true for classes such as Thread that represent active entities rather than values. The equals implementation provided by Object has exactly the right behavior for these classes.

  • 类的每个实例本质上都是唯一的。对于表示活动实体而不是表示值的类确实如此,例如Thread。对于这些类,Object提供的equals实现具有完全正确的行为。

  • You don’t care whether the class provides a “logical equality” test. For example, java.util.Random could have overridden equals to check whether two Random instances would produce the same sequence of random numbers going forward, but the designers didn’t think that clients would need or want this functionality. Under these circumstances, the equals implementation inherited from Object is adequate.

  • 不关心类是否提供“逻辑等价”的测试。例如,java.util.Random可以重写equals方法来检查两个Random实例是否会产生相同的随机数序列,但设计者认为客户不需要或者不想要这个功能。在这种情况下,从Object继承的equals实现就足够了。

  • A super class has already overridden equals,and the super class behavior is appropriate for this class. For example, most Set implementations inherit their equals implementation from AbstractSet, List implementations from AbstractList, and Map implementations from AbstractMap.

  • 超类已经重写了equals,超类的行为对于子类是合适的。例如,大多数Set实现从AbstractSet继承了equals实现,List实现从AbstractList继承了equals实现,Map实现从AbstractMap继承了equals实现。

  • The class is private or package-private,and you are certain that its equals method will never be invoked. Arguably, the equals method should be overridden under these circumstances, in case it is accidentally invoked:

  • 类是私有的或包私有的,可以确定它的equals方法从不会被调用。可以说,在这些情况下equals方法应该重写,以防它被偶然调用:

1
2
3
@Override public boolean equals(Object o) {
throw new AssertionError(); // Method is never called
}

So when is it appropriate to override Object.equals? When a class has a notion of logical equality that differs from mere object identity, and a superclass has not already overridden equals to implement the desired behavior. This is generally the case for value classes. A value class is simply a class that represents a value, such as Integer or Date. A programmer who compares references to value objects using the equals method expects to find out whether they are logically equivalent, not whether they refer to the same object. Not only is overriding the equals method necessary to satisfy programmer expectations; it enables instances to serve as map keys or set elements with predictable, desirable behavior.

什么时候重写Object.equals方法是合适的?如果类具有逻辑等的概念,不同于对象同一性,并且超类没有重写equals方法来实现要求的行为,这时候就需要重写equals方法。这种情况通常是对值类而言的。值类仅仅是表示值的类,例如IntegerDate。程序员用equals方法比较值对象的引用,期望找出它们是否是逻辑等价的,而不管它们是否是同一对象。重写equals方法不仅满足了程序员的期望;它也能使实例作为映射表的主键或者集合的元素,使它们表现出可预期的行为。

One kind of value class that does not require the equals method to be overridden is a class that uses instance control (Item 1) to ensure that at most one object exists with each value. Enum types (Item 30) fall into this category. For these classes, logical equality is the same as object identity, so Object’s equals method functions as a logical equals method.

有一种不需要重写equals方法的值类,它通过实例控制(Item 1)来确保每个值至多存在一个对象。枚举类型(Item 30)就是这种类。对于这种类而言,逻辑等价等同与对象同一性,Objectequals方法在功能上就如同逻辑等价方法。

When you override the equals method, you must adhere to its general contract. Here is the contract, copied from the specification for Object [JavaSE6]:

当你重写equals方法时,你必须遵循通用约定。下面是约定内容,从Object规范[JavaSE6]中拷贝的:

The equals method implements an equivalence relation. It is:

  • Reflexive:For any non-null reference value x,x.equals(x) must return true.

  • Symmetric:For any non-null reference values x and y,x.equals(y) must return true if and only if y.equals(x) returns true.

  • Transitive:For any non-null reference values x,y,z,if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) must return true.

  • Consistent: For any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.

  • For any non-null reference value x,x.equals(null) must return false.

equals实现了一种等价关系。它是:

  • 自反性:对于任何非空引用值xx.equals(x)必须返回true

  • 对称性:对于任何非空引用值xyx.equals(y)必须返回true当且仅当y.equals(x)返回true

  • 传递性:对于任何非空引用值,xyz,如果x.equals(y)返回true并且y.equals(z)返回true,则x.equals(z)必须返回true

  • 一致性:对于任何非空引用值xyx.equals(y)的多次调用一致返回true或一致返回false,假设对象进行equals比较时没有修改任何信息。

  • 对于非空引用值xx.equals(null)必须返回false

Unless you are mathematically inclined, this might look a bit scary, but do not ignore it! If you violate it, you may well find that your program behaves erratically or crashes, and it can be very difficult to pin down the source of the failure. To paraphrase John Donne, no class is an island. Instances of one class are frequently passed to another. Many classes, including all collections classes, depend on the objects passed to them obeying the equals contract.

除非你擅长数学,否则这可能看起来有点可怕,但不要忽视它!如果你违反了它,你可能会发现你的程序表现不正常或程序崩溃,并且很难确定失败的来源。用John Donne的话来说,没有类是孤立的。一个类的实例频繁传递给另一个类。许多类,包括所有的集合类,都依赖于传递给它们的对象遵循equals约定。

Now that you are aware of the dangers of violating the equals contract, let’s go over the contract in detail. The good news is that, appearances notwithstanding, the contract really isn’t very complicated. Once you understand it, it’s not hard to adhere to it. Let’s examine the five requirements in turn:

现在你已经意识到了违反了equals约定的危险,让我们详细回顾一下这个约定。好消息是实际上这个约定并不复杂,尽管从表面上来看不是这样。一旦你理解了它,遵循它并不难。让我们依次检查这五个要求:

Reflexivity—The first requirement says merely that an object must be equal to itself. It is hard to imagine violating this requirement unintentionally. If you were to violate it and then add an instance of your class to a collection, the collection’s contains method might well say that the collection didn’t contain the instance that you just added.

自反性——第一个要求仅仅是说一个对象必须等价于它本身。很难想象会无意的违反这个要求。如果你违反了它并将你的类实例添加到一个集合中,集合的contains方法可能会说这个集合中不包含你刚刚添加的实例。

Symmetry—The second requirement says that any two objects must agree on whether they are equal. Unlike the first requirement, it’s not hard to imagine violating this one unintentionally. For example, consider the following class, which implements a case-insensitive string. The case of the string is preserved by toString but ignored in comparisons:

对称性——第二个要求是说任何两个对象必须对它们是否相等达成一致。不像第一个要求,不难想象会无意的违反这个要求。例如,考虑下面的类,它实现了不区分大小写的字符串。字符串保存在toString中,但在比较时被忽略了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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;
}
... // Remainder omitted
}

The well-intentioned equals method in this class naively attempts to interoperate with ordinary strings. Let’s suppose that we have one case-insensitive string and one ordinary one:

这个类中,equals方法的意图很好,单纯的想要与普通的字符串进行互操作。假设我们有一个区分大小写的字符串和一个普通的字符串:

1
2
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";

As expected, cis.equals(s) returns true. The problem is that while the equals method in CaseInsensitiveString knows about ordinary strings, the equals method in String is oblivious to case-insensitive strings. Therefore s.equals(cis) returns false, a clear violation of symmetry. Suppose you put a case-insensitive string into a collection:

正如预料的那样,cis.equals(s)返回true。问题是虽然CaseInsensitiveString中的equals知道普通的字符串,但是String中的equals方法不注意不区分大小写的字符串。因此s.equals(cis)返回false,这明显违反了对称性。假设你将一个不区分大小写的字符串放到一个集合中:

1
2
List<CaseInsensitiveString> list = new ArrayList<CaseInsensitiveString>();
list.add(cis);

What does list.contains(s) return at this point? Who knows? In Sun’s current implementation, it happens to return false, but that’s just an implementation artifact. In another implementation, it could just as easily return true or throw a runtime exception. Once you’ve violated the equals contract, you simply don’t know how other objects will behave when confronted with your object.

这时list.contains(s)会返回什么?谁知道呢?在Sun当前的实现中,它碰巧会返回false,但那仅是一种实现方案。在另一种实现中,它也可能很容易的返回true或抛出一个运行时异常。一旦你违反了equals约定,当面对你的对象时,你根本不指定其它的对象行为会怎样。

To eliminate the problem, merely remove the ill-conceived attempt to interoperate with String from the equals method. Once you do this, you can refactor the method to give it a single return:

为了消除这个问题,只要从equals方法中移除与String进行交互的,考虑不周的尝试即可。一旦你这样做了,你可以重构这个方法给它一个返回即可:

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

Transitivity—The third requirement of the equals contract says that if one object is equal to a second and the second object is equal to a third, then the first object must be equal to the third. Again, it’s not hard to imagine violating this requirement unintentionally. Consider the case of a subclass that adds a new value component to its superclass. In other words, the subclass adds a piece of information that affects equals comparisons. Let’s start with a simple immutable two-dimensional integer point class:

传递性——equals约定的第三个要求是说如果一个对象等价于第二个对象,而第二个对象等价于第三个对象,则第一个对象等价于第三个对象。同样的,不难想象会无意中违反这个要求。考虑这样一种情况,子类添加一个新的值组件到它的超类中。换句话说,子类添加的信息会影响equals比较。以一个简单的不可变的二维整数点类作为开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
... // Remainder omitted
}

Suppose you want to extend this class, adding the notion of color to a point:

假设你想扩展这个类,给点添加颜色的概念:

1
2
3
4
5
6
7
8
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
... // Remainder omitted
}

How should the equals method look? If you leave it out entirely, the implementation is inherited from Point and color information is ignored in equals comparisons. While this does not violate the equals contract, it is clearly unacceptable. Suppose you write an equals method that returns true only if its argument is another color point with the same position and color:

equals方法应该看起来是怎样的?如果一点也不修改,直接从Point继承equals方法,在进行equals比较时颜色信息会被忽略。虽然这没有违反equals约定,但很明显这是不可接受的。假设你写了一个equals方法,只有在它的参数是另一个有色点,且它们具有相同的位置和颜色时才返回true

1
2
3
4
5
6
7
// Broken - violates symmetry!
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}

The problem with this method is that you might get different results when comparing a point to a color point and vice versa. The former comparison ignores color, while the latter comparison always returns false because the type of the argument is incorrect. To make this concrete, let’s create one point and one color point:

这个方法的问题在于:当你比较一个普通点和一个有色点或相反的情况时,你可能会得到不同的结果。前者的比较忽略了颜色,而后者总是返回false,因为参数类型不正确。为了使这个更具体一点,我们创建一个普通点和一个有色点:

1
2
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);

Then p.equals(cp) returns true, while cp.equals(p) returns false. You might try to fix the problem by having ColorPoint.equals ignore color when doing “mixed comparisons”:

p.equals(cp)返回true,而cp.equals(p)返回false。你可能想让ColorPoint.equals进行比较混合比较时忽略颜色来修正这个问题:

1
2
3
4
5
6
7
8
9
10
11
// Broken - violates transitivity!
@Override
public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
// If o is a normal Point, do a color-blind comparison
if (!(o instanceof ColorPoint))
return o.equals(this);
// o is a ColorPoint; do a full comparison
return super.equals(o) && ((ColorPoint)o).color == color;
}

This approach does provide symmetry, but at the expense of transitivity:

这个方法提供了对称性,但违反了传递性:

1
2
3
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

Now p1.equals(p2) and p2.equals(p3) return true, while p1.equals(p3) returns false, a clear violation of transitivity. The first two comparisons are “color-blind,” while the third takes color into account.

现在p1.equals(p2)p2.equals(p3)返回true,而p1.equals(p3)返回false,很明显这违反了传递性。前两个比较忽略了颜色,而第三个比较考虑了颜色。

So what’s the solution? It turns out that this is a fundamental problem of equivalence relations in object-oriented languages. There is no way to extend an instantiable class and add a value component while preserving the equals contract, unless you are willing to forgo the benefits of object-oriented abstraction.

因此解决方案是什么?事实证明:在面向对象语言中,等价关系问题是一个基本的问题。当保留equals约定时,你无法在扩展一个实例化的类的同时添加值组件,除非你愿意放弃面向对象抽象的优势。

You may hear it said that you can extend an instantiable class and add a value component while preserving the equals contract by using a getClass test in place of the instanceof test in the equals method:

你可能听说过你可以在equals方法中通过使用getClass测试代替instanceof测试,从而在扩展一个可实例化的类并添加值组件的同时,保留equals约定:

1
2
3
4
5
6
7
8
// Broken - violates Liskov substitution principle (page 40)
@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != getClass())
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}

This has the effect of equating objects only if they have the same implementation class. While this may not seem so bad, the consequences are unacceptable.

当且仅当它们具有相同的实现类时,上面的代码在比较对象时才会有效。虽然这不是很糟糕,但结果是不可接受的。

Let’s suppose we want to write a method to tell whether an integer point is on the unit circle. Here is one way we could do it:

假设我们想写一个方法来判断一个整数点是否在单位圆上。下面是一种写法:

1
2
3
4
5
6
7
8
9
10
11
// 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);
}

While this may not be the fastest way to implement the functionality, it works fine. But suppose you extend Point in some trivial way that doesn’t add a value component, say, by having its constructor keep track of how many instances have been created:

虽然这可能不是实现这个功能的最快方式,但它确实有效。但假设你以某种不添加值组件的方式扩展了Point,例如通过它的构造函数来追踪创建了多少实例:

1
2
3
4
5
6
7
8
9
10
11
12
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();
}
}

The Liskov substitution principle says that any important property of a type should also hold for its subtypes, so that any method written for the type should work equally well on its subtypes [Liskov87]. But suppose we pass a CounterPoint instance to the onUnitCircle method. If the Point class uses a getClass based equals method, the onUnitCircle method will return false regardless of the CounterPoint instance’s x and y values. This is so because collections, such as the HashSet,used by the onUnitCircle method, use the equals method to test for containment, and no CounterPoint instance is equal to any Point. If, however, you use a proper instanceof-based equals method on Point, the same onUnitCircle method will work fine when presented with a CounterPoint.

里氏替换原则认为,一个类型的任何重要属性也适用于它的子类型,因此该类型编写的任何方法在它的子类型中也都应该工作良好[Liskov87]。但假设我们给onUnitCircle传递了一个CounterPoint实例。如果Point类使用了基于getClassequals方法,onUnitCircle将会返回false,无论CounterPoint实例的x值和y值是多少。这是因为集合,例如onUnitCircle方法中的HashSet,使用equals方法来测试是否包含元素,没有CounterPoint实例等于Point。然而,如果你在Point上使用合适的基于instanceofequals方法,当面对CounterPoint时,同样的onUnitCircle方法会工作的很好。

While there is no satisfactory way to extend an instantiable class and add a value component, there is a fine workaround. Follow the advice of Item 16, “Favor composition over inheritance.” Instead of having ColorPoint extend Point, give ColorPoint a private Point field and a public view method (Item 5) that returns the point at the same position as this color point:

尽管没有令人满意的方式来扩展一个可实例化的类并添加值组件,但有一个很好的解决方案。遵循Item 16 “Favor composition over inheritance”的建议,不再让ColorPoint继承Point,而是通过在ColorPoint中添加一个私有的Point字段和一个公有的视图方法(Item 5),此方法返回一个与有色点具有相同位置的普通点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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
}

There are some classes in the Java platform libraries that do extend an instantiable class and add a value component. For example, java.sql.Timestamp extends java.util.Date and adds a nanoseconds field. The equals implementation for Timestamp does violate symmetry and can cause erratic behavior if Timestamp and Date objects are used in the same collection or are otherwise intermixed. The Timestamp class has a disclaimer cautioning programmers against mixing dates and timestamps. While you won’t get into trouble as long as you keep them separate, there’s nothing to prevent you from mixing them, and the resulting errors can be hard to debug. This behavior of the Timestamp class was a mistake and should not be emulated.

在Java平台库中有一些类扩展了一个可实例化的类并添加了一个值组件。例如,java.sql.Timestamp扩展了java.util.Date并添加了一个nanoseconds字段。Timestampequals实现确实违反了对称性,如果TimestampDate用在同一个集合中或混杂在一起,会引起不稳定的行为。Timestamp类有一个免责声明,警告程序员不要混合日期和时间戳。虽然只要你将它们分开就不会有麻烦,但是没有任何东西阻止你混合它们,而且产生的错误很难调试。Timestamp类的这个行为是一个错误,不应该进行模仿。

Note that you can add a value component to a subclass of an abstract class without violating the equals contract. This is important for the sort of class hierarchies that you get by following the advice in Item 20, “Prefer class hierarchies to tagged classes.” For example, you could have an abstract class Shape with no value components, a subclass Circle that adds a radius field, and a subclass Rectangle that adds length and width fields. Problems of the sort shown above won’t occur so long as it is impossible to create a superclass instance directly.

注意,你可以添加值组件到抽象类的子类而且不会违反equals约定。对于遵循Item 20 “Prefer class hierarchies to tagged classes”的建议而得到这种类层次来说,这是非常重要的。例如,你可以有一个没有值组件的抽象类Shape,子类Circle添加了radius字段,子类Rectangle添加了lengthwidth字段。只要不能直接创建一个超类实例,上面的种种问题就不会发生。

Consistency—The fourth requirement of the equals contract says that if two objects are equal, they must remain equal for all time unless one (or both) of them is modified. In other words, mutable objects can be equal to different objects at different times while immutable objects can’t. When you write a class, think hard about whether it should be immutable (Item 15). If you conclude that it should, make sure that your equals method enforces the restriction that equal objects remain equal and unequal objects remain unequal for all time.

一致性——equals约定的第四个要求是说如果两个对象相等,它们必须一致相等,除非其中一个(或二者)被修改了。换句话说,可变对象在不同的时间可以等于不同的对象而不可变对象不能。当你写了一个类,仔细想想它是否应该是不可变的(Item 15)。如果你推断它应该是不可变的,那么要确保你的equals方法满足这样的约束条件:相等的对象永远相等,不等的对象永远不等。

Whether or not a class is immutable, do not write an equals method that depends on unreliable resources. It’s extremely difficult to satisfy the consistency requirement if you violate this prohibition. For example, java.net.URL’s equals method relies on comparison of the IP addresses of the hosts associated with the URLs. Translating a host name to an IP address can require network access, and it isn’t guaranteed to yield the same results over time. This can cause the URL equals method to violate the equals contract and has caused problems in practice. (Unfortunately, this behavior cannot be changed due to compatibility requirements.) With very few exceptions, equals methods should perform deterministic computations on memory-resident objects.

无论一个类是否是不可变的,都不要写一个依赖于不可靠资源的equals方法。如果你违反了这个禁令,要满足一致性要求是非常困难的。例如,java.net.URLequals方法依赖于对关联URL主机的IP地址的比较。将主机名转换成IP地址可能需要访问网络,随时间推移它不能保证取得相同的结果。这可能会导致URL equals方法违反equals约定并在实践中产生问题。(很遗憾,由于兼容性问题,这一行为不能被修改。)除了极少数例外,equals方法应该对常驻内存对象进行确定性计算。

Non-nullity”—The final requirement, which in the absence of a name I have taken the liberty of calling “non-nullity,” says that all objects must be unequal to null. While it is hard to imagine accidentally returning true in response to the invocation o.equals(null), it isn’t hard to imagine accidentally throwing a NullPointerException. The general contract does not allow this. Many classes
have equals methods that guard against this with an explicit test for null:

非空性”——最后的要求由于没有名字我称之为“非空性”,这个要求是说所有的对象都不等于null。虽然很难想象调用o.equals(null)会偶然的返回true,但不难想象会意外抛出NullPointerException的情况。通用约定不允许出现这种情况。许多类的equals方法为了防止出现这种情况都进行对null的显式测试:

1
2
3
4
5
6
@Override 
public boolean equals(Object o) {
if (o == null)
return false;
...
}

This test is unnecessary. To test its argument for equality, the equals method must first cast its argument to an appropriate type so its accessors may be invoked or its fields accessed. Before doing the cast, the method must use the instanceof operator to check that its argument is of the correct type:

这个测试是没必要的。为了平等测试其参数,为了调用它的访问器或访问其字段,equals方法首先必须将它的参数转换成合适的类型。在进行转换之前,equals方法必须使用instanceof操作符来检查它的参数是否是正确的类型:

1
2
3
4
5
6
7
@Override 
public boolean equals(Object o) {
if (!(o instanceof MyType))
return false;
MyType mt = (MyType) o;
...
}

If this type check were missing and the equals method were passed an argument of the wrong type, the equals method would throw a ClassCastException, which violates the equals contract. But the instanceof operator is specified to return false if its first operand is null, regardless of what type appears in the second operand [JLS, 15.20.2]. Therefore the type check will return false if null is passed in, so you don’t need a separate null check.

如果缺少类型检查,equals方法传入了一个错误类型的参数,equals方法会抛出ClassCastException,这违反了equals约定。但当指定instanceof时,如果它的第一个操作数为null,无论它的第二个操作数是什么类型,它都会返回false[JLS, 15.20.2]。所以如果传入null类型检查将会返回false,因此你不必进行单独的null检查。

Putting it all together, here’s a recipe for a high-quality equals method:

  1. Use the == operator to check if the argument is a reference to this object. If so, return true. This is just a performance optimization, but one that is worth doing if the comparison is potentially expensive.

  2. Use the instanceof operator to check if the argument has the correct type. If not, return false. Typically, the correct type is the class in which the method occurs. Occasionally, it is some interface implemented by this class. Use an interface if the class implements an interface that refines the equals contract to permit comparisons across classes that implement the interface. Collection interfaces such as Set, List, Map, and Map.Entry have this property.

  3. Cast the argument to the correct type. Because this cast was preceded by an instanceof test, it is guaranteed to succeed.

  4. For each “significant” field in the class, check if that field of the argument matches the corresponding field of this object. If all these tests succeed, return true; otherwise, return false. If the type in step 2 is an interface, you must access the argument’s fields via interface methods; if the type is a class, you may be able to access the fields directly, depending on their accessibility.

将上面所有的内容放在一起,下面是编写一个高质量equals方法的流程:

  1. 使用==操作符来检查参数是否是这个对象的一个引用,。如果是,返回true。这只是一个性能优化,如果比较的代价有可能很昂贵,这样做是值得的。

  2. 使用instanceof操作符来检查参数类型是否正确。如果不正确,返回false。通常,正确的类型是指equals方法所在的那个类。有时候,它是这个类实现的一些接口。如果一个类实现了一个接口,这个接口提炼了equals约定来允许比较那些实现了这个接口类,那么就使用接口。集合接口例如SetListMapMap.Entry都有这个属性。

  3. 将参数转换成正确的类型。由于转换测试已经被instanceof在之前做了,因此它保证能成功。

  4. 对于类中的每一个“有效”字段,检查参数的这个字段是否匹配这个对象的对应字段。如果所有的这些测试都成功了,返回true;否则返回false。如果第二步中的类型是一个接口,你必须通过接口方法访问参数的字段;如果类型是一个类,你可能要直接访问字段,依赖于它们的可访问性。

For primitive fields whose type is not float or double, use the == operator for comparisons; for object reference fields, invoke the equalsmethod recursively; for float fields, use the Float.compare method; and for double fields, use Double.compare. The special treatment of float and double fields is made necessary by the existence of Float.NaN, -0.0f and the analogous double constants; see the Float.equals documentation for details. For array fields, apply these guidelines to each element. If every element in an array field is significant, you can use one of the Arrays.equals methods added in release 1.5.

对于基本类型,如果不是floatdouble,使用==操作符进行比较;对于对象引用字段,递归地调用equals方法;对于float自动,使用Float.compare方法;对于double字段,使用Double.comparefloatdouble字段的特别对待是有必要的,因为存在Float.NaN-0.0f和类似的double常量;更多细节请看Float.equals。对于数组字段,对每个元素应用这些指导。如果数组中的每个元素都是有意义的,你可以使用1.5版本中添加的Arrays.equals方法。

Some object reference fields may legitimately contain null. To avoid the possibility of a NullPointerException, use this idiom to compare such fields:

某些对象引用字段可能合理的包含null。为了避免产生NullPointerException的可能性,使用下面的习惯用法来比较这些字段:

1
(field == null ? o.field == null : field.equals(o.field))

This alternative may be faster if field and o.field are often identical:

如果fieldo.field经常是等价的,使用下面的可替代方式可能会更快:

1
(field == o.field || (field != null && field.equals(o.field)))

For some classes, such as CaseInsensitiveString above, field comparisons are more complex than simple equality tests. If this is the case, you may want to store a canonical form of the field, so the equals method can do cheap exact comparisons on these canonical forms rather than more costly inexact comparisons. This technique is most appropriate for immutable classes (Item 15); if the object can change, you must keep the canonical form up to date.

对于某些类而言,例如上面的CaseInsensitiveString,字段比较比简单的相等性检测更复杂。如果是这种情况,你可能想存储这个字段的标准形式,因此equals方法可以在这些标准形式上进行低开销的精确比较,而不是更高代码的非精确比较。这种技术最适合不可变类(Item 15);如果对象可以改变,你必须保持最新的标准形式。

The performance of the equals method may be affected by the order in which fields are compared. For best performance, you should first compare fields that are more likely to differ, less expensive to compare, or, ideally, both. You must not compare fields that are not part of an object’s logical state, such as Lock fields used to synchronize operations. You need not compare redundant fields, which can be calculated from “significant fields,” but doing so may improve the performance of the equals method. If a redundant field amounts to a summary description of the entire object, comparing this field will save you the expense of comparing the actual data if the comparison fails. For example, suppose you have a Polygon class, and you cache the area. If two polygons have unequal areas, you needn’t bother comparing their edges and vertices.

equals方法的性能可能会受到字段比较顺序的影响。为了最佳性能,你首先应该比较那些更可能不同,比较代价更小的字段,或者理想情况下二者兼具的字段。你不能比较那些不属于对象逻辑状态一部分的字段,例如同步操作中的Lock字段。你也不需要比较冗余的字段,它们能从“有意义字段”中计算出来,但这样做可能会改善equals方法的性能。如果冗余字段相当于整个对象的概要描述,比较这个字段,如果失败的话会节省你比较真正数据的开销。例如,假设你有一个Polygon类,并且你缓存这个区域。如果两个多边形有不同的面积,你就不需要比较它们的边和顶点。

  1. When you are finished writing your equals method, ask yourself three questions: Is it symmetric? Is it transitive? Is it consistent? And don’t just ask yourself; write unit tests to check that these properties hold! If they don’t, figure out why not, and modify the equals method accordingly. Of course your equals method also has to satisfy the other two properties (reflexivity and “non-nullity”), but these two usually take care of themselves.

  2. 当你完成了equals方法的编写时,问你自己三个问题:它是否是对称的?是否是可传递的?是否是一致的?并且不要只问你自己;编写单元测试来检查是否拥有这些属性!如果没有这些属性,弄清楚为什么没有,对应的修改equals方法。当然你的equals方法也必须满足其它两个属性(自反性和“非空性”),但这两个属性通常会自动满足。

For a concrete example of an equals method constructed according to the above recipe, see PhoneNumber.equals in Item 9. Here are a few final caveats:

根据上述规则构建的equals方法具体例子请看Item 9的PhoneNumber.equals`。下面是一些最后的警告:

  • Always override hashCode when you override equals(Item9).

  • 当你重写equals时,总是重写hashCode方法(Item9)

  • Don’t try to be too clever. If you simply test fields for equality, it’s not hard to adhere to the equals contract. If you are overly aggressive in searching for equivalence, it’s easy to get into trouble. It is generally a bad idea to take any form of aliasing into account. For example, the File class shouldn’t attempt to equate symbolic links referring to the same file. Thankfully, it doesn’t.

  • 不要试图自作聪明。如果你简单的测试字段的相等性,不难遵循equals约定。如果过度的追求等价关系,很容易陷入到麻烦中。考虑任何形式的别名通常不是一个好想法。例如,File类不应该试图把指向同名的符号链接看作相等。所幸它没有这样做。

  • Don’t substitute another type for Object in the equals declaration.It is not uncommon for a programmer to write an equals method that looks like this, and then spend hours puzzling over why it doesn’t work properly:

  • 不要将equals声明中的Object对象替换为其它对象。对于程序员来讲,写一个equals方法看起来像下面的一样是不常见的,并且花费了好几个小时都不明白它为什么不能正确工作:

1
2
3
public boolean equals(MyClass o) {
...
}

The problem is that this method does not override Object.equals, whose argument is of type Object, but overloads it instead (Item 41). It is acceptable to provide such a “strongly typed” equals method in addition to the normal one as long as the two methods return the same result, but there is no compelling reason to do so. It may provide minor performance gains under certain circumstances, but it isn’t worth the added complexity (Item 55).

这个问题在于这个方法没有重写Object.equals方法,Object.equals方法的参数类型是Object,但相反,它重载了equals方法(Item 41)。除了正常的equals方法之外,提供这样一个“强类型”equals方法是可接受的,只要这两个方法返回同样的结果,但没有令人信服的理由去这样做。在某些特定环境下它可能会提供很小的收益,但相对于增加的复杂性来讲是不值得的(Item 55)。

Consistent use of the @Override annotation, as illustrated throughout this item, will prevent you from making this mistake (Item 36). This equals method won’t compile and the error message will tell you exactly what is wrong:

正如本条目阐述的那样,@Override注解的一致使用会阻止你犯这个错误(Item 36)。这个equals方法不能编译并且错误信息会确切告诉你错误是什么。

1
2
3
4
@Override 
public boolean equals(MyClass o) {
...
}
如果有收获,可以请我喝杯咖啡!