Item 10: Obey the general contract when overriding equals(覆蓋 equals 方法時(shí)應(yīng)遵守的約定)

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 方法似乎很簡(jiǎn)單,但是有很多覆蓋的方式會(huì)導(dǎo)致出錯(cuò),而且后果可能非常嚴(yán)重。避免問題的最簡(jiǎn)單方法是不覆蓋 equals 方法,在這種情況下,類的每個(gè)實(shí)例都只等于它自己。如果符合下列任何條件,就是正確的做法:

  • 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.

類的每個(gè)實(shí)例本質(zhì)上都是唯一的。 對(duì)于像 Thread 這樣表示活動(dòng)實(shí)體類而不是值類來說也是如此。Object 提供的 equals 實(shí)現(xiàn)對(duì)于這些類具有完全正確的行為。

  • There is no need for the class to provide a “l(fā)ogical equality” test. For example, java.util.regex.Pattern could have overridden equals to check whether two Pattern instances represented exactly the same regular expression, but the designers didn’t think that clients would need or want this functionality. Under these circumstances, the equals implementation inherited from Object is ideal.

該類不需要提供「邏輯相等」測(cè)試。 例如,java.util.regex.Pattern 可以覆蓋 equals 來檢查兩個(gè) Pattern 實(shí)例是否表示完全相同的正則表達(dá)式,但設(shè)計(jì)人員認(rèn)為客戶端不需要或不需要這個(gè)功能。在這種情況下,從 Object 繼承的 equals 實(shí)現(xiàn)是理想的。

  • A superclass has already overridden equals, and the superclass 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.

超類已經(jīng)覆蓋了 equals,超類行為適合于這個(gè)類。 例如,大多數(shù) Set 的實(shí)現(xiàn)從 AbstractSet 繼承其對(duì)等實(shí)現(xiàn),List 從 AbstractList 繼承實(shí)現(xiàn),Map 從 AbstractMap 繼承實(shí)現(xiàn)。

  • The class is private or package-private, and you are certain that its equals method will never be invoked. If you are extremely risk-averse,you can override the equals method to ensure that it isn’t invoked accidentally:

類是私有的或包私有的,并且你確信它的 equals 方法永遠(yuǎn)不會(huì)被調(diào)用。 如果你非常厭惡風(fēng)險(xiǎn),你可以覆蓋 equals 方法,以確保它不會(huì)意外調(diào)用:

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

So when is it appropriate to override equals? It is when a class has a notion of logical equality that differs from mere object identity and a superclass has not already overridden equals. This is generally the case for value classes. A value class is simply a class that represents a value, such as Integer or String. 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.

什么時(shí)候覆蓋 equals 方法是合適的?當(dāng)一個(gè)類有一個(gè)邏輯相等的概念,而這個(gè)概念不同于僅判斷對(duì)象的同一性(相同對(duì)象的引用),并且超類還沒有覆蓋 equals。對(duì)于值類通常是這樣。值類只是表示值的類,例如 Integer 或 String。使用 equals 方法比較引用和值對(duì)象的程序員希望發(fā)現(xiàn)它們?cè)谶壿嬌鲜欠竦葍r(jià),而不是它們是否引用相同的對(duì)象。覆蓋 equals 方法不僅是為了滿足程序員的期望,它還使實(shí)例能夠作為 Map 的鍵或 Set 元素時(shí),具有可預(yù)測(cè)的、理想的行為。

譯注 1:有一個(gè)表示狀態(tài)的內(nèi)部類。沒有覆蓋 equals 方法時(shí),equals 的結(jié)果與 s1==s2 相同,為 false,即兩者并不是相同對(duì)象的引用。

public static void main(String[] args) {

    class Status {
        public String status;
    }

    Status s1 = new Status();
    Status s2 = new Status();

    System.out.println(s1==s2); // false
    System.out.println(s1.equals(s2)); // false
}

譯注 2:覆蓋 equals 方法后,以業(yè)務(wù)邏輯來判斷是否相同,具備相同 status 字段即為相同。在使用去重功能時(shí),也以此作為判斷依據(jù)。

