Effective Java 3rd 條目10 當(dāng)覆寫(xiě)equals時(shí)遵從通用協(xié)定

覆寫(xiě)equals方法看上去簡(jiǎn)單览露,但是有許多方式弄錯(cuò)這個(gè)問(wèn)題,而且后果可能?chē)?yán)重搞挣。避免這個(gè)問(wèn)題的最容易的方式是,不要覆寫(xiě)equals方法音羞,這種情況下囱桨,這個(gè)類的每個(gè)實(shí)例只等于它本身。如果下列條件之一符合嗅绰,就是做對(duì)的事情:

  • 類的每個(gè)實(shí)例本質(zhì)上是唯一的舍肠。對(duì)于這些類,比如表示激活實(shí)體(active entity)而不是值的Thread窘面,是正確的翠语。對(duì)于這些類,Object提供的equals實(shí)現(xiàn)恰好有正確的行為财边。

  • 沒(méi)有必要為類提供一個(gè)“邏輯上相等”的檢測(cè)肌括。比如,java.util.regex.Pattern本可以覆寫(xiě)equals檢測(cè)兩個(gè)Pattern實(shí)例是否恰好表示同一個(gè)正則表達(dá)式酣难,但是設(shè)計(jì)者認(rèn)為客戶端沒(méi)必要或者不想有這個(gè)功能谍夭。在這些情況下,從Object繼承的equals實(shí)現(xiàn)是合適的憨募。

  • 一個(gè)超類已經(jīng)有覆寫(xiě)的equals紧索,而且這個(gè)超類行為對(duì)于這個(gè)類來(lái)說(shuō)是合適的。比如菜谣,Set的大多數(shù)實(shí)現(xiàn)從AbstractSet繼承它們的equals實(shí)現(xiàn)珠漂,List實(shí)現(xiàn)是從AbstractList,Map實(shí)現(xiàn)是從AbstractMap尾膊。

  • 類是私有的或者包私有的甘磨,而且你肯定它的equals方法永遠(yuǎn)不會(huì)調(diào)用。如果你是極端風(fēng)險(xiǎn)規(guī)避的眯停,你可以覆寫(xiě)equals方法保證它不會(huì)意外調(diào)用:

@Override public boolean equals(Object o) { 
    throw new AssertionError(); // 方法不會(huì)被調(diào)用 
}

那么什么時(shí)候覆寫(xiě)方法合適呢?邏輯相等(logical equality)不同于單單的對(duì)象標(biāo)識(shí)卿泽,當(dāng)類有這個(gè)概念莺债,而且超類還沒(méi)有覆寫(xiě)equals滋觉,這個(gè)時(shí)候是合適的。這通常是值類(value class)這種情況齐邦。一個(gè)值類僅僅是表示值的類椎侠,比如Integer或者String。程序員用equals方法比較值類的引用措拇,期望找出它們是否在邏輯上相等我纪,而不是它們是否引用同一個(gè)對(duì)象。覆寫(xiě)equals方法不僅僅對(duì)于符合程序員期望是必須的丐吓,而且它也使得實(shí)例作為map的鍵或者set的元素是可預(yù)測(cè)的和合理的行為浅悉。

不需要覆寫(xiě)equals方法的一種值類是這樣的類,它使用實(shí)例控制(條目1)保證每個(gè)值最多存在一個(gè)對(duì)象券犁。Enum類型(條目34)屬于這一類术健。對(duì)于這些類,邏輯相等和對(duì)象標(biāo)識(shí)是相同的粘衬,所以
Object的equals方法荞估,它的作用相當(dāng)于邏輯的equals方法。

當(dāng)你覆寫(xiě)equals方法時(shí)稚新,你必須遵守它的通用協(xié)定勘伺。下面是來(lái)自O(shè)bject文檔上的協(xié)定:

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

  • 反身性:對(duì)于任何非空的引用值x褂删,x.equals(x)必須返回真飞醉。
  • 對(duì)稱性:對(duì)于任何非空引用值x和y,當(dāng)且僅當(dāng)y.equals(x)返回真笤妙,x.equals(y)必須返回真冒掌。
  • 傳遞性:對(duì)于任何非空引用值x、y和z蹲盘,如果x.equals(y)返回真股毫,而且y.equals(z)返回真,那么x.equals(z)返回真召衔。
  • 一致性:對(duì)于任何非空引用值x铃诬、y和z,x.equals(y)的多次調(diào)用必須一致返回真苍凛,或者一致返回假趣席,假如用在equals比較上的信息沒(méi)有改變。
  • 對(duì)于任何非空引用值x醇蝴,x.equals(null)必須返回假宣肚。

