第十條:覆蓋equals方法請(qǐng)遵守通用規(guī)范【對(duì)于所有對(duì)象都通用的方法start】

盡管Object是一個(gè)具體類(lèi)绝页,但是設(shè)計(jì)它主要是為了擴(kuò)展,它所有的非final方法(equals诫咱、hashCode、toString洪灯、clone、finalize)都有明確的通用約定竟痰,因?yàn)樗鼈冊(cè)O(shè)計(jì)成是可覆蓋的(override)签钩,任何一個(gè)類(lèi),它在覆蓋這些方法的時(shí)候坏快,都有責(zé)任遵守這些通用約定铅檩,如果不能做到這點(diǎn),其他依賴(lài)于這些約定的類(lèi)(例如:HashMap和HashSet)就無(wú)法結(jié)合該類(lèi)一起正常運(yùn)作莽鸿。

覆蓋equals方法看起來(lái)似乎很簡(jiǎn)單昧旨,但是有許多覆蓋方式會(huì)導(dǎo)致錯(cuò)誤,并且后果非常嚴(yán)重祥得,最容易避免這類(lèi)問(wèn)題的辦法就是不覆蓋equals方法兔沃,在這種情況下,類(lèi)的每個(gè)實(shí)例都只與它自身相等级及,如果滿(mǎn)足以下任何一個(gè)條件乒疏,這就正是所期望的結(jié)果(就不用覆蓋equals方法):

1.類(lèi)的每個(gè)實(shí)例本質(zhì)上是唯一的:對(duì)于代表活動(dòng)實(shí)體而不是值的類(lèi)確實(shí)如此,例如Thread饮焦,Object提供的equals實(shí)現(xiàn)對(duì)于這些類(lèi)來(lái)說(shuō)正是正確的行為怕吴。
2.類(lèi)沒(méi)有必要提供“邏輯相等“的測(cè)試功能。例如:java.util.regex.Pattern可以覆蓋equals方法县踢,已檢查兩個(gè)Pattern實(shí)例是否代表同一個(gè)正則表達(dá)式转绷,但是設(shè)計(jì)者并不認(rèn)為客戶(hù)端需要或者期望這樣的功能。在這類(lèi)情況下硼啤,從Object繼承的得到的equals實(shí)現(xiàn)就足夠了议经。
3.超類(lèi)已經(jīng)覆蓋了equals,超類(lèi)的行為對(duì)于這個(gè)類(lèi)也是合適的丙曙。例如爸业,大多數(shù)的Set實(shí)現(xiàn)都從AbstractSet繼承equals實(shí)現(xiàn),Map實(shí)現(xiàn)從AbsrtractMap繼承equals實(shí)現(xiàn)亏镰。
4.類(lèi)是私有的扯旷,或者是包級(jí)私有的,可以確保它的equals方法永遠(yuǎn)不會(huì)被調(diào)用索抓。如果非常想要避免風(fēng)險(xiǎn)钧忽,可以覆蓋equals方法毯炮,并加以控制,以確保不會(huì)被意外調(diào)用

@Override
public boolean equals(Object o){
    throw new AssertionError();
}

那么耸黑,什么時(shí)候應(yīng)該覆蓋equals方法呢桃煎?如果類(lèi)具有自己特有的"邏輯相等"概念(不同于對(duì)象等同的概念),而且超類(lèi)還沒(méi)有覆蓋equals方法大刊。這通常屬于值類(lèi)的情形为迈。值類(lèi)僅僅是一個(gè)表示值的類(lèi),例如:Integer或者String缺菌,程序員在利用equals方法來(lái)比較值對(duì)象的引用時(shí)葫辐,希望知道它們?cè)谶壿嬌鲜欠裣嗟龋皇窍窳私馑鼈兪欠裰赶蛲粋€(gè)對(duì)象伴郁,不僅必須覆蓋equals方法耿战,而且這樣做也使得這個(gè)類(lèi)的實(shí)例可以被用作映射表(map)的鍵(key)或者集合(set)的元素,使映射或者集合表現(xiàn)出逾期的行為焊傅。