public static void main(String[] args) {

    class Status {
        public String status;

        @Override
        public boolean equals(Object o) {
            return Objects.equals(status, ((Status) o).status);
        }
    }

    Status s1 = new Status();
    Status s2 = new Status();

    System.out.println(s1==s2); // false
    System.out.println(s1.equals(s2)); // true
}

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 34) 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 方法的一種值類是使用實(shí)例控件(Item-1)來確保每個(gè)值最多只存在一個(gè)對(duì)象的類。枚舉類型(Item-34)屬于這一類。對(duì)于這些類,邏輯相等與對(duì)象標(biāo)識(shí)相同,因此對(duì)象的 equals 方法函數(shù)與邏輯 equals 方法相同。

When you override the equals method, you must adhere to its general contract. Here is the contract, from the specification(n.規(guī)范,說明書) for Object :

當(dāng)你覆蓋 equals 方法時(shí),你必須遵守它的通用約定。以下是具體內(nèi)容,來自 Object 規(guī)范:

The equals method implements an equivalence relation. It has these properties:

equals 方法實(shí)現(xiàn)了等價(jià)關(guān)系。它應(yīng)有這些屬性:

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

反身性:對(duì)于任何非空的參考值 x,x.equals(x) 必須返回 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.

對(duì)稱性:對(duì)于任何非空參考值 x 和 y,x.equals(y) 必須在且僅當(dāng) y.equals(x) 返回 true 時(shí)返回 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.

傳遞性:對(duì)于任何非空的引用值 x, y, z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,那么 x.equals(z) 必須返回 true。

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

一致性:對(duì)于任何非空的引用值 x 和 y, x.equals(y) 的多次調(diào)用必須一致地返回 true 或一致地返回 false,前提是不修改 equals 中使用的信息。

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

對(duì)于任何非空引用值 x,x.equals(null) 必須返回 false。

Unless you are mathematically inclined(v.使…傾向;adj.趨向于…的), 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.

除非你有數(shù)學(xué)方面的傾向,否則這些起來有點(diǎn)可怕,但不要忽略它!如果你違反了它,你的程序很可能會(huì)出現(xiàn)行為異常或崩潰,并且很難確定失敗的根源。用 John Donne 的話來說,沒有一個(gè)類是孤立的。一個(gè)類的實(shí)例經(jīng)常被傳遞給另一個(gè)類。許多類(包括所有集合類)依賴于傳遞給它們的對(duì)象遵守 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, it really isn’t very complicated. Once you understand it, it’s not hard to adhere to it.

既然你已經(jīng)意識(shí)到了違反 equals 約定的危險(xiǎn),讓我們?cè)敿?xì)討論一下。好消息是,盡管表面上看起來很復(fù)雜,但其實(shí)并不復(fù)雜。一旦你明白了,就不難堅(jiān)持下去了。

So what is an equivalence relation? Loosely speaking, it’s an operator that partitions a set of elements into subsets whose elements are deemed equal to one another. These subsets are known as equivalence classes. For an equals method to be useful, all of the elements in each equivalence class must be interchangeable from the perspective of the user. Now let’s examine the five requirements in turn:

什么是等價(jià)關(guān)系?簡(jiǎn)單地說,它是一個(gè)操作符,它將一組元素劃分為子集,子集的元素被認(rèn)為是彼此相等的。這些子集被稱為等價(jià)類。為了使 equals 方法有用,從用戶的角度來看,每個(gè)等價(jià)類中的所有元素都必須是可互換的?,F(xiàn)在讓我們依次檢查以下五個(gè)需求:

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

反身性 ,第一個(gè)要求僅僅是說一個(gè)對(duì)象必須等于它自己。很難想象會(huì)無意中違反了這條規(guī)則。如果你違反了它,然后將類的一個(gè)實(shí)例添加到集合中,contains 方法很可能會(huì)說該集合不包含你剛才添加的實(shí)例。

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 equals comparisons:

對(duì)稱性 ,第二個(gè)要求是任何兩個(gè)對(duì)象必須在是否相等的問題上達(dá)成一致。與第一個(gè)要求不同,無意中違反了這個(gè)要求的情況不難想象。例如,考慮下面的類,它實(shí)現(xiàn)了不區(qū)分大小寫的字符串。字符串的情況是保留的 toString,但忽略在 equals 的比較:

// Broken - violates symmetry!
public final class CaseInsensitiveString {
    private final String s;