除非你熟悉數(shù)學(xué),這看上去可能有點(diǎn)嚇人悠栓,但是不要忽略它霉涨。如果違反了它按价,你可能會(huì)發(fā)現(xiàn),你的程序表現(xiàn)不穩(wěn)定或者崩潰笙瑟,有可能非常難于確定失敗的來(lái)源楼镐。用John Donne的話說(shuō),類不是一個(gè)孤島往枷。一個(gè)類的實(shí)例經(jīng)常傳遞給另外一個(gè)框产。許多類,包括所有的集合類错洁,依賴于傳入的對(duì)象秉宿,這些對(duì)象遵從于equals協(xié)定。

既然你已經(jīng)知道違反equals協(xié)定的危險(xiǎn)墓臭,那么讓我們仔細(xì)檢查這個(gè)協(xié)定蘸鲸。好消息是,盡管表面如此窿锉,它實(shí)際上不是特別復(fù)雜酌摇。一旦你理解了它,遵守它是不難的嗡载。

那么什么是等價(jià)關(guān)系呢窑多?大約地講,它是一個(gè)操作子洼滚,它把一個(gè)集的元素劃分到子集埂息,這個(gè)子集里面的元素被認(rèn)為同另外一個(gè)是相等的。這些子集也稱為相等類遥巴。對(duì)于一個(gè)有用的equals方法千康,每個(gè)相等類里面的所有元素,在用戶的角度看铲掐,必須是可交換的∈捌現(xiàn)在讓我們依次檢查這五項(xiàng)要求:

  • 反身性:第一個(gè)要求說(shuō),一個(gè)對(duì)象必須和自身相等摆霉。很難想象無(wú)意違反了這個(gè)要求豪椿。如果你違反了它,然后把你的類的對(duì)象添加到一個(gè)集中携栋,contains可能會(huì)說(shuō)搭盾,這個(gè)集不含有你剛才添加的實(shí)例。

  • 對(duì)稱性:第二個(gè)要求說(shuō)婉支,任意兩個(gè)對(duì)象必須對(duì)它們是否是相等的要達(dá)成一致鸯隅。不像第一個(gè)要求,不難想象無(wú)意中違反這個(gè)要求向挖。比如滋迈,考慮如下的類霎奢,它實(shí)現(xiàn)了大小寫(xiě)不敏感的字符串。這個(gè)字符串的大小寫(xiě)通過(guò)toString保存著饼灿,但是在equals比較中被忽略:

// 已破壞 - 違反了對(duì)稱性!
public final class CaseInsensitiveString { 
    private final String s;
    
    public CaseInsensitiveString(String s) { 
        this.s = Objects.requireNonNull(s); 
    }

    // 已破壞 - 違反了對(duì)稱性!
    @Override public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString)
            return s.equalsIgnoreCase( 
                ((CaseInsensitiveString) o).s);

        if (o instanceof String) // 單方向可交互!
            return s.equalsIgnoreCase((String) o);
        return false; 
    } 
    ... // 其余省略
}

這個(gè)類中出于好意的equals方法,徒勞地嘗試和普通的字符串互操作帝美。讓我們假設(shè)碍彭,我們有一個(gè)大小寫(xiě)不敏感的字符串,還有一個(gè)普通的字符串:

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

正如所料悼潭,cis.equals(s)返回真庇忌,問(wèn)題在于,當(dāng)CaseInsensitiveString的equals方法知道普通對(duì)象舰褪,但是String的equals方法不知道大小寫(xiě)不敏感的字符串皆疹。所以,s.equals(cis)返回假占拍,一個(gè)明顯的對(duì)稱性的違反略就。假設(shè)你把大小寫(xiě)不敏感的字符串放入一個(gè)集:

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