有一種"值類(lèi)"不需要覆蓋equals方法剂陡,即用實(shí)例受控確保"每個(gè)值最多只存在一個(gè)對(duì)象"的類(lèi),枚舉類(lèi)型就屬于這種類(lèi)狐胎,對(duì)于這樣的類(lèi)而言鸭栖,邏輯相同與對(duì)象相同是一回事。

在覆蓋equals方法的時(shí)候顽爹,必須遵守通用約定纤泵,下面是約定的內(nèi)容,來(lái)自O(shè)bject的規(guī)范:
自反性:對(duì)于任何非null的引用值x镜粤,x.equals(x)捏题,必須返回true。
對(duì)稱(chēng)性:對(duì)于任何非null的引用值x肉渴、y公荧,當(dāng)且僅當(dāng)x.equals(y)返回true時(shí),y.equals(x)也必須返回true同规。
傳遞性:對(duì)于任何非null的引用值x循狰、y、z券勺,如果x.equals(y)返回true绪钥,y.equals(z)也返回true,那么x.equals(z)也必須返回true关炼。
一致性:對(duì)于任何非null引用值x程腹、y,只要equals的比較操作在對(duì)象中所用的信息沒(méi)有修改儒拂,多次調(diào)用x.equals(y)就會(huì)一致的返回true寸潦,或者一致的返回false色鸳。
非空性:對(duì)于任何非null引用值x,x.equals(null)必須返false见转。

這些規(guī)定絕對(duì)不能忽略命雀,如果違反了,就會(huì)發(fā)現(xiàn)程序?qū)?huì)表現(xiàn)不正常斩箫,甚至崩潰吏砂,而且很難找到失敗的根源。一個(gè)類(lèi)的實(shí)例通常會(huì)被頻繁的傳遞給另一個(gè)類(lèi)的實(shí)例乘客,有許多類(lèi)赊抖,包括所有的集合類(lèi)在內(nèi),都依賴(lài)于傳遞給它們的對(duì)象是否遵守了equals約定寨典。

那么什么是等價(jià)關(guān)系呢?不嚴(yán)格的說(shuō)房匆,它是一個(gè)操作符耸成,將一組元素劃分到其元素與另一個(gè)元素等價(jià)的分組中。這些分組被稱(chēng)作等價(jià)類(lèi)浴鸿。從用戶(hù)的角度看井氢,對(duì)于有用的equals方法,每個(gè)等價(jià)類(lèi)中的所有元素都必須是可交換的≡懒矗現(xiàn)在我們按照順序逐一查看以下5個(gè)要求:

自反性:第一個(gè)要求僅僅說(shuō)明對(duì)象必須等于其自身花竞,假如違背了這一條,然后把該類(lèi)的實(shí)例添加到集合中掸哑,該集合的contains方法將果斷的告訴你约急,該集合不包含你剛剛添加的元素。

對(duì)稱(chēng)性:第二個(gè)要求是說(shuō):任何兩個(gè)對(duì)象對(duì)于"它們是否相等"的問(wèn)題都必須保持一致苗分,與第一個(gè)要求不同厌蔽,若無(wú)意中違反了這一條,這種情形倒不難想象摔癣,例如下面的類(lèi)奴饮,它實(shí)現(xiàn)了一個(gè)不區(qū)分大小寫(xiě)的字符串的equals方法。

public class CaseInsensitiveString {

    private String str;

    public CaseInsensitiveString(String str) {
        this.str = str;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof CaseInsensitiveString) {
            CaseInsensitiveString caseString = (CaseInsensitiveString) obj;
            return this.str.equalsIgnoreCase(caseString.str);
        }
        if (obj instanceof String) {
            return this.str.equalsIgnoreCase((String) obj);
        }
        return false;
    }
}

在這個(gè)類(lèi)中择浊,equals方法的意圖非常好戴卜,它企圖與普通的字符串對(duì)象進(jìn)行互操作。假設(shè)我們有一個(gè)不區(qū)分大小寫(xiě)的字符串和一個(gè)普通的字符串琢岩。