    public CaseInsensitiveString(String s) {
        this.s = Objects.requireNonNull(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 caseinsensitive string and one ordinary one:

這個(gè)類中的 equals 方法天真地嘗試與普通字符串進(jìn)行互操作。假設(shè)我們有一個(gè)不區(qū)分大小寫的字符串和一個(gè)普通字符串:

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:

正如預(yù)期的那樣,cis.equals(s) 返回 true。問題是,雖然 CaseInsensitiveString 中的 equals 方法知道普通字符串,但是 String 中的 equals 方法對(duì)不區(qū)分大小寫的字符串不知情。因此,s.equals(cis) 返回 false,這明顯違反了對(duì)稱性。假設(shè)你將不區(qū)分大小寫的字符串放入集合中:

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

What does list.contains(s) return at this point? Who knows? In the current OpenJDK 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.

此時(shí) list.contains(s) 返回什么?誰知道呢?在當(dāng)前的 OpenJDK 實(shí)現(xiàn)中,它碰巧返回 false,但這只是一個(gè)實(shí)現(xiàn)案例。在另一個(gè)實(shí)現(xiàn)中,它可以很容易地返回 true 或拋出運(yùn)行時(shí)異常。一旦你違反了 equals 約定,就不知道當(dāng)其他對(duì)象面對(duì)你的對(duì)象時(shí),會(huì)如何表現(xiàn)。

譯注:contains 方法在 ArrayList 中的實(shí)現(xiàn)源碼如下(省略了源碼中的多行注釋):

// ArrayList 的大小
private int size;

// 保存 ArrayList 元素的容器,一個(gè) Object 數(shù)組
transient Object[] elementData; // non-private to simplify nested class access

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

public int indexOf(Object o) {
    return indexOfRange(o, 0, size);
}

int indexOfRange(Object o, int start, int end) {
    Object[] es = elementData;
    if (o == null) {
        for (int i = start; i < end; i++) {
            if (es[i] == null) {
                return i;
            }
        }
    } else {
        for (int i = start; i < end; i++) {
            if (o.equals(es[i])) {
                return i;
            }
        }
    }
    return -1;
}

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 into a single return statement:

為了消除這個(gè)問題,只需從 equals 方法中刪除與 String 互操作的錯(cuò)誤嘗試。一旦你這樣做了,你可以重構(gòu)方法為一個(gè)單一的返回語(yǔ)句:

@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 約定的第三個(gè)要求是,如果一個(gè)對(duì)象等于第二個(gè)對(duì)象,而第二個(gè)對(duì)象等于第三個(gè)對(duì)象,那么第一個(gè)對(duì)象必須等于第三個(gè)對(duì)象。同樣,無意中違反了這個(gè)要求的情況不難想象。考慮向超類添加新的值組件時(shí),子類的情況。換句話說,子類添加了一條影響 equals 比較的信息。讓我們從一個(gè)簡(jiǎn)單的不可變二維整數(shù)點(diǎn)類開始:

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:

假設(shè)你想繼承這個(gè)類,對(duì)一個(gè)點(diǎn)添加顏色的概念:

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 方法應(yīng)該是什么樣子?如果你完全忽略它,則實(shí)現(xiàn)將從 Point 類繼承而來,在 equals 比較中顏色信息將被忽略。雖然這并不違反 equals 約定,但顯然是不可接受的。假設(shè)你寫了一個(gè) equals 方法,該方法只有當(dāng)它的參數(shù)是另一個(gè)顏色點(diǎn),且位置和顏色相同時(shí)才返回 true:

// 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:

這種方法的問題是,當(dāng)你比較一個(gè)點(diǎn)和一個(gè)顏色點(diǎn)時(shí),你可能會(huì)得到不同的結(jié)果,反之亦然。前者比較忽略顏色,而后者比較總是返回 false,因?yàn)閰?shù)的類型是不正確的。為了使問題更具體,讓我們創(chuàng)建一個(gè)點(diǎn)和一個(gè)顏色點(diǎn):

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。當(dāng)你做「混合比較」的時(shí)候,你可以通過讓 ColorPoint.equals 忽略顏色來解決這個(gè)問題:

// 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:

這種方法確實(shí)提供了對(duì)稱性,但犧牲了傳遞性:

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.

現(xiàn)在,p1.equals(p2)p2.equals(p3) 返回 true,而 p1.equals(p3) 返回 false,這明顯違反了傳遞性。前兩個(gè)比較是「色盲」,而第三個(gè)比較考慮了顏色。

Also, this approach can cause infinite recursion: Suppose there are two subclasses of Point, say ColorPoint and SmellPoint, each with this sort of equals method. Then a call to myColorPoint.equals(mySmellPoint) will throw a StackOverflowError.

同樣,這種方法會(huì)導(dǎo)致無限的遞歸:假設(shè)有兩個(gè)點(diǎn)的子類,比如 ColorPoint 和 SmellPoint,每個(gè)都使用這種 equals 方法。然后調(diào)用 myColorPoint.equals(mySmellPoint) 會(huì)拋出 StackOverflowError。

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’re willing to forgo the benefits of object-oriented abstraction.

那么解決方案是什么?這是面向?qū)ο笳Z(yǔ)言中等價(jià)關(guān)系的一個(gè)基本問題。除非你愿意放棄面向?qū)ο蟮某橄髢?yōu)點(diǎn),否則無法繼承一個(gè)可實(shí)例化的類并添加一個(gè)值組件,同時(shí)保留 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:

你可能會(huì)聽到它說你可以繼承一個(gè)實(shí)例化的類并添加一個(gè)值組件,同時(shí)通過在 equals 方法中使用 getClass 測(cè)試來代替 instanceof 測(cè)試來保持 equals 約定:

// Broken - violates Liskov substitution principle (page 43)
@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. This may not seem so bad, but the consequences are unacceptable: An instance of a subclass of Point is still a Point, and it still needs to function as one, but it fails to do so if you take this approach! Let’s suppose we want to write a method to tell whether a point is on the unit circle. Here is one way we could do it:

只有當(dāng)對(duì)象具有相同的實(shí)現(xiàn)類時(shí),才會(huì)產(chǎn)生相等的效果。這可能看起來不是很糟糕,但其后果是不可接受的:Point 的子類的實(shí)例仍然是一個(gè) Point,并且它仍然需要作為一個(gè)函數(shù)來工作,但是如果采用這種方法,它就不會(huì)這樣做!假設(shè)我們要寫一個(gè)方法來判斷一個(gè)點(diǎn)是否在單位圓上。我們可以這樣做:

// Initialize unitCircle to contain all Points on the unit circle
private static final Set<Point> unitCircle = Set.of(
        new Point( 1, 0), new Point( 0, 1),
        new Point(-1, 0), 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. 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:

雖然這可能不是實(shí)現(xiàn)功能的最快方法,但它工作得很好。假設(shè)你以一種不添加值組件的簡(jiǎn)單方式繼承 Point,例如,讓它的構(gòu)造函數(shù)跟蹤創(chuàng)建了多少實(shí)例:

public class CounterPoint extends Point {
    private static final AtomicInteger counter = new AtomicInteger();

    public CounterPoint(int x, int y) {
        super(x, y);
        counter.incrementAndGet();
    }

    public static int numberCreated() {
        return counter.get();
    }
}

The Liskov substitution principle says that any important property of a type should also hold for all its subtypes so that any method written for the type should work equally well on its subtypes [Liskov87]. This is the formal statement of our earlier claim that a subclass of Point (such as CounterPoint) is still a Point and must act as one. But suppose we pass a CounterPoint 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 coordinates. This is so because most collections, including 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 works fine when presented with a CounterPoint instance.

Liskov 替換原則指出,類型的任何重要屬性都應(yīng)該適用于所有子類型,因此為類型編寫的任何方法都應(yīng)該在其子類型上同樣有效 [Liskov87]。這是我們先前做的正式聲明,即點(diǎn)的子類(如 CounterPoint)仍然是一個(gè) Point,并且必須作為一個(gè) Point。但假設(shè)我們傳遞了一個(gè) CounterPoint 給 onUnitCircle 方法。如果 Point 類使用基于 getclass 的 equals 方法,那么不管 CounterPoint 實(shí)例的 x 和 y 坐標(biāo)如何,onUnitCircle 方法都會(huì)返回 false。這是因?yàn)榇蠖鄶?shù)集合,包括 onUnitCircle 方法使用的 HashSet,都使用 equals 方法來測(cè)試包含性,沒有一個(gè) CounterPoint 實(shí)例等于任何一個(gè)點(diǎn)。但是,如果你在 Point 上使用了正確的基于實(shí)例的 equals 方法,那么在提供對(duì)位實(shí)例時(shí),相同的 onUnitCircle 方法就可以很好地工作。