這時(shí)候,list.contains(s)返回什么呢晃酒?誰(shuí)知道呢表牢?在當(dāng)前的OpenJDK實(shí)現(xiàn)中,碰巧返回假贝次,但那僅僅是一個(gè)人工實(shí)現(xiàn)崔兴。在另外一個(gè)實(shí)現(xiàn)中,它可能同樣容易地返回真蛔翅,或者拋出一個(gè)運(yùn)行時(shí)錯(cuò)誤敲茄。如果你違反了equals協(xié)定,你簡(jiǎn)直不知道山析,其他對(duì)象面對(duì)你的對(duì)象時(shí)是什么行為堰燎。

為了消除這個(gè)問(wèn)題,僅需移除與String的equals方法交互這個(gè)錯(cuò)誤的嘗試盖腿。一旦這樣做了爽待,你可以重構(gòu)這個(gè)方法到一個(gè)簡(jiǎn)單的返回語(yǔ)句:

@Override public boolean equals(Object o) { 
    return o instanceof CaseInsensitiveString && 
        ((CaseInsensitiveString) o).s.equalsIgnoreCase(s); 
}
  • 傳遞性:equals協(xié)定的第三個(gè)要求說(shuō),如果一個(gè)對(duì)象和第二個(gè)相等翩腐,而且第二個(gè)對(duì)象和第三個(gè)相等鸟款,那么第一個(gè)對(duì)象必須和第三個(gè)相等。再次不難想象無(wú)意違反這個(gè)要求茂卦『问玻考慮一個(gè)子類的情形,這個(gè)子類添加一個(gè)新值組件(value component)到它的超類等龙。換句話說(shuō)处渣,子類添加一些信息伶贰,影響了equals比較。讓我們以簡(jiǎn)單不可變的二維整形點(diǎn)類開(kāi)始:
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; 
    }
    ...// 其余省略
}

假設(shè)你想擴(kuò)展這個(gè)類罐栈,給一個(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; 
    }
    ...// 其余省略
}

equals方法看起來(lái)怎么樣呢黍衙?如果你完全忽視它,繼承于Point的實(shí)現(xiàn)和顏色信息在equals比較時(shí)忽略了荠诬。雖然這不違反equals協(xié)定琅翻,但是明顯是不可接受的。假設(shè)你寫(xiě)了一個(gè)equals方法柑贞,只要它的參數(shù)是另外一個(gè)相同位置和顏色的顏色點(diǎn)方椎,就返回真:

// 已破壞 - 違反對(duì)稱性!
@Override public boolean equals(Object o) {
    if (!(o instanceof ColorPoint))
        return false;
    return super.equals(o) && ((ColorPoint) o).color == color; 
}

這個(gè)方法的問(wèn)題是,當(dāng)一個(gè)點(diǎn)和一個(gè)顏色點(diǎn)比較時(shí)钧嘶,或者相反棠众,可能有不同的結(jié)果。前者的比較忽略了顏色有决,而后者的比較總是返回假闸拿,因?yàn)閰?shù)的類型是不正確的。為了使這個(gè)更加具體疮薇,讓我們創(chuàng)建一個(gè)點(diǎn)和一個(gè)顏色點(diǎn):

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

于是胸墙,p.equals(cp)返回真,然而cp.equals(p)返回假按咒。當(dāng)進(jìn)行“混合比較”是迟隅,你可能使用ColorPoint.equals時(shí)忽略顏色,來(lái)試著解決這個(gè)問(wèn)題:

// 已破壞 - 違反傳遞性! 
@Override public boolean equals(Object o) {
    if (!(o instanceof Point))
        return false;

    // 如果o是個(gè)通常的Point励七,進(jìn)行無(wú)關(guān)顏色的比較 
    if (!(o instanceof ColorPoint)) 
        return o.equals(this);

    // o是ColorPoint類型; 進(jìn)行全面的比較 
    return super.equals(o) && ((ColorPoint) o).color == color;
}

這個(gè)方法提供了對(duì)稱性智袭,但是是以傳遞性為代價(jià):

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

現(xiàn)在p1.equals(p2)和p2.equals(p3)返回真,而p1.equals(p3)返回假掠抬,這個(gè)明顯的傳遞性違反吼野。前面兩個(gè)比較是無(wú)關(guān)顏色的,而第三個(gè)考慮了顏色两波。

