1. 程式人生 > >Effective Java (3rd Editin) 讀書筆記:2 所有物件共有的方法

Effective Java (3rd Editin) 讀書筆記:2 所有物件共有的方法

2 所有物件共有的方法

Item 10:重寫 equals 方法時遵守通用協同

不需要重寫 equals() 方法的情況:

  1. 類的每一個例項都認為是不同。比如 Thread 這種代表活躍的實體而不是值
  2. 不需要“邏輯相等”的判斷。比如 Pattern 不需要檢查內嵌的正則表示式是否相等
  3. 父類已經重寫了合適的 equals() 方法。比如,AbstractList 等的子類
  4. private 或 package 訪問許可權的類。包外無法訪問,不需要

需要重寫 equals() 方法的情況(大多數 value class):

  1. 只比較是否是堆中的同一個物件不夠,還需要判斷“邏輯相等”
  2. 父類沒有合適的 equals() 方法

有一些 value class 不需要重寫,因為它們能確保每一個值最多隻存在一個物件,比如 Enum 型別。

Object 類的 API 文件註釋中,說明了 equals() 方法的通用協同。

繼承一個可例項化的類並增加新屬性的時候,必然會違反 equals 協同

public class Point {
    private final int x, y;
    // ...
    @Override public boolean equals(Object o) {
        if (!(o instance of Point)
) return false; Point p = (Point) o; return p.x == x && p.y == y; } } public class ColorPoint extends Point{ private final Color color; // ... @Override public boolean equals(Object o) { // 違反了對稱性 if (!(o instance of ColorPoint)) return false; return
super.equals(o) && ((ColorPoint) o).color == color; } }

Point 物件可能等於某個 ColorPoint 物件,但是反之不成立,所以違反了對稱性。比如 java.util.Date 和它的子類 java.sql.Timestamp 之間就存在這個問題,它們不能混合比較,否則會產生混亂,而這個問題只能靠程式設計師在使用時自己注意。

解決上述問題的一個妥協方法是,用組合代替繼承

public class ColorPoint {
    private final Point point;
    private final Color color;
    // ...
    public Point asPoint() { return point; }
    @Override public boolean equals(Object o) {
        if (!(o instance of ColorPoint)) return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}

重寫 equals() 方法的建議:

  1. 使用 == 檢查是否是堆中的同一個物件
  2. 使用 instanceOf 檢查物件型別,並考慮了 null 的情況
  3. 將引數物件強制型別轉換為正確型別
  4. 對於物件中的每一個“有意義”的欄位屬性,檢查是否和測試物件相等,元素資料型別可以用 ==,float 和 double 用包裝類的 compare() 靜態方法,陣列使用 Arrays.equals() 方法,引用型別使用考慮了 null 的 Objects.equals() 方法。
  5. 最後,不要忘了,重寫 equals 方法時也要重寫 hashCode 方法(Item 11)

例如,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;
}

Item 11:重寫 equals 方法時也要重寫 hashCode 方法

如果不做到這一條的話,在使用 HashMap 和 HashSet 等會使用雜湊值的集合類時,會出現異常。

Object 的 API 文件中同樣規定了 hashCode 的通用協同:

  1. 一個程序中,同一個物件多次呼叫 hashCode 方法,如果 equal 方法中比較的欄位屬性沒有修改的話,它應該返回相同的 int 值
  2. equals 方法返回 true 的兩個物件,hashCode 方法返回相等的 int 值
  3. equals 方法返回 false 的兩個物件,不要求但是牆裂建議 hashCode 方法返回不相等的 int 值

重寫 equals 方法時沒有重寫 hashCode 方法的話,很可能會違反上述協同的第二條。

為了儘可能滿足第三條規定,這個有一個生成雜湊值的建議:

  1. 宣告 int 變數 result,初始化為你的物件的第一個重要欄位的雜湊值 c ,計算方法如 2.a.(“重要欄位”是指會影響 equals 比較的欄位)

  2. 物件中剩下的每個重要欄位,都按如下處理:

    a. 計算得到該欄位的 int 型別雜湊值 c :

    • ⅰ. 欄位是原始資料型別,使用包裝類的 hashCode 靜態函式來計算
    • ⅱ. 欄位是物件引用,如果 equals 方法中在比較此欄位時會遞迴呼叫它的 equals 方法,那麼這裡也要遞迴呼叫它的 hashCode 方法。如果欄位為 null,使用 0
    • ⅲ. 欄位是陣列,把陣列中的每個重要元素當作欄位,使用 2.a. 中的規則計算每個欄位的雜湊值,使用 2.b. 中的規則組合這些值。如果陣列沒有重要元素,使用一個常量(最好不要是 0);如果陣列中每個元素都重要,使用 Arrays.hashCode

    b. 將 2.a. 中計算得到的雜湊值 c 按照如下規則組合進 result 中:result = 31 * result + c

  3. 返回 result

之所以會用乘數因子 31 ,是因為它是素數而且 31 * i == (i << 5) - i,JVM 會自動完成此優化。