CaseInsensitiveString cis = new CaseInsensitiveString("Java");
String s = "java";

不出所料投剥,cis.equals(s)返回true,問(wèn)題在于粘捎,雖然CaseInsensitiveString類(lèi)中的equals方法知道普通的字符串對(duì)象薇缅,但是String類(lèi)中的equals方法并不知道不區(qū)分大小寫(xiě)的字符串危彩,因此,s.equals(cis)返回false泳桦,顯然違反了對(duì)稱(chēng)性汤徽。假設(shè)你把不區(qū)分大小寫(xiě)的字符串對(duì)象放到一個(gè)集合中

List<CaseInsensitiveString> list = new ArrayList<>();
list.contains(s); // false

此時(shí)list.contains(s)會(huì)返回false,一旦違反了equals約定灸撰,當(dāng)其他對(duì)象面對(duì)你的對(duì)象時(shí)谒府,你完全不知道這些對(duì)象的行為會(huì)怎么樣。

為了解決這個(gè)問(wèn)題浮毯,只需把企圖與String互操作的這段代碼從equals中刪除即可完疫。

@Override
public boolean equals(Object obj) {
        return obj instanceof CaseInsensitiveString &&
                ((CaseInsensitiveString) obj).str.equalsIgnoreCase(str);
}

傳遞性:equals約定的第三個(gè)要求是,如果一個(gè)對(duì)象等于第二個(gè)對(duì)象债蓝,而第二個(gè)對(duì)象又等于第三個(gè)對(duì)象壳鹤,則第三個(gè)對(duì)象一定等于第一個(gè)對(duì)象,同樣的饰迹,無(wú)意識(shí)的違反這條規(guī)則的情形也不難想象芳誓。用子類(lèi)舉例:假設(shè)它將一個(gè)新的值組件添加到了超類(lèi)中,換句話說(shuō)啊鸭,子類(lèi)增加的信息會(huì)影響equals的比較結(jié)果锹淌,我們以一個(gè)簡(jiǎn)單的不可變的二位整數(shù)型Point類(lèi)作為開(kāi)始:

public class Point {
    int x;
    int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Point) {
            Point point = (Point) obj;
            return x == point.x && y == point.y;
        }
        return false;
    }
}

假設(shè)想要擴(kuò)展這個(gè)類(lèi),為一個(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方法會(huì)是什么樣呢赠制?如果不完全提供equals方法赂摆,而是直接從Point繼承過(guò)來(lái),在equals作比較的時(shí)候钟些,顏色信息就會(huì)被忽略烟号。雖然這樣做不會(huì)違反equals約定,但是很明顯這是無(wú)法接收的政恍。假設(shè)編寫(xiě)一個(gè)equals方法褥符,只有當(dāng)它的參數(shù)是另一個(gè)有色點(diǎn)并且具有相同的位置和顏色時(shí),才返回true抚垃。

public class ColorPoint extends Point {
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ColorPoint)) {
            return false;
        }
        ColorPoint colorPoint = (ColorPoint) obj;
        return super.equals(obj) && color == colorPoint.color;
    }
}

這個(gè)equals方法的問(wèn)題在于喷楣,在比較普通點(diǎn)和有色點(diǎn),以及相反的情形時(shí)鹤树,可能會(huì)得到不一樣的結(jié)果铣焊。前一種比較會(huì)忽略顏色信息,而后一種比較總是返回false罕伯,因?yàn)閰?shù)類(lèi)型不正確曲伊,為了直觀的說(shuō)明問(wèn)題所在,我們創(chuàng)建一個(gè)普通點(diǎn)和一個(gè)有色點(diǎn)。

Point p1 = new Point(1, 2);
ColorPoint p2 = new ColorPoint(1, 2, Color.BLACK);
System.out.println(p1.equals(p2)); // true
System.out.println(p2.equals(p1)); // false