而且瞳步,這個(gè)方法可能造成無(wú)限遞歸:假設(shè)有兩個(gè)Point子類,比如說(shuō)ColorPoint和SmellPoint腰奋,每個(gè)子類都有這樣的equals方法单起。然后myColorPoint.equals(mySmellPoint)的調(diào)用將拋出StackOverflowError。

那么解決方案是什么呢劣坊?結(jié)果是嘀倒,在面向?qū)ο笳Z(yǔ)言中,這是一個(gè)等價(jià)關(guān)系的基本問(wèn)題。當(dāng)維護(hù)equals協(xié)定時(shí)测蘑,沒(méi)辦法擴(kuò)展一個(gè)不可實(shí)例化的類和添加一個(gè)值組件灌危,除非你愿意放棄面向?qū)ο蟮某橄笮浴?/p>

你可能聽(tīng)說(shuō)過(guò),可以擴(kuò)展一個(gè)不可實(shí)例化的類和添加一個(gè)值組件碳胳,而且以getClass檢測(cè)代替instanceof檢測(cè)的方式維護(hù)equals協(xié)定:

// 已破壞 - 違反了里氏代換原則
@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; 
}

只有在它們有相同實(shí)現(xiàn)類的時(shí)候勇蝙,才有相等對(duì)象的效果。這可能不算太糟固逗,但是結(jié)果是不可接受的:Point子類的實(shí)例仍然一個(gè)Point浅蚪,而且仍然需要起到它的作用,但是烫罩, 如果你使用這個(gè)方法,它不能夠起這樣的作用洽故。假設(shè)我們想要編寫(xiě)一個(gè)方法贝攒,辨別一個(gè)點(diǎn)是否在一個(gè)單元圓上。下面是我們可以這么做的一種方式:

// 初始化單位元时甚,包含單元圓上的所有點(diǎn)
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); 
}

雖然這可能不是最快的方式實(shí)現(xiàn)這個(gè)功能隘弊,但是它工作良好。假設(shè)你用某種簡(jiǎn)單的方式擴(kuò)展Point荒适,而不會(huì)添加一個(gè)值組件梨熙,即,通過(guò)它的構(gòu)造子跟蹤創(chuàng)建了多少個(gè)實(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(); 
    }
}

里氏代換原則說(shuō)刀诬,一個(gè)類型的任何重要屬性也應(yīng)該對(duì)所有它的子類有效咽扇,以便為類型編寫(xiě)的任意方法,同樣應(yīng)該在它的子類上工作良好 [Liskov87]陕壹。這是我們更早聲稱的正式說(shuō)明质欲,這個(gè)聲稱說(shuō),Point子類糠馆,比如CounterPoint嘶伟,仍然是一個(gè)Point,而且必須充當(dāng)它的作用又碌。但是九昧,假設(shè)我們傳遞CounterPoint到onUnitCircle。如果Point類使用基于getClass的equals方法毕匀,那么onUnitCircle方法將返回假铸鹰,不管CounterPoint實(shí)例的x和y坐標(biāo)。正是如此期揪,因?yàn)榇蠖鄶?shù)數(shù)據(jù)集掉奄,包括被CounterPoint使用的HashSet,用equals方法來(lái)檢測(cè)包含,而且CounterPoint實(shí)例不等于任何Point姓建。然而诞仓,如果你使用恰當(dāng)?shù)幕趇nstanceof的Point的equals方法,那么當(dāng)面對(duì)CounterPoint實(shí)例時(shí)速兔,同一個(gè)onUnitCircle方法工作良好墅拭。

雖然擴(kuò)展不可實(shí)例化的類和添加值組件是不令人滿意的方式,但是有個(gè)好的變通方案:根據(jù)條目18(組合優(yōu)于繼承)的建議涣狗,不是讓ColorPoint擴(kuò)展Point谍婉,而是給ColorPoint一個(gè)私有的Point域和一個(gè)公開(kāi)的視圖方法(view method)(條目6),這個(gè)方法返回像這個(gè)顏色點(diǎn)同樣位置的一個(gè)點(diǎn):