// java.lang.String 的 hashCode
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) { // 懶載入
        char val[] = value;
		// 重要欄位為陣列
        for (int i = 0; i < value.length; i++) { 
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

Item 12:總是重寫 toString 方法

設計良好的 toString 方法使得物件使用起來很愜意,也方便 debug 過程。儘可能使 toString 方法返回物件中的所有重要資訊。如果你決定在 toString 的 API 文件中指明返回格式的話,其他程式設計師可能為此量身定做一些持久化的物件儲存和解析的工具,這可能會限制未來的類的更新。

比如,AbstractMap 的 toString 方法,規定了返回格式為 {k1=v1, k2=v2} ,或 {}

/**
 * Returns a string representation of this map.  The string representation
 * consists of a list of key-value mappings in the order returned by the
 * map's <tt>entrySet</tt> view's iterator, enclosed in braces
 * (<tt>"{}"</tt>).  Adjacent mappings are separated by the characters
 * <tt>", "</tt> (comma and space).  Each key-value mapping is rendered as
 * the key followed by an equals sign (<tt>"="</tt>) followed by the
 * associated value.  Keys and values are converted to strings as by
 * {@link String#valueOf(Object)}.
 *
 * @return a string representation of this map
 */

Item 13:重寫 clone 方法要慎重

immutable 類不需要提供 clone 方法。

ArrayList 的 clone 方法:

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

使用父類的 super.clone(),只是簡單的欄位賦值,原始資料型別和 immutable 欄位不會出問題,但是對於 mutable 的物件引用型別的欄位,拷貝前後,引用的是同一個堆中的物件,因此需要額外的拷貝工作。

clone 方法最大的亮點是對陣列拷貝的實現,它會新建一個新陣列,並進行元素的賦值,和 Arrays.copyOf 的效果一樣。

另一個大問題是,clone 方法中不要呼叫子類可能會重寫的方法,否則會導致子類還為拷貝完成,就執行了自己的方法,有安全隱患,因此呼叫的方法最好有 final 修飾,比如 HashMap 的 clone 方法中呼叫了 final 型別的 putMapEntries 方法。

@Override
public Object clone() {
    HashMap<K,V> result;
    try {
        result = (HashMap<K,V>)super.clone();
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
    result.reinitialize();
    result.putMapEntries(this, false);
    return result;
}

考慮到 clone 方法的安全隱患,當你需要拷貝的功能時,最好考慮提供拷貝構造器或拷貝工廠方法,它們有如下優點:

  1. 沒有 clone 的種種注意事項
  2. 不會和 final 欄位衝突
  3. 不需要多餘的 try-catch 語句
  4. 不需要強制型別轉換
  5. 支援基於介面的拷貝

如下是 HashMap 中基於 Map 介面的拷貝構造器:

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

Item 14:考慮實現 Comparable 介面

Comparable 的 API 文件中指明瞭 compareTo 方法的通用協同(sgn 函式將負數、0、正數轉換為 -1、0、1):

  1. sgn(x.compareTo(y)) == - sgn(y.compareTo(x)),前者丟擲異常的充要條件是後者也丟擲異常
  2. transitive:x.compareTo(y) > 0 && y.compateTo(z) > 0,可推匯出 x.compareTo(z) > 0
  3. x.compareTo(y) == 0,可推匯出 sgn(x.compareTo(z)) == sgn(y.compareTo(z))
  4. 需要補充的是,不要求但是牆裂推薦,(x.compareTo(y) == 0) == (x.equals(y)),不滿足這條要求的 compareTo 方法必須在 API 文件中宣告:“Note: This class has a natural ordering that is inconsistent with equals.”

和 equals 方法類似,繼承一個可例項化的類並增加新屬性的時候,必然會違反 compareTo 協同 。解決方法也是一樣,使用組合代替繼承

協同的第四項需要特別注意,比如 BigDecimal 類就不滿足此建議,有如下效果:

Set<BigDecimal> treeSet = new TreeSet<BigDecimal>();
Set<BigDecimal> hashSet = new HashSet<BigDecimal>();
treeSet.add(new BigDecimal("1.0"));
hashSet.add(new BigDecimal("1.0"));
treeSet.add(new BigDecimal("1.00"));
hashSet.add(new BigDecimal("1.00"));
System.out.println(treeSet.size() == 1); // true
System.out.println(hashSet.size() == 2); // true

這是因為排序集合類(如 TreeSet,TreeMap)使用 compareTo 來判斷元素是否相等,而一般集合類使用 equals 來判斷元素是否相等。

原始資料型別的比較,避免使用 < > 符號,或者減法(可能溢位),建議使用包裝類的 compare 靜態方法,或者用 Comparator 介面的鏈式構造方法(會有少量效能損耗,但可讀性強且簡潔)。

private static final Comparator<PhoneNumber> COMPARATOR = 
    Comparator.compaingInt((PhoneNumber pn) -> pn.areaCode)
    	.thenComparingInt(pn -> pn.prefix)
    	.thenComparingInt(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) {
    return COMPATATOR.compare(this, pn);
}