然而坟募,p1.equals(p2)返回true岛蚤,p2.equals(p1)返回false,也可以嘗試修正這個(gè)問(wèn)題懈糯,讓ColorPoint.equals在進(jìn)行"混合比較"式忽略顏色涤妒。

@Override
public boolean equals(Object obj) {
        if (!(obj instanceof Point)) {
            return false;
        }
        if (!(obj instanceof ColorPoint)) {
            return obj.equals(this);
        }
        ColorPoint colorPoint = (ColorPoint) obj;
        return super.equals(obj) && color == colorPoint.color;
}

這種方式雖然提供了對(duì)稱(chēng)性,但是卻犧牲了傳遞性赚哗。例如:

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

System.out.println(p1.equals(p2)); // true
System.out.println(p2.equals(p3)); // true
System.out.println(p3.equals(p1)); // false

以上例子很明顯這違反了傳遞性她紫,前兩種不需要比較顏色信息,而第三種比較則需要考慮顏色信息屿储。
此外贿讹,這種方法還可能導(dǎo)致無(wú)線遞歸的問(wèn)題,假設(shè)Point有兩個(gè)子類(lèi)够掠,如ColorPoint和SmellPoint民褂,它們各自都帶有這種equals方法,那么ColorPoint.equals(SmellPoint)的調(diào)用就會(huì)出現(xiàn)遞歸疯潭,將會(huì)拋出StackOverflowError異常:舉個(gè)例子:

public class ColorPoint extends Point {
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point)) {
            return false;
        }
        if (!(obj instanceof ColorPoint)) {
            return obj.equals(this);
        }
        ColorPoint colorPoint = (ColorPoint) obj;
        return super.equals(obj) && color == colorPoint.color;
    }
}
public class SmellPoint extends Point {
    private final Color color;

    public SmellPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point)) {
            return false;
        }
        if (!(obj instanceof SmellPoint)) {
            return obj.equals(this);
        }
        return super.equals(obj) && ((SmellPoint) obj).color == this.color;
    }
}
public class Main{
    public static void main(String[] args) {
        ColorPoint p1 = new ColorPoint(1, 2, Color.BLACK);
        SmellPoint p2 = new SmellPoint(1, 2, Color.YELLOW);
        System.out.println(p1.equals(p2)); 
    }
}
console:
Exception in thread "main" java.lang.StackOverflowError
...

那么怎么解決呢助赞?事實(shí)上,這是面向?qū)ο笳Z(yǔ)言中關(guān)于等價(jià)關(guān)系的一個(gè)基本問(wèn)題袁勺,我們*無(wú)法*在擴(kuò)展可實(shí)例化的類(lèi)的同時(shí),即增加新的值組件畜普,同時(shí)又保留equals約定期丰,除非愿意放棄面向?qū)ο蟮某橄笏鶐?lái)的優(yōu)勢(shì)。

你可能聽(tīng)說(shuō)過(guò)吃挑,在equals方法中用getClass測(cè)試代替instanceof測(cè)試钝荡,可以擴(kuò)展可實(shí)例化的類(lèi)和增加新的值組件,同時(shí)保留約定舶衬。

@Override
public boolean equals(Object obj) {
        if (obj == null || obj.getClass() != getClass()) {
            return false;
        }
        Point point = (Point) obj;
        return point.x == x && point.y == y;
}

這段程序只有當(dāng)對(duì)象具有相同的實(shí)現(xiàn)類(lèi)時(shí)埠通,才能是對(duì)象相同,但是Point子類(lèi)的實(shí)例任然是一個(gè)Point逛犹,他仍然需要發(fā)揮作用端辱,但是如果采取這種方式,它就無(wú)法完成任務(wù)虽画!

假設(shè)我們要編寫(xiě)這樣一個(gè)方法舞蔽,以檢驗(yàn)?zāi)硞€(gè)點(diǎn)是否存處在單位園中,下面是可以采用的一種方式:

public class CounterPoint extends Point {

    private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger();

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

    public static int numberCreated() {
        return ATOMIC_INTEGER.get();
    }
}
public class Main {