// 添加一個(gè)值組件镀钓,而沒(méi)有違反equals協(xié)定 
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); 
    }

    /** 
      * 返回這個(gè)顏色點(diǎn)的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); 
    }
    ...// 其余省略
}

在Java平臺(tái)庫(kù)里有些類確實(shí)擴(kuò)展一個(gè)不可實(shí)例化的類穗熬,而且添加了一個(gè)值類型。比如丁溅,java.sql.Timestamp擴(kuò)展了java.util.Date唤蔗,而且添加了nanoseconds域。Timestamp的equals實(shí)現(xiàn)確實(shí)違反了對(duì)稱性而且可能造成不穩(wěn)定的行為窟赏,如果Timestamp和Date對(duì)象在同一個(gè)數(shù)據(jù)集中或者另外的混用妓柜。只要你把它們分開(kāi),你就不會(huì)遇到麻煩涯穷,但是沒(méi)有任何事情可以阻止你混用它們棍掐,而且造成的錯(cuò)誤很難調(diào)試。Timestamp類的行為是一個(gè)錯(cuò)誤拷况,不應(yīng)該模仿作煌。

注意,你可以添加值組件到抽象類的一個(gè)子類中蝠嘉,而沒(méi)有違反equals協(xié)定最疆。這是重要的,對(duì)于根據(jù)在條目23(類層級(jí)優(yōu)于標(biāo)記類)的建議蚤告,你獲取到這種類層級(jí)努酸。比如你可以有個(gè)沒(méi)有值組件的抽象Shape類,Circle子類添加了一個(gè)radius域杜恰,而且Rectangle子類添加了長(zhǎng)和寬的域获诈。只要不可能直接創(chuàng)建一個(gè)超類實(shí)例,前面展示的這種問(wèn)題不會(huì)發(fā)生心褐。

  • 一致性:equals協(xié)定的第四個(gè)要求是舔涎,如果兩個(gè)對(duì)象是相等的,那么它們所有時(shí)間都必須相等逗爹,除非其中之一或者兩者被修改了亡嫌。換句話說(shuō)嚎于,可變對(duì)象在不同的時(shí)間可能等于不同的對(duì)象,然而不可變對(duì)象是不能的挟冠。當(dāng)你編寫(xiě)一個(gè)類于购,認(rèn)真思考它是否應(yīng)該是不可變的(條目17)。如果你斷定它應(yīng)該知染,確保你的equals方法限制了相等對(duì)象永遠(yuǎn)相等肋僧,不相等對(duì)象永遠(yuǎn)不相等。

不管一個(gè)對(duì)象是否可變控淡,不要編寫(xiě)一個(gè)依賴不可靠資源的equals方法嫌吠。如果你違反了這個(gè)禁令,滿足一致性是極其困難的掺炭。比如辫诅,java.net.URL的equals方法依賴于與主機(jī)IP地址有關(guān)系的URL的比較。翻譯主機(jī)名到IP地址涧狮,可能需要網(wǎng)絡(luò)連接泥栖,而且不能夠保證隨著時(shí)間變化而產(chǎn)生相同的結(jié)果。這個(gè)可能造成URL的equals方法違反了equals協(xié)定勋篓,而且在實(shí)際中引發(fā)問(wèn)題。URL的equals方法的行為是一個(gè)大的錯(cuò)誤魏割,不應(yīng)該模仿譬嚣。不幸的是,由于兼容性要求钞它,這不能夠改變拜银。為了避免這樣的問(wèn)題,equals方法應(yīng)該僅僅對(duì)駐留內(nèi)存的對(duì)象遭垛,執(zhí)行確定性的計(jì)算尼桶。

  • 非空性: 最后一個(gè)要求缺少官方名字,所以我冒昧地叫它“非空性”锯仪。它是說(shuō)泵督,所有的對(duì)象應(yīng)該不與空相等。雖然難于想象庶喜,對(duì)o.equals(null)的調(diào)用的響應(yīng)小腊,意外返回真,但是不難想象意外拋出一個(gè)NullPointerException久窟。這個(gè)通用協(xié)定禁止這個(gè)秩冈。許多類有equals方法,用一個(gè)顯式的null檢測(cè)防止它:
@Override public boolean equals(Object o) { 
    if (o == null) 
        return false; 
    ...
}

這個(gè)檢測(cè)是必要的斥扛。為了相等檢測(cè)它的參數(shù)入问,equals方法必須首先強(qiáng)轉(zhuǎn)它的參數(shù)到一個(gè)正確的類型,使得它的訪問(wèn)器可以被調(diào)用或者它的域可以被訪問(wèn)。在進(jìn)行強(qiáng)轉(zhuǎn)之前芬失,方法必須使用instanceof操作子楣黍,檢查它的參數(shù)是正確的類型:

@Override public boolean equals(Object o) { 
    if (!(o instanceof MyType)) 
        return false; 
    MyType mt = (MyType) o;
    ...
}

如果這個(gè)類型檢查缺少,而且equals方法傳入了一個(gè)錯(cuò)誤的參數(shù)麸折,equals方法應(yīng)該拋出ClassCastException锡凝,這違反了equals協(xié)定。但是instanceof操作子指定返回假垢啼,如果他的第一個(gè)操作數(shù)是空窜锯,不管第二個(gè)操作數(shù)的類型是什么[JLS, 15.20.2]。所以芭析,如果類型檢查返回假锚扎,所以你不需要顯式的空檢查。

綜合前述馁启,下面是一個(gè)高質(zhì)量的equals方法的秘訣:

  1. 使用==操作子檢測(cè)參數(shù)是否是這個(gè)對(duì)象的一個(gè)引用驾孔。如果如此,返回真惯疙。這只是一個(gè)性能優(yōu)化翠勉,但是如果對(duì)比可能代價(jià)大時(shí),這件事情是值得做的霉颠。

  2. 使用instanceof操作子檢查參數(shù)是否有相同的類型对碌。如果沒(méi)有,返回假蒿偎。通常朽们,正確的類型是一個(gè)類,這個(gè)方法在這個(gè)類里面出現(xiàn)诉位。有時(shí)骑脱,類型是這個(gè)類實(shí)現(xiàn)的某個(gè)接口。如果類實(shí)現(xiàn)了一個(gè)接口苍糠,這個(gè)接口改善了equals協(xié)定叁丧,允許實(shí)現(xiàn)該接口的類之間比較,那么使用接口椿息。Collection接口歹袁,比如Set、List寝优、Map和Map.Entry轿腺,有這個(gè)屬性魄健。

  3. 強(qiáng)轉(zhuǎn)參數(shù)到正確的類型拭荤。因?yàn)檫@個(gè)強(qiáng)轉(zhuǎn)之前有instanceof檢測(cè),保證了成功迁杨。

  4. 在類里面的每個(gè)“重要”域,檢測(cè)參數(shù)的域是否匹配這個(gè)對(duì)象的相應(yīng)的域凄硼。如果所有這些檢測(cè)通過(guò)铅协,返回真;否則摊沉,返回假狐史。如果步驟2的類型是一個(gè)接口,你必須通過(guò)接口的方法獲取參數(shù)的域说墨;如果類型是類的話骏全,你或許可能直接獲取域,決定于它們的可存取性尼斧。

對(duì)于原始的域姜贡,而且它的類型不是float或者double,使用==操作子比較棺棵;對(duì)于對(duì)象引用的域楼咳,遞歸地調(diào)用equals方法;對(duì)于float域烛恤,使用靜態(tài)的Float.compare(float, float)母怜;對(duì)于double域,使用Double.compare(double, double)缚柏。float和double域的特別處理是必要的糙申,由于Float.NaN, -0.0f和double類似這樣的值。詳細(xì)情況參考JLS 15.21.1或者Float.equals的文檔船惨。雖然你可以用靜態(tài)方法Float.equals和Double.equals對(duì)比f(wàn)loat和double域,但是每次比較缕陕,會(huì)需要自動(dòng)裝箱粱锐,這使得有糟糕的性能。對(duì)于隊(duì)列的域扛邑,使用這些規(guī)則到每個(gè)元素怜浅。如果隊(duì)列域中的每個(gè)元素都重要,使用Arrays.equals方法之一蔬崩。

一些對(duì)象引用域恶座,可能合理地包括空。為了避免NullPointerException的可能性沥阳,使用靜態(tài)方法Objects.equals(Object, Object)跨琳,為相等性檢測(cè)這些域。

對(duì)于一些類桐罕,比如上面的CaseInsensitiveString脉让,域比較比簡(jiǎn)單的相等檢測(cè)復(fù)雜的多桂敛。如果是這個(gè)情況,你可能想要存儲(chǔ)域的標(biāo)準(zhǔn)形式(canonical form)溅潜,這樣术唬,equals方法可以用標(biāo)準(zhǔn)形式進(jìn)行便宜精確的比較,而不是一個(gè)代價(jià)更高的非標(biāo)準(zhǔn)的比較滚澜。這個(gè)技巧對(duì)不可變類是最合適的(條目17)粗仓;如果這個(gè)對(duì)象可以改變,你必須保持標(biāo)準(zhǔn)形式最新设捐。

equals方法的性能可能會(huì)被它里面域的比較順序所影響借浊。為了最好的性能,你應(yīng)該首先比較這樣的域:更可能不同的挡育、對(duì)比代價(jià)更低的巴碗,或者理想地,兩者即寒。你不可以比較不是對(duì)象邏輯狀態(tài)的部分的域橡淆,比如用在synchronize操作上的鎖域。你不需要比較派生的域母赵,這個(gè)可以從“重要的域”計(jì)算出來(lái)逸爵,但是這樣做可以改進(jìn)equals方法的性能。如果派生的域相當(dāng)于整個(gè)對(duì)象概要描述凹嘲,比較失敗時(shí)师倔,比較這個(gè)域?qū)槟闶∠卤容^實(shí)際數(shù)據(jù)的代價(jià)。舉個(gè)例子周蹭,假設(shè)你有一個(gè)Polygon類趋艘,而且你緩存了這個(gè)地方。如果兩個(gè)多邊形有不同的地方凶朗,你不需要麻煩比較它們的邊和頂點(diǎn)瓷胧。

當(dāng)你完成編寫(xiě)equals方法,問(wèn)你自己這個(gè)三個(gè)問(wèn)題:它是對(duì)稱的嗎棚愤?它是可傳遞的嗎搓萧?它是一致的嗎?不要僅僅問(wèn)自己宛畦;編寫(xiě)單元測(cè)試來(lái)檢測(cè)瘸洛,除非你使用AutoValue生成你的equals方法,這種情況下次和,你可以安全地省略檢測(cè)反肋。如果屬性不能夠,那么弄清楚為什么踏施,然后相應(yīng)修改equals方法囚玫。當(dāng)然喧锦,你的equals方法也必須滿足另外兩個(gè)屬性(反身性和非空性),但是這兩個(gè)屬性通常由它們自己處理抓督。

根據(jù)前面的秘籍構(gòu)建的equals方法燃少,用這個(gè)簡(jiǎn)化的PhoneNumber類顯示:

// 典型equals方法的類
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;
    } 
    ... // 其余省略
}

下面是一些最后的警告:

  • 當(dāng)你覆寫(xiě)equals時(shí),永遠(yuǎn)覆寫(xiě)hashCode(條目11)

  • 不要自作聰明铃在。如果你簡(jiǎn)單地為相等性檢測(cè)域阵具,遵守equals協(xié)定是不難的。如果你過(guò)度積極尋找等價(jià)定铜,容易陷入麻煩阳液。考慮任何混疊揣炕,通常只是一個(gè)壞主意帘皿。比如,F(xiàn)ile類不應(yīng)該嘗試使同個(gè)文件的符號(hào)鏈接相等畸陡。幸好鹰溜,它沒(méi)有。

  • 在equals聲明中丁恭,不要為Object替代另外的類別曹动。對(duì)于一個(gè)程序員,這不是不常見(jiàn)的牲览,編寫(xiě)一個(gè)equals方法墓陈,看上去這樣,然后花幾個(gè)小時(shí)苦苦考慮為什么沒(méi)有正常工作第献。

// 已破壞 - 參數(shù)類型必須是Object! 
public boolean equals(MyClass o) { 
    ...
}

問(wèn)題是贡必,這個(gè)方法沒(méi)有覆寫(xiě)Object.equals,它的參數(shù)是Object類別庸毫,而是相反赊级,重載了它(條目52)。這是不可接受的岔绸,提供這樣一個(gè)“強(qiáng)類型”的equals方法,甚至在正常的方法之外橡伞,因?yàn)樗斐勺宇愔械腛verride注解生成一個(gè)錯(cuò)誤的正值盒揉,而且提供了一個(gè)安全的錯(cuò)覺(jué)。

Override注解的一致使用兑徘,就像這個(gè)條目中一直闡述的刚盈,將阻止你犯這個(gè)錯(cuò)誤(條目40)。這個(gè)equals方法編譯不通過(guò)挂脑,而且這個(gè)錯(cuò)誤信息將告訴什么問(wèn)題藕漱。

// 仍然已破壞欲侮,但是編譯不通過(guò) 
@Override public boolean equals(MyClass o) { 
    ...
}

編寫(xiě)和測(cè)試equals(和hashCode)方法是乏味的,而且產(chǎn)生的結(jié)果是無(wú)奇的肋联。手動(dòng)編寫(xiě)和測(cè)試這些方法威蕉,有個(gè)絕佳的替代是使用谷歌的AutoValue開(kāi)源框架,它會(huì)自動(dòng)為你生成這些方法橄仍,由類的注釋觸發(fā)韧涨。在大多數(shù)情況,AutoValue生成的方法和你自己編寫(xiě)的方法本質(zhì)上是相同的侮繁。

同樣虑粥,IDE有工具生成equals和hashcode方法,但是產(chǎn)生的源代碼宪哩,相對(duì)于使用AutoValue娩贷,更加冗長(zhǎng)和更不可讀,不要在類中自動(dòng)地跟蹤改變锁孟,所以需要測(cè)試彬祖。這就是說(shuō),用IDE生成equals(和hashCode)方法罗岖,相對(duì)于手動(dòng)實(shí)現(xiàn)他們涧至,通常是更加可取的,因?yàn)镮DE不會(huì)犯粗心大意的錯(cuò)誤桑包,但是人類會(huì)南蓬。

總之,不要覆寫(xiě)equals方法哑了,除非你不得不:在許多情形下赘方,從Object繼承的實(shí)現(xiàn)恰好是如你所想的。如果你確實(shí)要覆寫(xiě)equals弱左,確保比較所有類的重要域窄陡,而且以一種符合equals協(xié)定的五種條款的方式比較它們。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拆火,一起剝皮案震驚了整個(gè)濱河市跳夭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌们镜,老刑警劉巖币叹,帶你破解...
    沈念sama閱讀 211,348評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異模狭,居然都是意外死亡颈抚,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)嚼鹉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)贩汉,“玉大人驱富,你說(shuō)我怎么就攤上這事∑ノ瑁” “怎么了褐鸥?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,936評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)策菜。 經(jīng)常有香客問(wèn)我晶疼,道長(zhǎng),這世上最難降的妖魔是什么又憨? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,427評(píng)論 1 283
  • 正文 為了忘掉前任翠霍,我火速辦了婚禮,結(jié)果婚禮上蠢莺,老公的妹妹穿的比我還像新娘寒匙。我一直安慰自己,他們只是感情好躏将,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布锄弱。 她就那樣靜靜地躺著,像睡著了一般祸憋。 火紅的嫁衣襯著肌膚如雪会宪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,785評(píng)論 1 290
  • 那天蚯窥,我揣著相機(jī)與錄音掸鹅,去河邊找鬼。 笑死拦赠,一個(gè)胖子當(dāng)著我的面吹牛巍沙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播荷鼠,決...
    沈念sama閱讀 38,931評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼句携,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了允乐?” 一聲冷哼從身側(cè)響起矮嫉,我...
    開(kāi)封第一講書(shū)人閱讀 37,696評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎牍疏,沒(méi)想到半個(gè)月后蠢笋,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,141評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡麸澜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了奏黑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炊邦。...
    茶點(diǎn)故事閱讀 38,625評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡编矾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出馁害,到底是詐尸還是另有隱情窄俏,我是刑警寧澤,帶...
    沈念sama閱讀 34,291評(píng)論 4 329
  • 正文 年R本政府宣布碘菜,位于F島的核電站凹蜈,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏忍啸。R本人自食惡果不足惜仰坦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望计雌。 院中可真熱鬧悄晃,春花似錦、人聲如沸凿滤。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)翁脆。三九已至眷蚓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間反番,已是汗流浹背沙热。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恬口,地道東北人校读。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像祖能,于是被迫代替她去往敵國(guó)和親歉秫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容