1. 程式人生 > >《Effective Java》Second Edition中文版筆記(完整)

《Effective Java》Second Edition中文版筆記(完整)

第2章 建立和銷燬物件

第1條:考慮用靜態工廠方法代替構造器

  1. 一個類只能有一個帶有指定簽名的構造器。程式設計人員通常知道如何避開這一限制:通過提供兩個構造器,它們的引數列表只在引數型別的順序上有所不同。實際上這並不是個好主意。面對這樣的API,使用者永遠也記不住該用哪個構造器,結果常常會呼叫錯誤的構造器。並且,讀到使用了這些構造器的程式碼時,如果沒有參考類的文件,往往不知所云。
  2. 由於靜態工廠方法有名稱,所以它們不受上述的限制。當一個類需要多個帶有相同簽名的構造器時,就用靜態工廠方法代替構造器,並且慎重地選擇名稱以便突出它們之間的區別。
  3. 靜態工廠方法能夠為重複的呼叫返回相同的物件,這樣有助於類總能嚴格控制在某個時刻哪些例項應該存在。這種類被稱作例項受控的類(instance-controlled)。編寫例項受控的類有幾個原因。例項受控使得類可以確保它是一個Singleton或者是不可例項化的。
  4. 靜態工廠方法與構造器不同的第三大優勢在於,它們可以返回原返回型別的任何子型別的物件。

第2條:遇到多個構造器引數時要考慮用構建器

  1. 靜態工廠和構造器有個共同的侷限性:它們都不能很好地擴充套件到大量的可選引數。
  2. 遇到許多構造器引數的時候,還有第二種代替方法,即JavaBeans模式,在這種模式下,呼叫一個無參構造器來建立物件,然後呼叫setter方法來設定每個必要的引數,以及每個相關的可選引數。
  3. 遺憾的是,JavaBeans模式自身有著很嚴重的缺點。因為構造過程被分到了幾個呼叫中,在構造過程中JavaBean可能處於不一致的狀態。
  4. 與此相關的另一點不足在於,JavaBeans模式阻止了把類做成不可變的可能,這就需要程式設計師付出額外的努力來確保它的執行緒安全。
  5. 注意NutritionFacts是不可變的,所有的預設引數值都單獨放在一個地方。builder的setter方法返回builder本身,以便可以把呼叫連結起來。下面就是客戶端程式碼:
NutritionFacts cocaCola = new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();

builder模式模擬了具名的可選引數,就像Ada和Python中的一樣。

// Builder Pattern
public class NutritionFacts {
    private
final int ServingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public static class Builder { // Required Parameters private final int servingSize; private final int servings; // Optional parameters - initialized to default values private int calories = 0; private int fat = 0; private int carbohydrate = 0; private int sodium = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; } }

第3條:用私有構造器或者列舉型別強化Singleton屬性

//Singleton with public final field
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }

    public void leaveTheBuilding() { ... }

私有構造器僅被呼叫一次,用來例項化公有的靜態final域Elvis.INSTANCE。由於缺少公有的或者受保護的構造器,所以保證了Elvis的全域性唯一性:一旦Elvis類被例項化,只會存在一個Elvis例項,不多也不少。

//Singleton with static factory
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() { ... }
}

公有域方法的主要好處在於,組成類的成員的宣告很清楚地表明瞭這個類是一個Singleton:公有的靜態域是final的,所以該域將總是包含相同的物件引用。公有域方法在效能上不再有任何優勢:現代的JVM實現幾乎都能夠將靜態工廠方法的呼叫內聯化。

為了使利用這其中一種方法實現的Singleton類變成是可序列化的(Serializable),僅僅在宣告中加上“implements Serializable”是不夠的。為了維護並保證Singleton,必須宣告所有例項域都是瞬時(transient)的,並提供一個readResolve方法。

從Java 1.5發行版本起,實現Singleton還有第三種方法。只需編寫一個包含單個元素的列舉型別:

//Enum singleton - the preferred approach
public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() { ... }
}

這種方法在功能上與公有域方法相近,但是它更加簡潔,無償提供了序列化機制,絕對防止多次例項化,即使是在面對複雜的序列化或者反射攻擊的時候。雖然這種方法還沒有廣泛採用,但是單元素的列舉型別已經成為實現Singleton的最佳方法。

第4條:通過私有構造器強化不可例項化的能力

  1. 有時候,你可能需要編寫只包含靜態方法和靜態域的類。這些類的名聲很不好,因為有些人在面向物件的語言中濫用這樣的類來編寫過程化的程式。儘管如此,它們也確實有它們特有的用處。我們可以利用這種類,以java.lang.Math或者java.util.Arrays的方式,把基本型別的值或者陣列型別上的相關方法組織起來。我們也可以通過java.util.Collections的方式,把實現特定介面的物件上的靜態方法(包括工廠方法)組織起來。最後,還可以利用這種類把final類上的方法組織起來,以取代擴充套件該類的做法。
  2. 這樣的工具類(utility class)不希望被例項化,例項對它沒有任何意義。然而,在缺少顯式構造器的情況下,編譯器會自動提供一個公有的、無參的預設構造器(default constructor)。對於使用者而言,這個構造器與其他的構造器沒有任何區別。
  3. 企圖通過將類做成抽象類來強制該類不可被例項化,這是行不通的。該類可以被子類化,並且該子類也可以被例項化。這樣做甚至會誤導使用者,以為這種類是專門為了繼承而設計的。