    private static final Set<Point> unitCircle = new HashSet<>(Arrays.asList(new Point(1, 2)
            , new Point(1, 0),
            new Point(-1, 2)));

    public static boolean onUnitCircle(Point p) {
        return unitCircle.contains(p);
    }
}

里氏替換原則認(rèn)為码撰,一個(gè)類(lèi)型的任何重要屬性也將適用于它的子類(lèi)型渗柿,因此為該類(lèi)型編寫(xiě)的任何方法,在它的子類(lèi)型上也應(yīng)該同樣運(yùn)行的很好脖岛。針對(duì)上述Point的子類(lèi)(如CountPoint)仍是Point朵栖,并且必須發(fā)揮作用的例子颊亮,這就是它的正式語(yǔ)句。但是假設(shè)我們將CountPoint實(shí)例傳給了onUnitCircle方法陨溅,如果Point類(lèi)使用了基于getClass的equals方法终惑,無(wú)論CountPoint實(shí)例的x和y值是什么,onUnitCircle都會(huì)返回false声登。這是因?yàn)橄駉nUnitCircle方法所用的HashSet這樣的集合狠鸳,利用equals方法檢驗(yàn)包含條件,沒(méi)有任何CountPoint實(shí)例與任何Point對(duì)應(yīng)悯嗓。

CounterPoint counterPoint = new CounterPoint(1, 2);
CounterPoint counterPoint1 = new CounterPoint(1, 0);
CounterPoint counterPoint2 = new CounterPoint(-1, 2);
System.out.println(onUnitCircle(counterPoint)); // false
System.out.println(onUnitCircle(counterPoint1)); // false
System.out.println(onUnitCircle(counterPoint2)); // false

但是件舵,如果在Point上使用基于instanceof的equals方法,當(dāng)遇到CountPoint時(shí)脯厨,相同的onUnitCircle方法就會(huì)工作得很好铅祸。

雖然沒(méi)有一種令人滿(mǎn)意的方式可以即擴(kuò)展可實(shí)例得類(lèi),又增加值組件合武,但是還有一種不錯(cuò)得權(quán)宜之計(jì):遵從第18條"復(fù)用優(yōu)先于繼承"得建議临梗。我們不再讓ColorPoint擴(kuò)展Point,而是在ColorPoint中加入一個(gè)私有Point域稼跳,以及一個(gè)公有的視圖方法盟庞,此方法返回一個(gè)與該有色點(diǎn)處在相同位置的普通Point對(duì)象:

public class ColorPoint {
    private final Point point;
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        this.point = new Point(x, y);
        this.color = color;
    }

    public Point asPoint() {
        return point;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ColorPoint)) {
            return false;
        }
        ColorPoint c = (ColorPoint) obj;
        return c.point.equals(point) && c.color.equals(color);
    }
}

注意,你可以在一個(gè)抽象類(lèi)的子類(lèi)中增加值組件且不違反equals約定汤善,根據(jù)第23條的建議而得到的那種類(lèi)層次結(jié)構(gòu)來(lái)說(shuō)什猖,這一點(diǎn)非常重要。例如红淡,你可能有一個(gè)抽象類(lèi)Shape不狮,它沒(méi)有任何值組件,Circle子類(lèi)添加了一個(gè)radius域在旱,Rectangle子類(lèi)添加了length和width域摇零。只要不可能直接創(chuàng)建超類(lèi)的實(shí)例,前面所述的種種問(wèn)題就都不會(huì)發(fā)生桶蝎。就保證了傳遞性驻仅。

一致性:equals約定的第四個(gè)要求是,如果兩個(gè)對(duì)象相等登渣,它們就必須始終相等雾家,除非它們中有一個(gè)對(duì)象(或者兩個(gè)都)被修改。可變對(duì)象在不同的時(shí)候可以與不同的對(duì)象相等绍豁,而不可變對(duì)象則不會(huì)這樣芯咧。當(dāng)在編寫(xiě)一個(gè)類(lèi)的時(shí)候,應(yīng)該仔細(xì)考慮它是否應(yīng)該是不可變的,如果認(rèn)為它應(yīng)該是不可變的敬飒,就必須保證equals方法滿(mǎn)足這樣的限制邪铲,相等的對(duì)象永遠(yuǎn)相等,不相等的對(duì)象永遠(yuǎn)不相等无拗。