譯注:里氏替換原則(Liskov Substitution Principle,LSP)面向?qū)ο笤O(shè)計(jì)的基本原則之一。里氏替換原則指出:任何父類可以出現(xiàn)的地方,子類一定可以出現(xiàn)。LSP 是繼承復(fù)用的基石,只有當(dāng)衍生類可以替換掉父類,軟件單位的功能不受到影響時(shí),父類才能真正被復(fù)用,而衍生類也能夠在父類的基礎(chǔ)上增加新的行為。

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 18,“Favor composition over inheritance.” Instead of having ColorPoint extend Point, give ColorPoint a private Point field and a public view method (Item 6) that returns the point at the same position as this color point:

雖然沒有令人滿意的方法來繼承一個(gè)可實(shí)例化的類并添加一個(gè)值組件,但是有一個(gè)很好的解決方案:遵循 Item-18 的建議,「Favor composition over inheritance.」。給 ColorPoint 一個(gè)私有的 Point 字段和一個(gè) public 視圖方法(Item-6),而不是讓 ColorPoint 繼承 Point,該方法返回與這個(gè)顏色點(diǎn)相同位置的點(diǎn):

// 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) {
        point = new Point(x, y);
        this.color = Objects.requireNonNull(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 庫(kù)中有一些類確實(shí)繼承了一個(gè)可實(shí)例化的類并添加了一個(gè)值組件。例如,java.sql.Timestamp 繼承 java.util.Date 并添加了納秒字段。如果在同一個(gè)集合中使用時(shí)間戳和日期對(duì)象,或者以其他方式混合使用時(shí)間戳和日期對(duì)象,那么時(shí)間戳的 equals 實(shí)現(xiàn)確實(shí)違反了對(duì)稱性,并且可能導(dǎo)致不穩(wěn)定的行為。Timestamp 類有一個(gè)免責(zé)聲明,警告程序員不要混合使用日期和時(shí)間戳。雖然只要將它們分開,就不會(huì)遇到麻煩,但是沒有什么可以阻止你將它們混合在一起,因此產(chǎn)生的錯(cuò)誤可能很難調(diào)試。時(shí)間戳類的這種行為是錯(cuò)誤的,不應(yīng)該效仿。

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 23, “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 earlier won’t occur so long as it is impossible to create a superclass instance directly.

注意,你可以向抽象類的子類添加一個(gè)值組件,而不違反 equals 約定。這對(duì)于遵循 Item-23 中的建議而得到的類層次結(jié)構(gòu)很重要,「Prefer class hierarchies to tagged classes.」。例如,可以有一個(gè)沒有值組件的抽象類形狀、一個(gè)添加半徑字段的子類圓和一個(gè)添加長(zhǎng)度和寬度字段的子類矩形。只要不可能直接創(chuàng)建超類實(shí)例,前面顯示的那種問題就不會(huì)發(fā)生。

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 17). 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.

一致性 ,對(duì)等約定的第四個(gè)要求是,如果兩個(gè)對(duì)象相等,它們必須一直保持相等,除非其中一個(gè)(或兩個(gè))被修改。換句話說,可變對(duì)象可以等于不同時(shí)間的不同對(duì)象,而不可變對(duì)象不能。在編寫類時(shí),仔細(xì)考慮它是否應(yīng)該是不可變的(Item-17)。如果你認(rèn)為應(yīng)該這樣做,那么請(qǐng)確保你的 equals 方法執(zhí)行了這樣的限制,即相等的對(duì)象始終是相等的,而不等的對(duì)象始終是不等的。

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. The behavior of URL’s equals method was a big mistake and should not be emulated. Unfortunately, it cannot be changed due to compatibility requirements. To avoid this sort of problem,equals methods should perform only deterministic computations on memoryresident objects.

無論一個(gè)類是否不可變,都不要編寫依賴于不可靠資源的 equals 方法。如果你違反了這個(gè)禁令,就很難滿足一致性要求。例如,java.net.URL 的 equals 方法依賴于與 url 相關(guān)聯(lián)的主機(jī)的 IP 地址的比較。將主機(jī)名轉(zhuǎn)換為 IP 地址可能需要網(wǎng)絡(luò)訪問,而且不能保證隨著時(shí)間的推移產(chǎn)生相同的結(jié)果。這可能會(huì)導(dǎo)致 URL 的 equals 方法違反約定,并在實(shí)踐中造成問題。URL 的 equals 方法的行為是一個(gè)很大的錯(cuò)誤,不應(yīng)該被模仿。不幸的是,由于兼容性需求,它不能更改。為了避免這種問題,equals 方法應(yīng)該只對(duì) memoryresident 對(duì)象執(zhí)行確定性計(jì)算。

