Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必不少人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到如今已經將近8年的時間,但隨着Java 6,7,8,甚至9的發佈,Java語言發生了深入的變化。
在這裏第一時間翻譯成中文版。供你們學習分享之用。java
雖然Object
是一個具體的類,但它主要是爲繼承而設計的。它的全部非 final方法(equals、hashCode、toString、clone和finalize)都有清晰的通用約定( general contracts),由於它們被設計爲被子類重寫。任何類都有義務重寫這些方法,以聽從他們的通用約定;若是不這樣作,將會阻止其餘依賴於約定的類(例如HashMap和HashSet)與此類一塊兒正常工做。程序員
本章論述什麼時候以及如何重寫Object
類的非final的方法。這一章省略了finalize方法,由於它在條目 8中進行了討論。Comparable.compareTo
方法雖然不是Object
中的方法,由於具備不少的類似性,因此也在這裏討論。正則表達式
重寫equals方法看起來很簡單,可是有不少方式會致使重寫出錯,其結果多是可怕的。避免此問題的最簡單方法不是覆蓋equals方法,在這種狀況下,類的每一個實例只與自身相等。若是知足如下任一下條件,則說明是正確的作法:sql
java.util.regex.Pattern
能夠重寫equals 方法檢查兩個是否表明徹底相同的正則表達式Pattern實例,可是設計者並不認爲客戶須要或但願使用此功能。在這種狀況下,從Object繼承的equals實現是最合適的。@Override public boolean equals(Object o) { throw new AssertionError(); // Method is never called }
那何時須要重寫 equals 方法呢?若是一個類包含一個邏輯相等( logical equality)的概念,此概念有別於對象標識(object identity),並且父類尚未重寫過equals 方法。這一般用在值類( value classes)的狀況。值類只是一個表示值的類,例如Integer或String類。程序員使用equals方法比較值對象的引用,指望發現它們在邏輯上是否相等,而不是引用相同的對象。重寫 equals方法不只能夠知足程序員的指望,它還支持重寫過equals 的實例做爲Map 的鍵(key),或者 Set 裏的元素,以知足預期和指望的行爲。數組
一種不須要equals方法重寫的值類是使用實例控制(instance control)(條目 1)的類,以確保每一個值至多存在一個對象。 枚舉類型(條目 34)屬於這個類別。 對於這些類,邏輯相等與對象標識是同樣的,因此Object的equals方法做用邏輯equals方法。緩存
當你重寫equals方法時,必須遵照它的通用約定。Object的規範以下:
equals方法實現了一個等價關係(equivalence relation)。它有如下這些屬性:
•自反性:對於任何非空引用x,x.equals(x)
必須返回true。
•對稱性:對於任何非空引用x和y,若是且僅當y.equals(x)
返回true時x.equals(y)
必須返回true。
•傳遞性:對於任何非空引用x、y、z,若是x.equals(y)
返回true,y.equals(z)
返回true,則x.equals(z)
必須返回true。
•一致性:對於任何非空引用x和y,若是在equals比較中使用的信息沒有修改,則x.equals(y)
的屢次調用必須始終返回true或始終返回false。
•對於任何非空引用x,x.equals(null)
必須返回false。安全
除非你喜歡數學,不然這看起來有點嚇人,但不要忽略它!若是一旦違反了它,極可能會發現你的程序運行異常或崩潰,而且很難肯定失敗的根源。套用約翰·多恩(John Donne)的說法,沒有哪一個類是孤立存在的。一個類的實例經常被傳遞給另外一個類的實例。許多類,包括全部的集合類,都依賴於傳遞給它們遵照equals約定的對象。性能優化
既然已經意識到違反equals約定的危險,讓咱們詳細地討論一下這個約定。好消息是,表面上看,這並非很複雜。一旦你理解了,就不難遵照這一約定。網絡
那麼什麼是等價關係? 籠統地說,它是一個運算符,它將一組元素劃分爲彼此元素相等的子集。 這些子集被稱爲等價類(equivalence classes)。 爲了使equals方法有用,每一個等價類中的全部元素必須從用戶的角度來講是能夠互換(interchangeable)的。 如今讓咱們依次看下這個五個要求:框架
自反性(Reflexivity)——第一個要求只是說一個對象必須與自身相等。 很難想象無心中違反了這個規定。 若是你違反了它,而後把類的實例添加到一個集合中,那麼contains
方法可能會說集合中沒有包含剛添加的實例。
對稱性(Symmetry)——第二個要求是,任何兩個對象必須在是否相等的問題上達成一致。與第一個要求不一樣的是,咱們不難想象在無心中違反了這一要求。例如,考慮下面的類,它實現了不區分大小寫的字符串。字符串被toString保存,但在equals比較中被忽略:
import java.util.Objects; 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 }
上面類中的 equals 試圖與正常的字符串進行操做,假設咱們有一個不區分大小寫的字符串和一個正常的字符串:
CaseInsensitiveString cis = new CaseInsensitiveString("Polish"); String s = "polish」; System.out.println(cis.equals(s)); // true System.out.println(s.equals(cis)); // false
正如所料,cis.equals(s)
返回true。 問題是,儘管CaseInsensitiveString
類中的equals方法知道正常字符串,但String類中的equals方法卻忽略了不區分大小寫的字符串。 所以,s.equals(cis
)返回false,明顯違反對稱性。 假設把一個不區分大小寫的字符串放入一個集合中:
List<CaseInsensitiveString> list = new ArrayList<>(); list.add(cis);
list.contains(s)
返回了什麼?誰知道呢?在當前的OpenJDK實現中,它會返回false,但這只是一個實現構件。在另外一個實現中,它能夠很容易地返回true或拋出運行時異常。一旦違反了equals約定,就不知道其餘對象在面對你的對象時會如何表現了。
要消除這個問題,只需刪除equals方法中與String類相互操做的惡意嘗試。這樣作以後,能夠將該方法重構爲單個返回語句:
@Override public boolean equals(Object o) { return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s); }
傳遞性(Transitivity)——equals 約定的第三個要求是,若是第一個對象等於第二個對象,第二個對象等於第三個對象,那麼第一個對象必須等於第三個對象。一樣,也不難想象,無心中違反了這一要求。考慮子類的狀況, 將新值組件( value component)添加到其父類中。換句話說,子類添加了一個信息,它影響了equals方法比較。讓咱們從一個簡單不可變的二維整數類型Point類開始:
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 }
假設想繼承這個類,將表示顏色的Color類添加到Point類中:
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 }
equals方法應該是什麼樣子?若是徹底忽略,則實現是從Point類上繼承的,顏色信息在equals方法比較中被忽略。雖然這並不違反equals約定,但這顯然是不可接受的。假設你寫了一個equals方法,它只在它的參數是另外一個具備相同位置和顏色的ColorPoint實例時返回true:
// Broken - violates symmetry! @Override public boolean equals(Object o) { if (!(o instanceof ColorPoint)) return false; return super.equals(o) && ((ColorPoint) o).color == color; }
當你比較Point對象和ColorPoint對象時,能夠會獲得不一樣的結果,反之亦然。前者的比較忽略了顏色屬性,然後者的比較會一直返回 false,由於參數的類型是錯誤的。爲了讓問題更加具體,咱們建立一個Point對象和ColorPoint對象:
Point p = new Point(1, 2); ColorPoint cp = new ColorPoint(1, 2, Color.RED);
p.equals(cp)返回 true,可是 cp.equals(p)返回 false。你可能想使用ColorPoint.equals 經過混合比較的方式來解決這個問題。
@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; }
這種方法確實提供了對稱性,可是喪失了傳遞性:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED); Point p2 = new Point(1, 2); ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
如今,p1.equals(p2)
和p2.equals(p3)
返回了 true,可是p1.equals(p3)
卻返回了 false,很明顯違背了傳遞性的要求。前兩個比較都是不考慮顏色信息的,而第三個比較時卻包含顏色信息。
此外,這種方法可能致使無限遞歸:假設有兩個Point的子類,好比ColorPoint和SmellPoint,每一個都有這種equals方法。 而後調用myColorPoint.equals(mySmellPoint)
將拋出一個StackOverflowError異常。
那麼解決方案是什麼? 事實證實,這是面嚮對象語言中關於等價關係的一個基本問題。 除非您願意放棄面向對象抽象的好處,不然沒法繼承可實例化的類,並在保留 equals 約定的同時添加一個值組件。
你可能據說過,能夠繼承一個可實例化的類並添加一個值組件,同時經過在equals方法中使用一個getClass測試代替instanceof測試來保留equals約定:
@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; }
只有當對象具備相同的實現類時,纔會產生相同的效果。這看起來可能不是那麼糟糕,可是結果是不可接受的:一個Point類子類的實例仍然是一個Point的實例,它仍然須要做爲一個Point來運行,可是若是你採用這個方法,就會失敗!假設咱們要寫一個方法來判斷一個Point 對象是否在unitCircle集合中。咱們能夠這樣作:
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); }
雖然這可能不是實現功能的最快方法,但它能夠正常工做。假設以一種不添加值組件的簡單方式繼承 Point 類,好比讓它的構造方法跟蹤記錄建立了多少實例:
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(); } }
里氏替代原則( Liskov substitution principle)指出,任何類型的重要屬性都應該適用於全部的子類型,所以任何爲這種類型編寫的方法都應該在其子類上一樣適用[Liskov87]。 這是咱們以前聲明的一個正式陳述,即Point的子類(如CounterPoint)仍然是一個Point,必須做爲一個Point類來看待。 可是,假設咱們將一個CounterPoint對象傳遞給onUnitCircle方法。 若是Point類使用基於getClass的equals方法,則不管CounterPoint實例的x和y座標如何,onUnitCircle方法都將返回false。 這是由於大多數集合(包括onUnitCircle方法使用的HashSet)都使用equals方法來測試是否包含元素,而且CounterPoint實例並不等於任何Point實例。 可是,若是在Point上使用了適當的基於instanceof
的equals方法,則在使用CounterPoint實例呈現時,一樣的onUnitCircle方法能夠正常工做。
雖然沒有使人滿意的方法來繼承一個可實例化的類並添加一個值組件,可是有一個很好的變通方法:按照條目18的建議,「優先使用組合而不是繼承」。取代繼承Point類的ColorPoint類,能夠在ColorPoint類中定義一個私有Point屬性,和一個公共的試圖(view)(條目6)方法,用來返回具備相同位置的ColorPoint對象。
// 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 }
Java平臺類庫中有一些類能夠繼承可實例化的類並添加一個值組件。 例如,java.sql.Timestamp
繼承了java.util.Date
並添加了一個nanoseconds字段。 Timestamp的等價equals確實違反了對稱性,而且若是Timestamp和Date對象在同一個集合中使用,或者以其餘方式混合使用,則可能致使不穩定的行爲。 Timestamp類有一個免責聲明,告誡程序員不要混用Timestamp和Date。 雖然只要將它們分開使用就不會遇到麻煩,但沒有什麼能夠阻止你將它們混合在一塊兒,而且由此產生的錯誤可能很難調試。 Timestamp類的這種行爲是一個錯誤,不該該被仿效。
你能夠將值組件添加到抽象類的子類中,而不會違反equals約定。這對於經過遵循第23個條目中「優先考慮類層級(class hierarchies)來代替標記類(tagged classes)」中的建議而得到的類層級,是很是重要的。例如,能夠有一個沒有值組件的抽象類Shape,子類Circle有一個radius屬性,另外一個子類Rectangle包含length和width屬性 。 只要不直接建立父類實例,就不會出現前面所示的問題。
一致性——equals 約定的第四個要求是,若是兩個對象是相等的,除非一個(或兩個)對象被修改了, 那麼它們必須始終保持相等。 換句話說,可變對象能夠在不一樣時期能夠與不一樣的對象相等,而不可變對象則不會。 當你寫一個類時,要認真思考它是否應該設計爲不可變的(條目 17)。 若是你認爲應該這樣作,那麼確保你的equals方法強制執行這樣的限制:相等的對象永遠相等,不相等的對象永遠都不會相等。
無論一個類是否是不可變的,都不要寫一個依賴於不可靠資源的equals方法。 若是違反這一禁令,知足一致性要求是很是困難的。 例如,java.net.URL類中的equals方法依賴於與URL關聯的主機的IP地址的比較。 將主機名轉換爲IP地址可能須要訪問網絡,而且不能保證隨着時間的推移會產生相同的結果。 這可能會致使URL類的equals方法違反equals 約定,並在實踐中形成問題。 URL類的equals方法的行爲是一個很大的錯誤,不該該被效仿。 不幸的是,因爲兼容性的要求,它不能改變。 爲了不這種問題,equals方法應該只對內存駐留對象執行肯定性計算。
非空性(Non-nullity)——最後equals 約定的要求沒有官方的名稱,因此我冒昧地稱之爲「非空性」。意思是說說全部的對象都必須不等於 null。雖然很難想象在調用 o.equals(null)
的響應中意外地返回true,但不難想象不當心拋出NullPointerException
異常的狀況。通用的約定禁止拋出這樣的異常。許多類中的 equals方法都會明確阻止對象爲null的狀況:
@Override public boolean equals(Object o) { if (o == null) return false; ... }
這個判斷是沒必要要的。 爲了測試它的參數是否相等,equals方法必須首先將其參數轉換爲合適類型,以便調用訪問器或容許訪問的屬性。 在執行類型轉換以前,該方法必須使用instanceof運算符來檢查其參數是不是正確的類型:
@Override public boolean equals(Object o) { if (!(o instanceof MyType)) return false; MyType mt = (MyType) o; ... }
若是此類型檢查漏掉,而且equals方法傳遞了錯誤類型的參數,那麼equals方法將拋出ClassCastException
異常,這違反了equals約定。 可是,若是第一個操做數爲 null,則指定instanceof運算符返回false,而無論第二個操做數中出現何種類型[JLS,15.20.2]。 所以,若是傳入null,類型檢查將返回false,所以不須要 明確的 null檢查。
綜合起來,如下是編寫高質量equals方法的配方(recipe):
instanceof
運算符來檢查參數是否具備正確的類型。 若是不是,則返回false。 一般,正確的類型是equals方法所在的那個類。 有時候,改類實現了一些接口。 若是類實現了一個接口,該接口能夠改進 equals約定以容許實現接口的類進行比較,那麼使用接口。 集合接口(如Set,List,Map和Map.Entry)具備此特性。對於類型爲非float或double的基本類型,使用= =運算符進行比較;對於對象引用屬性,遞歸地調用equals方法;對於float 基本類型的屬性,使用靜態Float.compare(float, float)
方法;對於double 基本類型的屬性,使用Double.compare(double, double)
方法。因爲存在Float.NaN
,-0.0f
和相似的double類型的值,因此須要對float和double屬性進行特殊的處理;有關詳細信息,請參閱JLS 15.21.1或Float.equals方法的詳細文檔。 雖然你可使用靜態方法Float.equals和Double.equals方法對float和double基本類型的屬性進行比較,這會致使每次比較時發生自動裝箱,引起很是差的性能。 對於數組屬性,將這些準則應用於每一個元素。 若是數組屬性中的每一個元素都很重要,請使用其中一個重載的Arrays.equals方法。
某些對象引用的屬性可能合法地包含null。 爲避免出現NullPointerException異常,請使用靜態方法 Objects.equals(Object, Object)檢查這些屬性是否相等。
對於一些類,例如上的CaseInsensitiveString
類,屬性比較相對於簡單的相等性測試要複雜得多。在這種狀況下,你想要保存屬性的一個規範形式( canonical form),這樣 equals 方法就能夠基於這個規範形式去作開銷很小的精確比較,來取代開銷很大的非標準比較。這種方式其實最適合不可變類(條目 17)。一旦對象發生改變,必定要確保把對應的規範形式更新到最新。
equals方法的性能可能受到屬性比較順序的影響。 爲了得到最佳性能,你應該首先比較最可能不一樣的屬性,開銷比較小的屬性,或者最好是二者都知足(derived fields)。 你不要比較不屬於對象邏輯狀態的屬性,例如用於同步操做的lock 屬性。 不須要比較能夠從「重要屬性」計算出來的派生屬性,可是這樣作能夠提升equals方法的性能。 若是派生屬性至關於對整個對象的摘要描述,比較這個屬性將節省在比較失敗時再去比較實際數據的開銷。 例如,假設有一個Polygon類,並緩存該區域。 若是兩個多邊形的面積不相等,則沒必要費心比較它們的邊和頂點。
當你完成編寫完equals方法時,問你本身三個問題:它是對稱的嗎?它是傳遞嗎?它是一致的嗎?除此而外,編寫單元測試加以排查,除非使用AutoValue框架(第49頁)來生成equals方法,在這種狀況下能夠安全地省略測試。若是持有的屬性失敗,找出緣由,並相應地修改equals方法。固然,equals方法也必須知足其餘兩個屬性(自反性和非空性),但這兩個屬性一般都會知足。
在下面這個簡單的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; } ... // Remainder omitted }
如下是一些最後提醒:
// Broken - parameter type must be Object!public boolean equals(MyClass o) { … }
問題在於這個方法並無重寫Object.equals方法,它的參數是Object類型的,這樣寫只是重載了 equals 方法(Item 52)。 即便除了正常的方法以外,提供這種「強類型」的equals方法也是不可接受的,由於它可能會致使子類中的Override註解產生誤報,提供不安全的錯覺。
在這裏,使用Override註解會阻止你犯這個錯誤(條目 40)。這個equals方法不會編譯,錯誤消息會告訴你到底錯在哪裏:
// Still broken, but won’t compile @Override public boolean equals(MyClass o) { … }
編寫和測試equals(和hashCode)方法很繁瑣,生的代碼也很普通。替代手動編寫和測試這些方法的優雅的手段是,使用谷歌AutoValue開源框架,該框架自動爲你生成這些方法,只需在類上添加一個註解便可。在大多數狀況下,AutoValue框架生成的方法與你本身編寫的方法本質上是相同的。
不少 IDE(例如 Eclipse,NetBeans,IntelliJ IDEA 等)也有生成equals和hashCode方法的功能,可是生成的源代碼比使用AutoValue框架的代碼更冗長、可讀性更差,不會自動跟蹤類中的更改,所以須要進行測試。這就是說,使用IDE工具生成equals(和hashCode)方法一般比手動編寫它們更可取,由於IDE工具不會犯粗枝大葉的錯誤,而人類則會。
總之,除非必須:在不少狀況下,不要重寫equals方法,從Object繼承的實現徹底是你想要的。 若是你確實重寫了equals 方法,那麼必定要比較這個類的全部重要屬性,而且以保護前面equals約定裏五個規定的方式去比較。