無(wú)論類(lèi)是否不可變带到,都不要使equals方法依賴(lài)于不可靠的資源。如果違反了這條
禁令英染,想要滿(mǎn)足一致性的要求就非常困難揽惹。

非空性:最后一個(gè)要求沒(méi)有正式的名字,我們姑且稱(chēng)它為“非空性“四康,意思是指所有的對(duì)象都不能等于null搪搏,盡管很難想象在什么情況下o.equals(null)調(diào)用會(huì)意外的返回true,但是意外的拋出NullPointerException異常的情形卻不難想象闪金。通用約定允許拋出NullPointerException異常疯溺。許多類(lèi)的equals方法都通過(guò)一個(gè)顯式的null測(cè)試來(lái)防止這種情況;

@Override
public boolean equals(Object o){
    if(o == null){
        return false;  
    } 
    ....
}

這項(xiàng)測(cè)試是不必要的哎垦,為了測(cè)試其參數(shù)的等同性囱嫩,equals方法必須先把參數(shù)轉(zhuǎn)換成適當(dāng)?shù)念?lèi)型,以便可以調(diào)用它的訪問(wèn)方法漏设,或者訪問(wèn)它的域墨闲,在進(jìn)行轉(zhuǎn)換之前,equals方法必須使用instanceof操作符郑口,檢查其參數(shù)的類(lèi)型是否正確鸳碧。

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

如果漏掉了這一步類(lèi)型檢查,并且傳遞給equals方法參數(shù)又是錯(cuò)誤的類(lèi)型潘酗,那么equals方法將會(huì)拋出ClassCastException異常,這就違反了equals約定雁仲。但是仔夺,如果
instanceof的第一個(gè)操作數(shù)為null,那么攒砖,不管第二個(gè)是哪種類(lèi)型缸兔,instanceof操作符都指定應(yīng)該返回false,因此吹艇,如果把null傳給equals方法惰蜜,類(lèi)型檢查就會(huì)返回false,所以不需要顯式的null檢查受神。

結(jié)合所有要求抛猖,得出了以下實(shí)現(xiàn)高質(zhì)量equals方法的訣竅:
1.使用==操作符檢查”參數(shù)是否為這個(gè)對(duì)象的引用“。如果是,則返回true财著,這只不過(guò)是一種性能優(yōu)化联四,如果比較操作有可能很昂貴,就值得這么做撑教。
2.使用instanceof操作符檢查”參數(shù)是否為正確的類(lèi)型“朝墩,如果不是,則返回false伟姐。一般來(lái)說(shuō)收苏,所謂“正確的類(lèi)型”是指equals方法所在的那個(gè)類(lèi)。某些情況下愤兵,是指該類(lèi)所實(shí)現(xiàn)的某個(gè)接口鹿霸。如果類(lèi)實(shí)現(xiàn)的接口進(jìn)行了equals約定,允許在實(shí)現(xiàn)了該接口的類(lèi)之間進(jìn)行比較恐似,那就使用接口杜跷,集合接口如Set,List矫夷,Map和Map.Entry具有這樣的特性葛闷。
3.把參數(shù)轉(zhuǎn)換正確的類(lèi)型,因?yàn)檗D(zhuǎn)換之前進(jìn)行過(guò)instanceof測(cè)試双藕,所以確保會(huì)成功淑趾。
4.對(duì)于該類(lèi)的每個(gè)關(guān)鍵域,檢查參數(shù)中的域是否與該對(duì)象中的對(duì)應(yīng)的域相匹配忧陪。如果這些測(cè)試全部成功扣泊,則返回true,否則返回false嘶摊,如果第2步中的類(lèi)型是個(gè)接口延蟹,就必須接口方法訪問(wèn)參數(shù)中的域,如果該類(lèi)型是一個(gè)類(lèi)叶堆,也許就能夠直接訪問(wèn)參數(shù)中的域阱飘,這要取決與它們的可訪問(wèn)性。