Non-nullity— The final requirement lacks an official name, so I have taken the liberty of calling it “non-nullity.” It 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 prohibits this.Many classes have equals methods that guard against it with an explicit test for null:

非無效性 ,最后的要求沒有一個(gè)正式的名稱,所以我冒昧地稱之為「非無效性」。它說所有對(duì)象都不等于 null。雖然很難想象在響應(yīng)調(diào)用 o.equals(null) 時(shí)意外地返回 true,但不難想象意外地拋出 NullPointerException。一般約定中禁止這樣做。許多類都有相等的方法,通過顯式的 null 測(cè)試來防止它:

@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 can 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:

這個(gè)測(cè)試是不必要的。要測(cè)試其參數(shù)是否相等,equals 方法必須首先將其參數(shù)轉(zhuǎn)換為適當(dāng)?shù)念愋?,以便能夠調(diào)用其訪問器或訪問其字段。在執(zhí)行轉(zhuǎn)換之前,方法必須使用 instanceof 運(yùn)算符來檢查其參數(shù)的類型是否正確:

@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 an explicit null check.

如果缺少這個(gè)類型檢查,并且 equals 方法傳遞了一個(gè)錯(cuò)誤類型的參數(shù),equals 方法將拋出 ClassCastException,這違反了 equals 約定。但是,如果 instanceof 操作符的第一個(gè)操作數(shù)為空,則指定該操作符返回 false,而不管第二個(gè)操作數(shù) [JLS, 15.20.2] 中出現(xiàn)的是什么類型。因此,如果傳入 null,類型檢查將返回 false,因此不需要顯式的 null 檢查。

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

綜上所述,這里有一個(gè)高質(zhì)量構(gòu)建 equals 方法的秘訣:

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.

使用 == 運(yùn)算符檢查參數(shù)是否是對(duì)該對(duì)象的引用。 如果是,返回 true。這只是一種性能優(yōu)化,但如果比較的代價(jià)可能很高,那么這種優(yōu)化是值得的。

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.

使用 instanceof 運(yùn)算符檢查參數(shù)是否具有正確的類型。 如果不是,返回 false。通常,正確的類型是方法發(fā)生的類。有時(shí)候,它是由這個(gè)類實(shí)現(xiàn)的某個(gè)接口。如果類實(shí)現(xiàn)了一個(gè)接口,該接口對(duì) equals 約定進(jìn)行了改進(jìn),以允許跨實(shí)現(xiàn)該接口的類進(jìn)行比較,則使用該接口。集合接口,如 Set、List、Map 和 Map.Entry 具有此屬性。

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

將參數(shù)轉(zhuǎn)換為正確的類型。 因?yàn)樵谶@個(gè)強(qiáng)制類型轉(zhuǎn)換之前有一個(gè)實(shí)例測(cè)試,所以它肯定會(huì)成功。

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.

對(duì)于類中的每個(gè)「重要」字段,檢查參數(shù)的字段是否與該對(duì)象的相應(yīng)字段匹配。 如果所有這些測(cè)試都成功,返回 true;否則返回 false。如果第 2 步中的類型是接口,則必須通過接口方法訪問參數(shù)的字段;如果是類,你可以根據(jù)字段的可訪問性直接訪問它們。

For primitive fields whose type is not float or double, use the == operator for comparisons; for object reference fields, call the equals method recursively; for float fields, use the static Float.compare(float,float) method; and for double fields, use Double.compare(double, double). The special treatment of float and double fields is made necessary by the existence of Float.NaN, -0.0f and the analogous double values; see JLS 15.21.1 or the documentation of Float.equals for details. While you could compare float and double fields with the static methods Float.equals and Double.equals, this would entail autoboxing on every comparison, which would have poor performance. For array fields, apply these guidelines to each element. If every element in an array field is significant, use one of the Arrays.equals methods.

