[Java讀書筆記] Effective Java(Third Edition) 第 3 章 對於全部對象都通用的方法

第 10 條:覆蓋equals時請遵照通用約定程序員

在不覆蓋equals方法下,類的每一個實例都只與它自身相等。算法

  • 類的每一個實例本質上都是惟一的。
  • 類不須要提供一個」邏輯相等(logical equality)」的測試功能。
  • 父類已經重寫了 equals 方法,而且父類的行爲徹底適合於該子類。
  • 類是私有的或包級私有的,而且能夠肯定它的 equals 方法永遠不會被調用。

何時須要覆蓋equals方法?
  若是一個類包含一個邏輯相等( logical equality)的概念——此概念有別於對象同一性(object identity),並且父類尚未重寫過 equals 方法。數組

  這一般用在值類( value classes)的狀況。性能優化

在覆蓋equals 方法時,必須遵照它的通用規範。下面是 Object 類註釋裏的規範:ide

  • 自反性:x.equals(x) 必須返回 true
  • 對稱性:x.equals(y) 返回 true 當且僅當 y.equals(x) 返回 true
  • 傳遞性:若是 x.equals(y) 返回 true,y.equals(z) 返回 true,則x.equals(z) 必須返回 true
  • 一致性:若是在 equals 比較中使用的信息沒有修改,則 x.equals(y) 的屢次調用必須始終返回true或始終返回false
  • 對於任何非空引用 x,x.equals(null) 必須返回 false

編寫高質量 equals 方法的祕訣:工具

  • 使用 == 運算符檢查參數是否爲該對象的引用。若是是,返回true。
  • 使用 instanceof 運算符來檢查參數是否具備正確的類型。 若是不是,則返回 false。
  • 將參數轉換爲正確的類型。
  • 對於類中的每一個關鍵域(屬性),檢查參數的屬性是否與該對象對應的屬性相匹配。
  • 對於類型爲非 float 或 double 的基本類型,使用 == 運算符進行比較;對於對象引用屬性,遞歸地調用 equals 方法;對於 float 基本類型的屬性,使用靜態方法 Float.compare(float, float);對於 double 基本類型的屬性,使用 Double.compare(double, double) 方法。
  • 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時總要覆蓋hashCode。
  • 不要企圖讓equals方法過於智能。
  • 不要將equals聲明中的Object對象替換爲其餘的類型。

  總之,不要輕易覆蓋equals方法,除非無可奈何。由於不少狀況下,從Object繼承的實現正是你想要的。優化

  若是覆蓋equals方法,必定要比較這個類的全部關鍵域,而且確保遵照equals合約的五個條款。this

 

第 11 條:覆蓋equals時總要覆蓋hashCode

  在每個重寫 equals 方法的類中,都要重寫 hashCode 方法。
  若是不這樣作,你的類會違反 hashCode 的通用約定,這會阻止它在 HashMap 和 HashSet 這樣的集合中正常工做。
 

  Object源碼約定內容:

  • 在一個應用程序執行過程當中,若是在 equals 方法比較中沒有修改任何信息,在一個對象上重複調用 hashCode 方法必須始終返回相同的值。從一個應用程序到另外一個應用程序時返回的值能夠是不一致的。
  • 若是兩個對象根據 equals(Object) 方法比較是相等的,那麼在這兩個對象上調用 hashCode 就必須產生相同的整數結果。
  • 若是兩個對象根據 equals(Object) 方法比較並不相等,不要求在每一個對象上調用 hashCode 都必須產生不一樣的結果。 爲不相等的對象生成不一樣的結果可能會提升散列表(hash tables)的性能。

    沒有覆蓋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);
}
相關文章
相關標籤/搜索