對(duì)于既不是float也不是double類(lèi)型的基本類(lèi)型域虱颗,可以直接使用==操作符進(jìn)行比較沥匈,對(duì)于對(duì)象域,可以遞歸調(diào)用equals方法忘渔,對(duì)于float域高帖,可以使用靜態(tài)Float.compare(float f1,float f2)方法;對(duì)于double域畦粮,則使用Double.compare(double d1,double d2)散址。對(duì)float和double域進(jìn)行特殊的處理是有必要的乖阵,因?yàn)榇嬖谥鳩loat.NaN、-0.0f以及類(lèi)似的double常量爪飘;雖然可以用靜態(tài)方法Float.equals和Double.equals對(duì)float和double域進(jìn)行比較义起,但是每次比較都要進(jìn)行自動(dòng)裝箱,這回導(dǎo)致性能下降师崎,對(duì)于數(shù)組域默终,則要把以上這些原則應(yīng)用到每一個(gè)元素,如果數(shù)組域中的每個(gè)元素都很重要犁罩,就可以使用其中一個(gè)Arrays.equals方法齐蔽。

有些對(duì)象引用域包含null可能是合法的,所以床估,為了避免可能導(dǎo)致NullPointerException異常含滴,則使用靜態(tài)方法Object.equals(Object o1,Object o2)來(lái)檢查這類(lèi)域的等同性。

域的比較順序可能會(huì)影響equals方法的性能丐巫,為了獲得最佳的性能谈况,應(yīng)該最先比較最有可能不一致的域,或者開(kāi)銷(xiāo)低的域递胧,最理想的情況是兩個(gè)條件同時(shí)滿(mǎn)足的域碑韵,不應(yīng)該比較那些不屬于對(duì)象邏輯狀態(tài)的域,例如用于同步操作的Lock域缎脾,也不需要比較衍生域祝闻,因?yàn)檫@些域可以由關(guān)鍵域計(jì)算獲得,但是這樣做有可能提高equals方法的性能遗菠,如果衍生域代表了整個(gè)對(duì)象的綜合描述联喘,比較這個(gè)域可以節(jié)省在比較失敗時(shí)去比較實(shí)際數(shù)據(jù)所需要的開(kāi)銷(xiāo),例如:假設(shè)有一個(gè)Polygon類(lèi)辙纬,并緩存了該面積豁遭,如果兩個(gè)多邊形有著不同的面積,就沒(méi)有必要再去比較它們的邊和頂點(diǎn)贺拣。

在編寫(xiě)完equals方法之后蓖谢,應(yīng)該問(wèn)自己三個(gè)問(wèn)題:它是否對(duì)稱(chēng)性?它是否傳遞性纵柿?它是否一致性蜈抓?
并且不要只是自問(wèn)启绰,還要編寫(xiě)單元測(cè)試來(lái)檢驗(yàn)這些特性昂儒,除非用AutoValue(Goole開(kāi)源的框架)生成equals方法,在這種情況下就可以放心的省略測(cè)試委可。如果答案是否定渊跋,就要找出原因腊嗡,再相應(yīng)的修改equals方法的代碼邏輯。equals方法也必須滿(mǎn)足其他兩個(gè)特征(自反性和非空性)拾酝,當(dāng)然燕少,這兩種特性通常會(huì)自動(dòng)滿(mǎn)足。

根據(jù)上面的訣竅構(gòu)建equals方法的具體例子:

public final class Person {
   private final String name;
   private final int age;

   public Person(String name, int age) {
       this.name = name;
       this.age = age;
   }

   @Override
   public boolean equals(Object o) {
       if (this == o) {
           return true;
       }
       if (!(o instanceof Person)) {
           return false;
       }
       Person person = (Person) o;
       return age == person.age && Objects.equals(name, person.name);
   }
}