對(duì)于類型不是 float 或 double 的基本類型字段,使用 == 運(yùn)算符進(jìn)行比較;對(duì)于對(duì)象引用字段,遞歸調(diào)用 equals 方法;對(duì)于 float 字段,使用 static Float.compare(float,float) 方法;對(duì)于 double 字段,使用 Double.compare(double, double)。float 和 double 字段的特殊處理是由于 Float.NaN、-0.0f 和類似的雙重值的存在而必須的;請(qǐng)參閱 JLS 15.21.1 或 Float.equals 文檔。雖然你可以將 float 和 double 字段與靜態(tài)方法 Float.equals 和 Double.equals 進(jìn)行比較,這將需要在每個(gè)比較上進(jìn)行自動(dòng)裝箱,這將有較差的性能。對(duì)于數(shù)組字段,將這些指導(dǎo)原則應(yīng)用于每個(gè)元素。如果數(shù)組字段中的每個(gè)元素都很重要,那么使用 Arrays.equals 方法之一。

Some object reference fields may legitimately contain null. To avoid the possibility of a NullPointerException, check such fields for equality using the static method Objects.equals(Object, Object).

一些對(duì)象引用字段可能合法地包含 null。為了避免可能出現(xiàn) NullPointerException,請(qǐng)使用靜態(tài)方法 Objects.equals(Object, Object) 檢查這些字段是否相等。

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 a cheap exact comparison on canonical forms rather than a more costly nonstandard comparison. This technique is most appropriate for immutable classes (Item 17); if the object can change, you must keep the canonical form up to date.

對(duì)于某些類,例如上面的 CaseInsensitiveString,字段比較比簡(jiǎn)單的 equal 測(cè)試更復(fù)雜。如果是這樣,你可能希望存儲(chǔ)字段的規(guī)范形式,以便 equals 方法可以對(duì)規(guī)范形式進(jìn)行廉價(jià)的精確比較,而不是更昂貴的非標(biāo)準(zhǔn)比較。這種技術(shù)最適合于不可變類(Item-17);如果對(duì)象可以更改,則必須使規(guī)范形式保持最新。

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 derived fields, which can be calculated from “significant fields,” but doing so may improve the performance of the equals method. If a derived 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 方法的性能可能會(huì)受到字段比較順序的影響。為了獲得最佳性能,你應(yīng)該首先比較那些更可能不同、比較成本更低的字段,或者理想情況下兩者都比較。不能比較不屬于對(duì)象邏輯狀態(tài)的字段,例如用于同步操作的鎖字段。你不需要比較派生字段(可以從「重要字段」計(jì)算),但是這樣做可能會(huì)提高 equals 方法的性能。如果派生字段相當(dāng)于整個(gè)對(duì)象的摘要描述,那么如果比較失敗,比較該字段將節(jié)省比較實(shí)際數(shù)據(jù)的開銷。例如,假設(shè)你有一個(gè)多邊形類,你緩存這個(gè)區(qū)域。如果兩個(gè)多邊形的面積不相等,你不需要比較它們的邊和頂點(diǎn)。

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, unless you used AutoValue (page 49) to generate your equals method, in which case you can safely omit the tests. If the properties fail to hold, figure out why, and modify the equals method accordingly. Of course your equals method must also satisfy the other two properties (reflexivity and non-nullity), but these two usually take care of themselves.

寫完 equals 方法后,問自己三個(gè)問題:它具備對(duì)稱性嗎?具備傳遞性嗎?具備一致性嗎? 不要只問自己,要編寫單元測(cè)試來檢查,除非使用 AutoValue(第 49 頁(yè))來生成 equals 方法,在這種情況下,你可以安全地省略測(cè)試。如果屬性不能保持,請(qǐng)找出原因,并相應(yīng)地修改 equals 方法。當(dāng)然,equals 方法還必須滿足其他兩個(gè)屬性(反身性和非無效性),但這兩個(gè)通常會(huì)自己處理。

An equals method constructed according to the previous recipe(n.食譜,配方) is shown in this simplistic PhoneNumber class:

在這個(gè)簡(jiǎn)單的 PhoneNumber 類中,根據(jù)前面的方法構(gòu)造了一個(gè) equals 方法:

// Class with a typical equals method
public final class PhoneNumber {
    private final short areaCode, prefix, lineNum;

    public PhoneNumber(int areaCode, int prefix, int lineNum) {
        this.areaCode = rangeCheck(areaCode, 999, "area code");
        this.prefix = rangeCheck(prefix, 999, "prefix");
        this.lineNum = rangeCheck(lineNum, 9999, "line num");
    }