// Noninstantiable utility class
public class UtilityClass {
    //Suppress default constructor for noninstantiability
    private UtilityClass() {
        throw new AssertionError();
    }
    ... //Remainder omitted
}

第5條:避免建立不必要的物件

  1. 如果物件是不可變的(immutable),它就始終可以被重用。
  2. 傳遞給String構造器的引數(”stringette”)本身就是一個String例項,功能方面等同於構造器建立的所有物件。
  3. 而且,它可以保證,對於所有在同一臺虛擬機器中執行的程式碼,只要它們包含相同的字串字面常量,該物件就會被重用。
  4. 對於同時提供了靜態工廠方法和構造器的不可變類,通常可以使用靜態工廠方法而不是構造器,以避免建立不必要的物件。例如,靜態工廠方法Boolean.valueOf(String)幾乎總是優先於構造器Boolean(String)。
  5. 下面的版本用一個靜態的初始化器(initializer),避免了這種效率低下的情況:
class Person {
    private final Date birthDate;
    // Other fields, methods, and constructor omitted

    private static final Date BOOM_START;
    private static final Date BOOM_END;

    static {
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_START = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_END = gmtCal.getTime();
    }

    public boolean isBabyBoomer() {
        return birthDate.compareTo(BOOM_START) >= 0 && 
            birthDate.compareTo(BOOM_END) < 0;
    }
}

改進後的Person類只在初始化的時候建立Calendar、TimeZone和Date例項一次,而不是在每次呼叫isBabyBoomer的時候都建立這些例項。

把boomStart和boomEnd從區域性變數改為final靜態域,這些日期顯然是被作為常量對待,從而使得程式碼更易於理解。

如果改進後的Person類被初始化了,它的isBabyBoomer方法卻永遠不會被呼叫,那就沒有必要初始化BOOM_START和BOOM_END域。通過延遲初始化(lazily initializing),即把對這些域的初始化延遲到isBabyBoomer方法第一次被呼叫的時候進行,則有可能消除這些不必要的初始化工作,但是不建議這樣做。正如延遲初始化中常見的情況一樣,這樣做會使方法的實現更加複雜,從而無法將效能顯著提高到超過已經達到的水平。

考慮介面卡(adpater)的情形,有時也叫做檢視(view)。介面卡是指這樣一個物件:它把功能委託給一個後備物件(backing object),從而為後備物件提供一個可以替代的介面。由於介面卡除了後備物件之外,沒有其他的狀態資訊,所以針對某個給定物件的特定介面卡而言,它不需要建立多個介面卡例項。

例如,Map介面的keySet方法返回該Map物件的Set檢視,其中包含該Map中所有的鍵(key)。粗看起來,好像每次呼叫keySet都應該建立一個新的Set例項,但是,對於一個給定的Map物件,實際上每次呼叫keySet都返回同樣的Set例項。雖然被返回的Set例項一般是可改變的,但是所有返回的物件在功能上是等同的:當其中一個返回物件發生變化的時候,所有其他的返回物件也要發生變化,因為它們是由同一個Map例項支撐的。

變數sum被宣告成Long而不是long,意味著程式構造了大約231個多餘的Long例項(大約每次往Long sum中增加long時構造一個例項)。結論很明顯:要優先使用基本型別而不是裝箱基本型別,要當心無意識的自動裝箱。

真正正確使用物件池的典型物件示例就是資料庫連線池。建立資料庫連線的代價是非常昂貴的,因此重用這些物件非常有意義。而且,資料庫的許可可能限制你只能使用一定數量的連線。但是,一般而言,維護自己的物件池必定會把程式碼弄得很亂,同時增加記憶體佔用(footprint),並且還會損害效能。現代的JVM實現具有高度優化的垃圾回收器,其效能很容易就會超過輕量級物件池的效能。