下面是最后一些告誡:
1.覆蓋equals方法時(shí)總是要覆蓋hashCode方法
2.不要企圖讓equals方法過(guò)于智能蒿囤,如果只是簡(jiǎn)單的測(cè)試域中的值是否相等客们,則不難做的equals約定,如果項(xiàng)過(guò)度的的去尋求各種等價(jià)關(guān)系材诽,則很容易陷入麻煩之中底挫。把任何一種別名形式考慮到等價(jià)范圍內(nèi),往往不是個(gè)好主意脸侥。例如建邓,F(xiàn)ile類(lèi)不應(yīng)該試圖把指向同一文件的符號(hào)鏈接當(dāng)作相等對(duì)象來(lái)看待。所幸File類(lèi)沒(méi)有這樣做睁枕。
3.不要將equals聲明中的Object對(duì)象替換為其他的類(lèi)型官边。程序員編寫(xiě)出下面這樣的equals方法并不鮮見(jiàn),這會(huì)使得程序員花上好幾個(gè)小時(shí)都搞不清楚為什么它不能正常工作外遇。

public boolean equals(MyClass o){
....
}

這個(gè)方法并沒(méi)有覆蓋Object.equals方法注簿,因?yàn)閰?shù)應(yīng)該是Object類(lèi)型,相反臀规,它重載了Object.equals方法滩援,在正常的equals方法的基礎(chǔ)上,再提供一個(gè)強(qiáng)類(lèi)型的equals方法塔嬉,這樣會(huì)導(dǎo)致子類(lèi)中的Override注解產(chǎn)生錯(cuò)誤玩徊。

@Override注解的用法一致,就如本條目中所示谨究,可以防止犯這種錯(cuò)誤恩袱,這和equals方法不能編譯,錯(cuò)誤信息會(huì)告訴你到底哪里出了問(wèn)題胶哲。

總而言之畔塔,不要輕易的覆蓋equals方法,除非特殊需求鸯屿,在許多情況下澈吨,從Object處繼承就夠了。如果要覆蓋equals方法寄摆,一定要比較這個(gè)類(lèi)的所有關(guān)鍵域谅辣,并且檢查覆蓋的equals方法是否遵守equals合約的所有五個(gè)條款。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末婶恼,一起剝皮案震驚了整個(gè)濱河市桑阶,隨后出現(xiàn)的幾起案子柏副,更是在濱河造成了極大的恐慌,老刑警劉巖蚣录,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件割择,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡萎河,警方通過(guò)查閱死者的電腦和手機(jī)荔泳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)虐杯,“玉大人换可,你說(shuō)我怎么就攤上這事∠梅” “怎么了沾鳄?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)确憨。 經(jīng)常有香客問(wèn)我译荞,道長(zhǎng),這世上最難降的妖魔是什么休弃? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任吞歼,我火速辦了婚禮,結(jié)果婚禮上塔猾,老公的妹妹穿的比我還像新娘篙骡。我一直安慰自己,他們只是感情好丈甸,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布糯俗。 她就那樣靜靜地躺著,像睡著了一般睦擂。 火紅的嫁衣襯著肌膚如雪得湘。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,772評(píng)論 1 290
  • 那天顿仇,我揣著相機(jī)與錄音淘正,去河邊找鬼。 笑死臼闻,一個(gè)胖子當(dāng)著我的面吹牛鸿吆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播述呐,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼惩淳,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了市埋?” 一聲冷哼從身側(cè)響起黎泣,我...
    開(kāi)封第一講書(shū)人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缤谎,沒(méi)想到半個(gè)月后抒倚,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坷澡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年托呕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片频敛。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡项郊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出斟赚,到底是詐尸還是另有隱情着降,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布拗军,位于F島的核電站任洞,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏发侵。R本人自食惡果不足惜交掏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望刃鳄。 院中可真熱鬧盅弛,春花似錦、人聲如沸叔锐。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)愉烙。三九已至狰住,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間齿梁,已是汗流浹背催植。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留勺择,地道東北人创南。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像省核,于是被迫代替她去往敵國(guó)和親稿辙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

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