第 10 條:覆蓋equals時請遵照通用約定程序員
在不覆蓋equals方法下,類的每一個實例都只與它自身相等。算法
何時須要覆蓋equals方法?
若是一個類包含一個邏輯相等( logical equality)的概念——此概念有別於對象同一性(object identity),並且父類尚未重寫過 equals 方法。數組
這一般用在值類( value classes)的狀況。性能優化
在覆蓋equals 方法時,必須遵照它的通用規範。下面是 Object 類註釋裏的規範:ide
編寫高質量 equals 方法的祕訣:工具
例如String 的例子:性能
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
最後的告誡:測試
總之,不要輕易覆蓋equals方法,除非無可奈何。由於不少狀況下,從Object繼承的實現正是你想要的。優化
若是覆蓋equals方法,必定要比較這個類的全部關鍵域,而且確保遵照equals合約的五個條款。this
第 11 條:覆蓋equals時總要覆蓋hashCode
在每個重寫 equals 方法的類中,都要重寫 hashCode 方法。
若是不這樣作,你的類會違反 hashCode 的通用約定,這會阻止它在 HashMap 和 HashSet 這樣的集合中正常工做。
Object源碼約定內容:
沒有覆蓋hashCode違反上述規約第二條:相等對象必須具備相等的散列碼(hashCode)。
一個好的 hash 方法趨向於爲不相等的實例生成不相等的哈希碼。
理想狀況下,hash 方法爲集合中不相等的實例均勻地分配 int 範圍內的哈希碼。實現這種理想狀況可能很困難。
簡單步驟:
1. 聲明一個 int 類型的變量 result,並將其初始化爲對象中第一個重要屬性 c 的哈希碼,以下面步驟 2.a 中所計算的那樣。
2. 對於對象中剩餘的重要屬性 f ,執行如下操做:
a. 爲屬性 f 與計算一個 int 類型的哈希碼 c:
i. 若是這個屬性是基本類型,使用 Type.hashCode(f) 方法計算,其中 Type 類是對應屬性 f 的包裝類。
ii. 若是該屬性是一個對象引用,而且該類的 equals 方法經過遞歸調用 equals 來比較該屬性,那麼遞歸地調用 hashCode 方法。
若是須要更復雜的比較,則計算此字段的「範式」(canonical representation),並在範式上調用 hashCode 。
若是該字段的值爲空,則使用 0(也可使用其餘常數,但一般使用 0 表示)。
iii. 若是屬性 f 是一個數組,把數組中每一個重要的元素都看做是一個獨立的屬性。
若是數組沒有重要的元素,則使用一個常量,最好不要爲0。若是全部元素都很重要,則使用 Arrays.hashCode 方法。
b. 將步驟 2.a 中計算出的哈希碼 c 合併爲以下結果:result = 31 * result + c;
3. 返回 result 值。
例子:
// Typical hashCode method @Override public int hashCode() { int result = Short.hashCode(areaCode); result = 31 * result + Short.hashCode(prefix); result = 31 * result + Short.hashCode(lineNum); return result; }
寫完以後驗證,問本身「相等的實例是否都具備相等的散列碼」。
總之,每當覆蓋equals方法時都必須覆蓋hashCode,不然程序將沒法正確運行。
還能夠利用AutoValue(google)生成equals和hashCode方法,沒必要手工編寫,能夠省略測試。部分IDE也提供相似的部分功能。
第 12 條:始終要覆蓋toString
提供好的易讀的toString實現可讓使用這個類的系統更容易調試。
在實際應用中,toString方法應該返回對象中包含的全部值得關注的信息。不管是否指定格式,應該在文檔中明確地代表你的意圖。
在靜態工具類中編寫toString方法時沒有意義的,也不用在大多數枚舉類型中編寫toString方法。
Google開源的AutoValue會替你生成toString方法。
總之,要在你編寫的每個可實例化的類中覆蓋Object的toString實現,除非已經在超類中這麼作了。
這樣會讓類的使用易於調試。toString方法應該返回一個關於對象的簡潔、有用的描述。
第 13 條:謹慎地覆蓋clone
Cloneable接口的目的是做爲對象的一個接口,代表這樣的對象容許克隆(clone)。
實現Cloneable接口的類是爲了提供一個功能適當的公有的clone方法。
假設你但願在一個類中實現Cloneable接口,它的父類提供了一個行爲良好的 clone方法。
首先調用super.clone。 獲得的對象將是原始的徹底功能的複製品。 在你的類中聲明的任何屬性將具備與原始屬性相同的值。
若是每一個屬性包含原始值或對不可變對象的引用,則返回的對象可能正是你所須要的,在這種狀況下,不須要進一步的處理。(淺拷貝)
不可變的類永遠都不該該提供clone方法。
若是對象包含引用可變對象的屬性,則前面顯示的簡單clone實現多是災難性的。
例子:
ublic class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { this.elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // Eliminate obsolete reference return result; } // Ensure space for at least one more element. private void ensureCapacity() { if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } }
上述例子但願作成可克隆的,若是clone方法僅返回super.clone()調用的對象,那麼生成的Stack實例在其size 屬性中具備正確的值,
但elements屬性引用與原始Stack實例相同的數組。 修改原始實例將破壞克隆中的約束條件,反之亦然。
你會很快發現你的程序產生了無心義的結果,或者拋出NullPointerException異常。
實際上,clone方法就是另外一個構造器,必須確保它不會傷害到原始的對象,並確保正確地建立被克隆對象中的約束條件。
上述例子在elements數組中遞歸地調用clone:
// Clone method for class with references to mutable state @Override public Stack clone() { try { Stack result = (Stack) super.clone(); result.elements = elements.clone(); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } }
在數組上調用clone返回的數組,編譯時的類型與被克隆數組的類型相同,這是複製數組的最佳習慣。
若是elements屬性是final的,則之前的解決方案將不起做用,由於克隆將被禁止向該屬性分配新的值。
這是一個基本的問題:像序列化同樣,Cloneable體系結構與引用可變對象的final 屬性的正常使用不兼容。
僅僅遞歸地調用clone方法並不老是足夠的。
例如一個類包含一個散列桶數組,每一個散列通都指向鍵-值對鏈表第一項,是一個單向鏈表。
若是僅克隆散列數組,可是這個數組引用的鏈表與原始對象同樣,容易引發克隆對象和原始對象中不肯定行爲。全部必須單獨地拷貝並組成每一個桶的鏈表。
簡而言之,實現Cloneable的全部類應該重寫公共clone方法,而這個方法的返回類型是類自己。
這個方法應該首先調用super.clone,而後修復任何須要修復的屬性。
一般,這意味着複製任何包含內部「深層結構」的可變對象,並用指向新對象的引用來代替原來指向這些對象的引用。
雖然這些內部拷貝一般能夠經過遞歸調用clone來實現,但這並不老是最好的方法。
若是類只包含基本類型或對不可變對象的引用,那麼極可能是沒有屬性須要修復的狀況。
這個規則也有例外, 例如,表示序列號或其餘惟一ID的屬性即便是基本類型的或不可變的,也須要被修正。
對象拷貝的更好方法時提供一個拷貝構造器(copy constructor)或拷貝工廠(copy factory)。
// Copy constructor public Yum(Yum yum) { ... };
// Copy factory public static Yum newInstance(Yum yum) { ... };
總之,考慮到與Cloneable接口相關的全部問題,新的接口不該該繼承它,新的可擴展類不該該實現它。
雖然實現Cloneable接口對於final類沒有什麼危害,但應該將其視爲性能優化的角度,僅在極少數狀況下才是合理的(條目67)。
一般,複製功能最好由構造方法或工廠提供。 這個規則的一個明顯的例外是數組,它最好用 clone方法複製。
第 14 條:考慮實現Comparable接口
與本章討論的其餘方法不一樣,compareTo 方法並無在 Object 類中聲明。
相反,它是 Comparable 接口中的惟一方法。 經過實現 Comparable 接口,一個類代表它的實例有一個天然序( natural ordering )。
經過實現 Comparable 接口,可讓你的類與全部依賴此接口的泛型算法和集合實現進行交互操做。
Java 平臺類庫中幾乎全部值類以及全部枚舉類型(條款 34)都實現了 Comparable 接口。
若是你正在編寫具備明顯天然序(如字母順序、數字順序或時間順序)的值類,則應該實現 Comparable 接口:
public interface Comparable<T> { int compareTo(T t); }
將此對象與指定的對象按照排序進行比較。返回值可能爲負整數,零或正整數,對應此對象小於,等於或大於指定的對象。
compareTo不能跨越不一樣類型的對象進行比較,在比較不一樣類型的對象時,拋出ClassCastException異常。
考慮 BigDecimal 類,其 compareTo 方法與 equals 不一致。
若是你建立一個空的 HashSet 實例,而後添加 new BigDecimal("1.0") 和 new BigDecimal("1.00"),則該集合將包含兩個元素,
由於用 equals 方法進行比較時,添加到集合的兩個 BigDecimal 實例是不相等的。
可是,若是使用 TreeSet 而不是 HashSet 執行相同的過程,則該集合將只包含一個元素,由於使用 compareTo 方法進行比較時,兩個 BigDecimal 實例是相等的。
在 Java 7 中,靜態比較方法被添加到 Java 的全部包裝類中。在 compareTo 方法中使用關係運算符 < 和 > 是冗長且容易出錯的,再也不推薦。
在 Java 8 中 Comparator 接口提供了一系列比較器方法,能夠流暢地構建比較器。
許多程序員更喜歡這種方法的簡潔性,儘管它會犧牲必定地性能。在使用這種方法時,考慮使用 Java 的靜態導入,以即可以經過其簡單名稱來引用比較器靜態方法。
// Comparable with comparator construction methods private static final Comparator<PhoneNumber> COMPARATOR = comparingInt((PhoneNumber pn) -> pn.areaCode) .thenComparingInt(pn -> pn.prefix) .thenComparingInt(pn -> pn.lineNum); public int compareTo(PhoneNumber pn) { return COMPARATOR.compare(this, pn); }