    private static short rangeCheck(int val, int max, String arg) {
        if (val < 0 || val > max)
            throw new IllegalArgumentException(arg + ": " + val);
        return (short) val;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;

        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNum == lineNum && pn.prefix == prefix && pn.areaCode == areaCode;
    } ... // Remainder omitted
}

Here are a few final caveats:

以下是一些最后的警告:

  • Always override hashCode when you override equals (Item 11).

當(dāng)你覆蓋 equals 時(shí),也覆蓋 hashCode。Item-11

  • 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.

不要自作聰明。 如果你只是為了判斷相等性而測(cè)試字段,那么遵循 equals 約定并不困難。如果你在尋求對(duì)等方面過于激進(jìn),很容易陷入麻煩。一般來說,考慮到任何形式的混疊都不是一個(gè)好主意。例如,F(xiàn)ile 類不應(yīng)該嘗試將引用同一文件的符號(hào)鏈接等同起來。值得慶幸的是,它不是。

  • 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 聲明中的對(duì)象。 對(duì)于程序員來說,編寫一個(gè)類似于這樣的 equals 方法,然后花上幾個(gè)小時(shí)思考為什么它不能正常工作是很常見的:

// Broken - parameter type must be Object!
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 52). It is unacceptable to provide such a “strongly typed” equals method even in addition to the normal one, because it can cause Override annotations in subclasses to generate false positives and provide a false sense of security.

這里的問題是,這個(gè)方法沒有覆蓋其參數(shù)類型為 Object 的 Object.equals,而是重載了它(Item-52)。即使是普通的方法,提供這樣一個(gè)「強(qiáng)類型的」equals 方法是不可接受的,因?yàn)樗鼤?huì)導(dǎo)致子類中的重寫注釋產(chǎn)生誤報(bào)并提供錯(cuò)誤的安全性。

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

如本條目所示,一致使用 Override 注釋將防止你犯此錯(cuò)誤(Item-40)。這個(gè) equals 方法不會(huì)編譯,錯(cuò)誤消息會(huì)告訴你什么是錯(cuò)誤的:

// Still broken, but won’t compile
@Override
public boolean equals(MyClass o) {
    ...
}

Writing and testing equals (and hashCode) methods is tedious, and the resulting code is mundane. An excellent alternative to writing and testing these methods manually is to use Google’s open source AutoValue framework, which automatically generates these methods for you, triggered by a single annotation on the class . In most cases, the methods generated by AutoValue are essentially identical to those you’d write yourself.

編寫和測(cè)試 equals (和 hashCode)方法很乏味,生成的代碼也很單調(diào)。手動(dòng)編寫和測(cè)試這些方法的一個(gè)很好的替代方法是使用谷歌的開源 AutoValue 框架,它會(huì)自動(dòng)為你生成這些方法,由類上的一個(gè)注釋觸發(fā)。在大多數(shù)情況下,AutoValue 生成的方法與你自己編寫的方法基本相同。

IDEs, too, have facilities to generate equals and hashCode methods, but the resulting source code is more verbose and less readable than code that uses AutoValue, does not track changes in the class automatically, and therefore requires testing. That said, having IDEs generate equals (and hashCode)methods is generally preferable to implementing them manually because IDEs do not make careless mistakes, and humans do.

IDE 也有生成 equals 和 hashCode 方法的功能,但是生成的源代碼比使用 AutoValue 的代碼更冗長(zhǎng),可讀性更差,不會(huì)自動(dòng)跟蹤類中的變化,因此需要進(jìn)行測(cè)試。也就是說,讓 IDE 生成 equals(和 hashCode)方法通常比手動(dòng)實(shí)現(xiàn)更可取,因?yàn)?IDE 不會(huì)出現(xiàn)粗心的錯(cuò)誤,而人會(huì)。

In summary, don’t override the equals method unless you have to: in many cases, the implementation inherited from Object does exactly what you want.If you do override equals, make sure to compare all of the class’s significant fields and to compare them in a manner that preserves all five provisions of the equals contract.

總之,除非必須,否則不要覆蓋 equals 方法:在許多情況下,從 Object 繼承而來的實(shí)現(xiàn)正是你想要的。如果你確實(shí)覆蓋了 equals,那么一定要比較類的所有重要字段,并以保留 equals 約定的所有 5 項(xiàng)規(guī)定的方式進(jìn)行比較。


Back to contents of the chapter(返回章節(jié)目錄)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容