第6條:消除過期的物件引用

  1. 如果一個棧先是增長,然後再收縮,那麼,從棧中彈出來的物件將不會被當作垃圾回收,即使使用棧的程式不再引用這些物件,它們也不會被回收。這是因為,棧內部維護著對這些物件的過期引用(obsolete reference)。所謂的過期引用,是指永遠也不會再被解除的引用。在本例中,凡是在elements陣列的”活動部分(active portion)”之外的任何引用都是過期的。活動部分是指elements中下標小於size的那些元素。
  2. 在支援垃圾回收的語言中,記憶體洩漏是很隱蔽的(稱這類記憶體洩漏為“無意識的物件保持(unintentional object retention)”更為恰當。
  3. 清空物件引用應該是一種例外,而不是一種規範行為。
  4. 一般而言,只要類是自己管理記憶體,程式設計師就應該警惕記憶體洩漏問題。
  5. 如果你正好要實現這樣的快取:只要在快取之外存在對某個項的鍵的引用,該項就有意義,那麼就可以用WeakHashMap代表快取;當快取中的項過期之後,它們就會自動被刪除。記住只有當所要的快取項的生命週期是由該鍵的外部引用而不是由值決定時,WeakHashMap才有用處。
  6. 記憶體洩漏的第三個常見來源是監聽器和其他回撥。如果你實現了一個API,客戶端在這個API中註冊回撥,卻沒有顯式地取消註冊,那麼除非你採取某些動作,否則它們就會積聚。確保回撥立即被當作垃圾回收的最佳方法是隻儲存它們的弱引用(weak reference),例如,只將它們儲存成WeakHashMap中的鍵。

第7條:避免使用終結方法

  1. 終結方法(finalizer)通常是不可預測的,也是很危險的,一般情況下是不必要的。
  2. C++的析構器也可以被用來回收其他的非記憶體資源。而在Java中,一般用try-finally塊來完成類似的工作。
  3. 這意味著,注重時間(time-critical)的任務不應該由終結方法來完成。例如,用終結方法來關閉已經開啟的檔案,這是嚴重錯誤,因為開啟檔案的描述符是一種很有限的資源。
  4. 一位同事最近在除錯一個長期執行的GUI應用程式的時候,該應用程式莫名其妙地出現OutOfMemoryError錯誤而死掉。分析表明,該應用程式死掉的時候,其終結方法佇列中有數千個圖形物件正在等待被終結和回收。
  5. 不要被System.gc和System.runFinalization這兩個方法所誘惑,它們確實增加了終結方法被執行的機會,但是它們並不保證終結方法一定會被執行。唯一聲稱保證終結方法被執行的方法是System.runFinalizersOnExit,以及它臭名昭著的孿生兄弟Runtime.runFinalizersOnExit。這兩個方法都有致命的缺陷,已經被廢棄了[ThreadStop]。
  6. 正常情況下,未被捕獲的異常將會使執行緒終止,並打印出軌跡棧(Stack Trace),但是,如果異常發生在終結方法之中,則不會如此,甚至連警告都不會打印出來。
  7. 那麼,如果類的物件中封裝的資源(例如檔案或者執行緒)確實需要終止,應該怎麼做才能不用編寫終結方法呢?只需提供一個顯式的終止方法,並要求該類的客戶端在每個例項不再有用的時候呼叫這個方法。
  8. 顯示終止方法的典型例子是InputStream、OutputStream和java.sql.Connection上的close方法。另一個例子是java.util.Timer上的cancel方法,它執行必要的狀態改變,使得與Timer例項相關聯的該執行緒溫和地終止自己。java.awt中的例子還包括Graphics.dispose和Window.dispose。這些方法通常由於效能不好而不被人們關注。一個相關的方法是Image.flush,它會釋放所有與Image例項相關聯的資源,但是該例項仍然處於可用的狀態,如果有必要的話,會重新分配資源。
  9. 顯式的終止方法通常與try-finally結構結合起來使用,以確保及時終止。在finally子句內部呼叫顯式的終止方法,可以保證即使在使用物件的時候有異常丟擲,該終止方法也會執行。
  10. 但是如果終結方法發現資源還未被終止,則應該在日誌中記錄一條警告,因為這表示客戶端程式碼中的一個Bug,應該得到修復。
  11. 顯式終止方法模式的示例中所示的四個類(FileInputStream、FileOutputStream、Timer和Connection),都具有終結方法,當它們的終止方法未能被呼叫的情況下,這些終結方法充當了安全網。
  12. 本地對等體是一個本地物件(native object),普通物件通過本地方法(native method)委託給一個本地物件。因為本地對等體不是一個普通物件,所以垃圾回收器不會知道它,當它的Java對等體被回收的時候,它不會被回收。在本地對等體並不擁有關鍵資源的前提下,終結方法正式執行這項任務最合適的工具。

第3章 對於所有物件都通用的方法

  1. 儘管Object是一個具體類,但是設計它主要是為了擴充套件。它所有的非final方法(equals、hashCode、toString、clone和finalize)都有明確的通用約定(general contract),因為它們被設計成是要被覆蓋(override)的。任何一個類,它在覆蓋這些方法的時候,都有責任遵守這些通用約定;如果不能做到這一點,其他依賴於這些約定的類(例如HashMap和HashSet)就無法結合該類一起正常運作。
  2. 而Comparable.compareTo雖然不是Object方法,但是本章也對它進行討論,因為它具有類似的特徵。
  3. Object的equals方法:
public boolean equals(Object obj) {
    return (this == obj);
}

第8條:覆蓋equals時請遵守通用約定

  1. 類的每個例項本質上都是唯一的。對於代表活動實體而不是值(value)的類來說確實如此,例如Thread。Object提供的equals實現對於這些類來說正是正確的行為。
  2. 超類已經覆蓋了equals,從超類繼承過來的行為對於子類也是合適的。例如,大多數的Set實現都從AbstractSet繼承equals實現,List實現從AbstractList繼承equals實現,Map實現從AbstractMap繼承equals實現。

java.util.AbstractSet的equals方法:

public boolean equals(Object o) {
    if (o==this)
        return true;
    if(!(o instanceof Set))
        return false;
    Collection c = (Collection) o;
    if (c.size() != size())
        return false;
    try {
        return containsAll(c);
    } catch (ClassCastException unused) {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }
}

java.util.AbstractMap的equals方法:

public boolean equals(Object o) {
    if (o==this)
        return true;
    if (!(o instanceof Map))
        return false;
    Map<K,V> m = (Map<K,V>) o;
    if (m.size()!=size())
        return false;
    try {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        while(i.hasNext()) {
            Entry<K,V> e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            if (value==null) {
                if(!(m.get(key)==null && m.containsKey(key)))
                    return false;
            } else {
                if(!value.equals(m.get(key)))
                    return false;
            }
        }
    } catch (ClassCastException unused) {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }
    return true;
}

類是私有的或是包級私有的,可以確定它的equals方法永遠不會被呼叫。在這種情況下,無疑是應該覆蓋equals方法的,以防它被意外呼叫:

@override public boolean equals(Object o) {
    throw new AssertionError(); //Method is never called
}

這通常屬於“值類(value class)”的情形。值類僅僅是一個表示值的類,例如Integer或者Date。程式設計師在利用equals方法來比較值物件的引用時,希望知道它們在邏輯上是否相等,而不是想了解它們是否指向同一個物件。

有一種“值類”不需要覆蓋equals方法,即用例項受控確保“每個值至多隻存在一個物件”的類。列舉型別就屬於這種類。對於這樣的類而言,邏輯相同與物件等同是一回事,因此Object的equals方法等同於邏輯意義上的equals方法。

對於既不是float也不是double型別的基本型別域,可以使用==操作符進行比較;對於物件引用域,可以遞迴地呼叫equals方法;對於float域,可以使用Float.compare方法;對於double域,則使用Double.compare。對float和double域進行特殊的處理是有必要的,因為存在著Float.NaN、-0.0f以及類似的double常量;詳細資訊請參考Float.equals的文件。對於陣列域,則要把以上這些指導原則應用到每個元素上。如果陣列域中的每個元素都很重要,就可以使用發行版本1.5中新增的其中一個Arrays.equals方法。

有些物件引用域包含null可能是合法的,所以,為了避免可能導致NullPointerException異常,則使用下面的習慣用法來比較這樣的域:

(field==null ? o.field==null : field.equals(o.field))

域的比較順序可能會影響到equals方法的效能。為了獲得最佳的效能,應該最先比較最有可能不一致的域,或者是開銷最低的域,最理想的情況是兩個條件同時滿足的域。你不應該去比較那些不屬於物件邏輯狀態的域,例如用於同步操作的Lock域。

覆蓋equals時總要覆蓋hashCode。

把任何一種別名形式考慮到等價的範圍內,往往不會是個好主意。例如,File類不應該試圖把指向同一個檔案的符號連結(symbolic link)當作相等的物件來看。所幸File類沒有這樣做。

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

問題在於,這個方法並沒有覆蓋Object.equals,因為它的引數應該是Object型別,相反,它過載(overload)了Object.equals。在原有equals方法的基礎上,再提供一個“強型別(strongly typed)”的equals方法,只要這兩個方法返回同樣的結果,那麼這就是可以接受的。在某些特定的情況下,它也許能夠稍微改善效能,但是與增加的複雜性相比,這種做法是不值得的。@override註解的用法一致,就如本條目中所示,可以防止犯這種錯誤。這個equals方法不能編譯,錯誤訊息會告訴你到底哪裡出了問題:

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

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

  1. 在應用程式執行期間,只要物件的equals方法的比較操作所用到的資訊沒有被修改,那麼對這同一個物件呼叫多次,hashCode方法都必須始終如一地返回同一個整數。
  2. 如果兩個物件根據equals(Object)方法比較是相等的,那麼呼叫這兩個物件中任意一個物件的hashCode方法都必須產生同樣的整數結果。
  3. 如果兩個物件根據equals(Object)方法比較是不相等的,那麼呼叫這兩個物件中任意一個物件的hashCode方法,則不一定要產生不同的整數結果。但是程式設計師應該知道,給不相等的物件產生截然不同的整數結果,有可能提高散列表(hash table)的效能。
  4. 由於PhoneNumber類沒有覆蓋hashCode方法,從而導致兩個相等的例項具有不相等的雜湊碼,違反了hashCode的約定。因此,put方法把電話號碼物件存放在一個雜湊桶(hash bucket)中,get方法卻在另一個雜湊桶中查詢這個電話號碼。即使這兩個例項正好被放到同一個雜湊桶中,get方法也必定會返回null,因為HashMap有一項優化,可以將與每個項相關聯的雜湊碼快取起來,如果雜湊碼不匹配,也不必檢驗物件的等同性。

    1. 把某個非零的常量值,比如說17,儲存在一個名為result的int型別的變數中。
    2. 對於物件中每個關鍵域f(指equals方法中涉及的每個域),完成以下步驟:
      a. 為該域計算int型別的雜湊碼c:
      i. 如果該域是boolean型別,則計算(f ? 1:0)。
      ii. 如果該域是byte、char、short或者int型別,則計算(int)f。
      iii. 如果該域是long型別,則計算(int)(f ^ (f>>>32))。
      iv. 如果該域是float型別,則計算Float.floatToIntBits(f)。
      v. 如果該域是double型別,則計算Double.doubleToLongBits(f),然後按照步驟2.a.iii,為得到的long型別值計算雜湊值。
      vi. 如果該域是一個物件引用,並且該類的equals方法通過遞迴地呼叫equals的方式來比較這個域,則同樣為這個域遞迴地呼叫hashCode。如果需要更復雜的比較,則為這個域計算一個“正規化(canonical representation)”,然後針對這個正規化呼叫hashCode。如果這個域的值為null,則返回0(或者其他某個常數,但通常是0)。
      vii. 如果該域是一個數組,則要把每一個元素當做單獨的域來處理。也就是說,遞迴地應用上述規則,對每個重要的元素計算一個雜湊碼,然後根據步驟b.2中的做法把這些雜湊值組合起來。如果陣列域中的每個元素都很重要,可以利用發行版本1.5中增加的其中一個Arrays.hashCode方法。
      b. 按照下面的公式,把步驟2.a中計算得到的雜湊碼c合併到result中:
      result=31×result+c;
      返回result。
  5. 必須排除equals比較計算中沒有用到的任何域,否則很有可能違反hashCode約定的第二條。

  6. 步驟2.b中的乘法部分使得雜湊值依賴於域的順序,如果一個類包含多個相似的域,這樣的乘法運算就會產生一個更好的雜湊函式。例如,如果String雜湊函式省略了這個乘法部分,那麼只是字母順序不同的所有字串都會有相同的雜湊碼。之所以選擇31,是因為它是一個奇素數。如果乘數是偶數,並且乘法溢位的話,資訊就會丟失,因為與2相乘等價於移位運算。31有個很好的特性,即用移位和減法來代替乘法,可以得到更好的效能:31 * i == (i << 5) - i。現代的VM可以自動完成這種優化。
  7. 如果一個類是不可變的,並且計算雜湊碼的開銷也比較大,就應該考慮把雜湊碼快取在物件內部,而不是每次請求的時候都重新計算雜湊碼。如果你覺得這種型別的大多數物件會被用作雜湊鍵(hash keys),就應該在建立例項的時候計算雜湊碼。否則,可以選擇“延遲初始化”雜湊碼,一直到hashCode被第一次呼叫的時候才初始化。
  8. 不要試圖從雜湊碼計算中排除掉一個物件的關鍵部分來提高效能。雖然這樣得到的雜湊函式執行起來可能更快,但是它的效果不見得會好,可能會導致散列表慢到根本無法使用。特別是在實踐中,雜湊函式可能面臨大量的例項,在你選擇忽略的區域之中,這些例項仍然區別非常大。如果是這樣,雜湊函式就會把所有這些例項對映到極少數的雜湊碼上,基於雜湊的集合將會顯示出平方級的效能指標。這不僅僅是個理論問題。在Java 1.2發行版本之前實現的String雜湊函式至多隻檢查16個字元,從第一個字元開始,在整個字串中均勻選取。對於像URL這種層次狀名字的大型集合,該雜湊函式正好表現出了這裡所提到的病態行為。
  9. java.lang.String的hashCode方法:
public int hashCode() {
    int h = hash; //hash: cache the hash code for the string
    if (h==0) { //h==0說明是第一次計算雜湊值,hash域還沒有快取
        int off = offset; // the offset is the first index of the storage that is used.
        char val[] = value; // the value is used for character storage.
        int len = count;
        for (int i=0;i<len;i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;

第10條:始終要覆蓋toString

  1. 在實現toString的時候,必須要做出一個很重要的決定:是否在文件中指定返回值的格式。
  2. 這種表示法可以用於輸入和輸出,以及用在永久的適合於人類閱讀的資料物件中,例如XML文件。
  3. 無論你是否決定指定格式,都應該在文件中明確地表明你的意圖。如果你要指定格式,則應該嚴格地這樣去做。

第11條:謹慎地覆蓋clone

  1. 其主要的缺陷在於,它缺少一個clone方法,Object的clone方法是受保護的。如果不借助於反射(reflection),就不能僅僅因為一個物件實現了Cloneable,就可以呼叫clone方法。即使是反射呼叫也可能會失敗,因為不能保證該物件一定具有可訪問的clone方法。
  2. 既然Cloneable並沒有包含任何方法,那麼它到底有什麼作用呢?它決定了Object中受保護的clone方法實現的行為:如果一個類實現了Cloneable,Object的clone方法就返回該物件的逐域拷貝,否則就會丟擲CloneNotSupportedException異常。這是介面的一種極端非典型的用法,也不值得效仿。通常情況下,實現介面是為了表明類可以為它的客戶做些什麼。然而,對於Cloneable介面,它改變了超類中受保護的方法的行為。
  3. 如果它的clone方法僅僅返回super.clone(),這樣得到的Stack例項,在其size域中具有正確的值,但是它的elements域將引用與原始Stack例項相同的陣列。
  4. 為了使Stack類中的clone方法正常地工作,它必須要拷貝棧的內部資訊。最容易的做法是,在elements陣列中遞迴地呼叫clone:
@Override public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

雖然被克隆物件有它自己的雜湊桶陣列,但是,這個陣列引用的連結串列與原始物件是一樣的,從而很容易引起克隆物件和原始物件中不確定的行為。為了修正這個問題,必須單獨地拷貝並組成每個桶的連結串列。下面是一種常見的做法:

public class HashTable implements Cloneable {
    private Entry[] buckets = ...;

    private static class Entry {
        final Object key;
        Object value;
        Entry next;
        Entry(Object key, Object value, Entry next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
        // Iteratively copy the linked list headed by this Entry
        Entry deepCopy() {
            Entry result = new Entry(key, value, next);
            for (Entry p = result; p.next != null; p = p.next)
                p.next = new Entry(p.next.key, p.next.value, p.next.next);
            return result;
        }
    }
    @Override public HashTable clone() {
        try {
            HashTable result = (HashTable) super.clone();
            result.buckets = new Entry[buckets.length];
            for(int i=0;i<buckets.length;i++) 
                if (buckets[i] != null)
                    result.buckets[i] = buckets[i].deepCopy();
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
    ... // Remainder omitted
}

如果專門為了繼承而設計的類覆蓋了clone方法,覆蓋版本的clone方法就應該模擬Object.clone的行為:它應該被宣告為protected、丟擲CloneNotSupportedException異常,並且該類不應該實現Cloneable介面。這樣做可以使子類具有實現或不實現Cloneable介面的自由,就彷彿它們直接擴充套件了Object一樣。

簡而言之,所有實現了Cloneable介面的類都應該用一個公有的方法覆蓋clone。此公有方法首先呼叫super.clone,然後修正任何需要修正的域。一般情況下,這意味著要拷貝任何包含內部“深層結構”的可變物件,並用指向新物件的引用代替原來指向這些物件的引用。

拷貝構造器的做法,以及靜態工廠方法的變形,都比Cloneable/clone方法具有更多的優勢:它們不依賴於某一種很有風險的、語言之外的物件建立機制;它們不要求遵守尚未制定好文件的規範;它們不會與final域的正常使用發生衝突;它們不會丟擲不必要的受檢查異常(checked exception);它們不需要進行型別轉換。雖然你不可能把拷貝構造器或者靜態工廠放到介面中,但是由於Cloneable介面缺少一個公有的clone方法,所以它也沒有提供一個介面該有的功能。因此,使用拷貝構造器或者拷貝工廠來代替clone方法時,並沒有放棄介面的功能特性。

第12條:考慮實現Comparable介面

  1. 一旦類實現了Comparable介面,它就可以跟許多泛型演算法(generic algorithm)以及依賴於該介面的集合實現(collection implementation)進行協作。你付出很小的努力就可以獲得非常強大的功能。事實上,Java平臺類庫中的所有值類(value classes)都實現了Comparable介面。
  2. 強烈建議(x.compareTo(y) == 0) == (x.equals(y)),但這並非絕對必要。一般來說,任何實現了Comparable介面的類,若違反了這個條件,都應該明確予以說明。推薦使用這樣的說法:“注意:該類具有內在的排序功能,但是與equals不一致。”
  3. 與equals不同的是,在跨越不同類的時候,compareTo可以不做比較:如果兩個被比較的物件引用不同類的物件,compareTo可以丟擲ClassCastException異常。通常,這正是compareTo在這種情況下應該做的事情,如果類設定了正確的引數,這也正是它所要做的事情。
  4. 就好像違法了hashCode約定的類會破壞其他依賴於雜湊做法的類一樣,違反compareTo約定的類也會破壞其他依賴於比較關係的類。依賴於比較關係的類包括有序集合類TreeSet和TreeMap,以及工具類Collections和Arrays,它們內部包含有搜尋和排序演算法。
  5. 因此,下面的告誡也同樣適用:無法在用新的值元件擴充套件可例項化的類時,同時保持compareTo約定,除非願意放棄面向物件的抽象優勢。針對equals的權宜之計也同樣適用於compareTo方法。如果你想為一個實現了Comparable介面的類增加值元件,請不要擴充套件這個類;而是要編寫一個不相關的類,其中包含第一個類的一個例項。然後提供一個“檢視(view)”方法返回這個例項。這樣既可以讓你自由地在第二個類上實現compareTo方法,同時也允許它的客戶端在必要的時候,把第二個類的例項視同第一個類的例項。
  6. 例如,考慮BigDecimal類,它的compareTo方法與equals不一致。如果你建立了一個HashSet例項,並且新增new BigDecimal (“1.0”)和new BigDecimal (“1.00”),這個集合就將包含兩個元素,因為新增到集合中的兩個BigDecimal例項,通過equals方法來比較時是不相等的。然而,如果你使用TreeSet而不是HashSet來執行同樣的過程,集合中將只包含一個元素,因為這兩個BigDecimal例項在通過compareTo方法進行比較時是相等的。
  7. 因為Comparable介面是引數化的,而且comparable方法是靜態的型別(意思是compareTo方法的引數型別是一定的,就是實現Comparable介面的時候指定的引數型別。而equals方法的引數型別是Object,所以要進行額外的型別檢查,instanceof。),因此不必進行型別檢查,也不必對它的引數進行型別轉換。如果引數的型別不合適,這個呼叫甚至無法編譯。
  8. 比較整數型別基本型別的域,可以使用關係操作符<和>。例如,浮點域用Double.compare或者Float.compare,而不用關係操作符,當應用到浮點值時,它們沒有遵守compareTo的通用約定。對於陣列域,則要把這些指導原則應用到每個元素上。
  9. 如果一個類有多個關鍵域,那麼,按什麼樣的順序來比較這些域是非常關鍵的。你必須從最關鍵的域開始,逐步進行到所有的重要域。如果某個域的比較產生了非零的結果(零代表相等),則整個比較操作結束,並返回該結果。
  10. 這項技巧有時不能正常工作的原因在於,一個有符號的32位的整數還沒有大到足以表達任意兩個32位整數的差。如果i是一個很大的正整數(int型別),而j是一個很大的負整數(int型別),那麼(i-j)將會溢位,並返回一個負值。

第4章 類和介面

第13條:使類和成員的可訪問性最小化

  1. 資訊隱藏之所以非常重要有許多原因,其中大多數理由都源於這樣一個事實:它可以有效地解除組成系統的各模組之間的耦合關係,使得這些模組可以獨立地開發、測試、優化、使用、理解和修改。
  2. 對於頂層的(非巢狀的)類和介面,只有兩種可能的訪問級別:包級私有的(package-private)和公有的(public)。如果類或者介面能夠被做成包級私有的,它就應該被做成包級私有。通過把類或者介面做成包級私有,它實際上成了這個包的實現的一部分,而不是該包匯出的API的一部分,在以後的發行版本中,可以對它進行修改、替換,或者刪除,而無需擔心會影響到現有的客戶端程式。
  3. 如果一個包級私有的頂層類(或者介面)只是在某一個類的內部被用到,就應該考慮使它成為唯一使用它的那個類的私有巢狀類。然而,降低不必要公有類的可訪問性,比降低包級私有的頂層類的更重要得多:因為公有類是包的API的一部分,而包級私有的頂層類則已經是這個包的實現的一部分。
  4. 包級私有的(package-private)——宣告該成員的包內部的任何類都可以訪問這個成員。從技術上講,它被稱為“預設(default)訪問級別”,如果沒有為成員指定訪問修飾符,就採用這個訪問級別。
  5. 其實,只有當同一個包內的另一個類真正需要訪問一個成員的時候,你才應該刪除private修飾符,使該成員變成包級私有的。如果你發現自己經常要做這樣的事情,就應該重新檢查你的系統設計,看看是否另一種分解方案所得到的類,與其他類之間的耦合度會更小。然而,如果這個類實現了Serializable介面,這些域就有可能被“洩漏(leak)”到匯出的API中。
  6. 受保護的成員是類的匯出的API的一部分,必須永遠得到支援。匯出的類的受保護成員也代表了該類對於某個實現細節的公開承諾。受保護的成員應該儘量少用。
  7. 如果方法覆蓋了超類中的一個方法,子類中的訪問級別就不允許低於超類中的訪問級別。這樣可以確保任何可使用超類的例項的地方也都可以使用子類的例項。
  8. 為了測試而將一個公有類的私有成員變成包級私有的,這還可以接受,但是要將訪問級別提高到超過它,這就無法接受了。換句話說,不能為了測試,而將類、介面或者成員變成包的匯出的API的一部分。幸運的是,也沒有必要這麼做,因為可以讓測試作為被測試的包的一部分來執行,從而能夠訪問它的包級私有的元素。
  9. 例項域決不能是公有的。如果域是非final的,或者是一個指向可變物件的final引用,那麼一旦使這個域成為公有的,就放棄了對儲存在這個域中的值進行限制的能力;這意味著,你也放棄了強制這個域不可變的能力。因此,包含公有可變域的類並不是執行緒安全的。即使域是final的,並且引用不可變的物件,當把這個域變成公有的時候,也就放棄了“切換到一種新的內部資料表示法”的靈活性。
  10. 假設常量構成了類提供的整個抽象中的一部分,可以通過公有的靜態final域來暴露這些常量。按慣例,這種域的名稱由大寫字母組成,單詞之間用下劃線隔開。很重要的一點是,這些域要麼包含基本型別的值,要麼包含指向不可變物件的引用。
  11. 注意,長度非零的陣列總是可變的,所以,類具有公有的靜態final陣列域,或者返回這種域的訪問方法,這幾乎總是錯誤的。
  12. 要注意,許多IDE會產生返回指向私有陣列域的引用的訪問方法,這樣就會產生這個問題。修正這個問題有兩種方法。可以使公有陣列變成私有的,並增加一個公有的不可變列表:
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = 
    Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

第14條:在公有類中使用訪問方法而非公有域

  1. Java平臺類庫中有幾個類違反了“公有類不應該直接暴露資料域”的告誡。顯著的例子包括java.awt包中的Point和Dimension類。它們是不值得效仿的例子,相反,這些類應該被當作反面警告示例。正如第55條中所講述的,決定暴露Dimension類的內部資料造成了嚴重的效能問題,而且這個問題至今依然存在。
  2. 總之,公有類永遠都不應該暴露可變的域。雖然還是有問題,但是讓公有類暴露不可變的域其危害比較小。但是,有時候會需要用包級私有的或者私有的巢狀類來暴露域,無論這個類是可變的還是不可變的。

第15條:使可變性最小化

  1. Java平臺類庫中包含許多不可變的類,其中有String、基本型別的包裝類、BigInteger和BigDecimal。
  2. 保證類不會被擴充套件。這樣可以防止粗心或者惡意的子類假裝物件的狀態已經改變,而破壞該類的不可變行為。為了防止子類化,一般做法是使這個類成為final的,但是後面我們還會討論到其他的做法。
  3. 確保對於任何可變元件的互斥訪問。如果類具有指向可變物件的域,則必須確保該類的客戶端無法獲得指向這些物件的引用。並且,永遠不要用客戶端提供的物件引用來初始化這樣的域,也不要從任何訪問方法(accessor)中返回該物件的引用。在構造器、訪問方法和readObject方法中請使用保護性拷貝(defensive copy)技術。
  4. 注意這些算術運算是如何建立並返回新的Complex例項,而不是修改這個例項。大多數重要的不可變類都使用了這種模式。它被稱為函式的(functional)做法,因為這些方法返回了一個函式的結果,這些函式對運算元進行運算但並不修改它。與之相對應的更常見的是過程的(procedural)或者命令式的(imperative)做法,使用這些方式時,將一個過程作用在它們的運算元上,會導致它的狀態發生改變。
  5. “不可變物件可以被自由地共享”導致的結果是,永遠也不需要進行保護性拷貝。實際上,你根本無需做任何拷貝,因為這些拷貝始終等於原始的物件。因此,你不需要,也不應該為不可變的類提供clone方法或者拷貝構造器(copy constructor)。這一點在Java平臺的早期並不好理解,所以String類仍然具有拷貝構造器,但是應該儘量少用它。
  6. 如果能夠精確地預測出客戶端將要在不可變的類上執行哪些複雜的多階段操作,這種包級私有的可變配套類的方法就可以工作得很好。如果無法預測,最好的辦法是提供一個公有的可變配套類。在Java平臺類庫中,這種方法的主要例子是String類,它的可變配套類是StringBuilder(和基本上已經廢棄的StringBuffer)。可以這樣認為,在特定的環境下,相對於BigInteger而言,BitSet同樣扮演了可變配套類的角色。
  7. 例如,假設你希望提供一種“基於極座標建立複數”的方式。如果使用構造器來實現這樣的功能,可能會使得這個類很零亂,因為這樣的構造器與已用的構造器Complex(double, double)具有相同的簽名。通過靜態工廠,這很容易做到。只需新增第二個靜態工廠,並且工廠的名字清楚地表明瞭它的功能即可:
public static Complex valueOfPolar(double r, double theta) {
    return new Complex(r * Math.cos(theta), r * Math.sin(theta));
}

當BigInteger和BigDecimal剛被編寫出來的時候,對於“不可變的類必須為final的”還沒有得到廣泛地理解,所以它們的所有方法都有可能會被覆蓋。遺憾的是,為了保持向後相容,這個問題一直無法得以修正。如果你編寫一個類,它的安全性依賴於(來自不可信客戶端的)BigInteger或者BigDecimal引數的不可變性,就必須進行檢查,以確定這個引數是否為“真正的”的BigInteger或者BigDecimal,而不是不可信任子類的例項。如果是後者的話,就必須在假設它可能是可變的前提下對它進行保護性拷貝:

public static BigInteger safeInstance(BigInteger val) {
    if (val.getClass() != BigInteger.class)
        return new BigInteger(val.toByteArray());
    return val;
}

總之,堅決不要為每個get方法編寫一個相應的set方法。除非有很好的理由要讓類稱為可變的類,否則就應該是不可變的。你應該總是使一些小的值物件,比如PhoneNumber和Complex,成為不可變的(在Java平臺類庫中,有幾個類如java.util.Date和java.awt.Point,它們本應該是不可變的,但實際上卻不是)。你也應該認真考慮把一些較大的值物件做成不可變的,例如String和BigInteger。只有當你確認有必要實現令人滿意的效能時,才應該為不可變的類提供公有的可變配套類。

可以通過TimerTask類來說明這些原則。它是可變的,但是它的狀態空間被有意地設計得非常小。你可以建立一個例項,對它進行排程使它執行起來,也可以隨意地取消它。一旦一個定時器任務(timer task)已經完成,或者已經被取消,就不可能再對它重新排程。

第16條:複合優先於繼承

  1. 在包的內部使用繼承是非常安全的,在那裡,子類和超類的實現都處在同一個程式設計師的控制之下。對於專門為了繼承而設計、並且具有很好的文件說明的類來說,使用繼承也是非常安全的。然而,對普通的具體類(concrete class)進行跨越包邊界的繼承,則是非常危險的。
  2. 我們只要去掉被覆蓋的addAll方法,就可以“修正”這個子類。雖然這樣得到的類可以正常工作,但是,它的功能正確性則需要依賴於這樣的事實:HashSet的addAll方法是在它的add方法上實現的。這種“自用性(self-use)”是實現細節,不是承諾,不能保證在Java平臺的所有實現中都保持不變,不能保證隨著發行版本的不同而不發生變化。因此,這樣得到的InstrumentedHashSet類將是非常脆弱的。
  3. 上面這兩個問題都來源於覆蓋(overriding)動作。如果在擴充套件一個類的時候,僅僅是增加新的方法,而不是覆蓋現有的方法,你可能會認為這是安全的。雖然這種擴充套件方式比較安全一些,但是也並非完全沒有風險。如果超類在後續的發行版本中獲得了一個新的方法,並且不幸的是,你給子類提供了一個簽名相同但返回型別不同的方法,那麼這樣的子類將無法通過編譯。如果給子類提供的方法帶有與新的超類方法完全相同的簽名和返回型別,實際上就覆蓋了超類中的方法,因此又回到上述的兩個問題上去了。
  4. 不用擴充套件現有的類,而是在新的類中增加