《Effective Java》讀書筆記

引言

1 代碼應該被重用 ,而不是被拷貝

2 錯誤應該儘早檢測出來,最好是在編譯時刻

3 接口、類、數組是引用類型(對象), 基本類型不是


第二章 創建和銷燬對象

1 考慮用靜態工廠方法代替構造器。

優勢:
1 有名稱(多個 相同簽名 的構造器)
2 不必每次調用它們都創建一個新對象。(可控)
3 可以返回原返回類型的任何子類型的對象。(靈活,可返回一個接口類型,強迫客戶端面向接口編程)
4 靜態工廠方法可以利用 類型推導 簡化 參數化類型實例 的創建。
缺點:
1 類不含public protected的構造器,不能被子類化。
2 Javadoc API 文檔中,沒有特殊標明 靜態工廠方法。 不方便查閱。
一些慣用名稱:
1. valueOf,實際上是類型轉換方法
2. of,valueOf的簡潔替代。
3. getInstance。
4. newInstance。與3相比,確保返回的每個實例都與其他的所有實例不同。
5. getXXXX。
6. newXXXX。
總結:
和構造器比,各有好處。優先使用靜態工廠。

(重用對象的好處 可以用 == 替代 equals,性能更好。靜態工廠參考Boolean.valueOf().)

2 遇到多個構造器參數時,要考慮用Builder

靜態工廠和構造器有個共同的侷限性:它們都不能很好地擴展到大量可選參數
Builder模式,既能保證像重疊構造器模式那樣的安全性,也能保證像JavaBeans模式那麼好的可讀性。還可以有多個 可變參數。

Builder模式構建時,被構建類的field一般都是final。

如類的構造器或者靜態工廠中具有多個參數,特別大多數參數時可選的時候,使用Builder模式。
(最好一開始就用Builder,否則兼容以前的構造函數和靜態工廠很煩。)

3 用私有構造器或者枚舉類型強化Singleton屬性

public enum EnumSingleton {
    INSTANCE;


    EnumSingleton() {
        System.out.println("我被創建了");
    }

    public void metho1() {
        System.out.println("單例的方法");
    }
}

4 通過私有構造器強化不可實例化的能力

副作用是 使得一個類不能被子類化。可用作常量定義工具類。

5 避免創建不必要的對象

Map的keySet 每次返回的是同一個Set實例。
優先使用基本類型而不是裝箱基本類型,當心無意識的自動裝箱。
並不代表我們要儘可能的避免創建對象,對於小對象而言,它的創建和回收動作是非常廉價的。
反之,通過維護自己的對象池來避免創建對象並不是一種好的做法。
除非池中的對象是非常重量級的。

6 消除過期的對象引用

避免內存泄漏。
只要類是自己管理內存(數組存儲對象引用,及時置位null),程序員就應該警惕內存泄漏問題。

7 避免使用終結方法

終結方法finalizer通常是不可預測的,也是很危險的,一般情況下是不必要的。
它不保證何時執行,是否執行。且會帶來性能損失。

如果需要,提供一個顯式的終止方法。類似InputStream。
顯式的終止方法通常與try-finally結合起來使用,以確保及時終止。在finally子句內調用顯式的終止方法,可以保證即使在使用對象的時候,有異常拋出,該終止方法也會執行。

終結方法的合法用途:
1 使用者忘記調用顯式終止方法,終結方法可以充當 “安全網”,遲一點釋放比不釋放要好。(希望儘可能不要這樣) 。如果終結方法發現資源還未被終止,應該在日誌中記錄一條警告。因爲這是客戶端的一個bug。但是要考慮這種額外的保護是否值得。
四個示例:FileInputStream、FileOutputStream、Timer、Connection。

2 釋放native對象的資源。

還有一點,終結方法鏈不會自動執行。如果類有終結方法,並且子類覆蓋了終結方法,要手動調用超類的終結方法。應該在try中終結子類,在finally中調用超類的finalize().

    protected void finalize() throws Throwable {
        try {
            //finalize自己
        } finally {
            super.finalize();
        }
    }

爲了防範子類沒有手工調用超類的終結方法。可以採用匿名內部類實現一個終結方法守衛者

    //終結方法守衛者
    private final Object finalizerGuardian = new Object() {
        @Override
        protected void finalize() throws Throwable {
            //在這裏調用外圍實例的finalize
            FinalizeTest.this.finalize();
        }
    };

第三章 對於所有對象都通用的方法

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

值類(value class)”僅僅是一個表示值的類,例如Integer或Date。需要覆蓋equals()
有一種值類不需要覆蓋,即第一條確保下的“每個值至多隻存在一個對象”的類。枚舉類型就屬於這種類。
equals()方法實現了等價關係。

  1. 自反性。對於任何非null的引用值x,x.equals(x)必須返回true.(對象必須等於其自身)
  2. 對稱性。對於任何非null的引用值x和y,當且僅當y.equals(x)返回true時,x.equals(y)必須返回true。(警惕子類父類不同的equals實現時)
  3. 傳遞性。對於任何非null的引用只x、y和z,如果x.equals(y)返回true,並且y.equals(z)也返回true,那麼x.equeals(z)也必須返回true。(優先使用組合而不是繼承,可以避免違反傳遞性 對稱性,否則沒有完美解決方案。 只要超類不能直接創建實例,則不會違反原則)
  4. 一致性。對於任何非null的引用值x和y,只要equals的比較操作在對象中所用的信息沒有被修改,多次調用x.equals(y)返回值一致。()
  5. 非空性。對於任何非null的引用值x,x.equals(null)必須返回false。(不用重複判空,只需要用instance 即可)

實現高質量equals方法的訣竅:

  1. 使用==操作符 檢查”參數是否爲這個對象的引用”。如果是,返回true。這是一種性能優化,適用於比較操作昂貴的情況。
  2. 使用instanceof操作符檢查“參數是否爲正確的類型”。如果不是,返回false。
  3. 把參數強轉成正確的類型。
  4. 對於該類中的每個“關鍵”域進行匹配比較。(float使用Float.compare()比較,double使用Double.compare()比較。 有些filed可能爲null,且合法。 可以用 (field ==null? o.field==null: field.equals(0.field)) 。 如果field和o.field通常是相同的對象引用,那麼下面的做法更快一些: ( field == o.field || field!=null && field.equlas(0.field))
  5. 編寫完equals方法之後,應該去測試 對稱性、傳遞性、一致性。
  6. 覆蓋equals時,同時覆蓋hashCode。
  7. 不要將equals()方法聲明中Object對象替換爲其他的類型。(public boolean equals(MyClass o)){})
    //一個示例
    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (!(o instanceof EnumSingletonTest)) {
            return false;
        }
        EnumSingletonTest data = (EnumSingletonTest) o;
        return data.xxx == xxx
                && dta.bbb == bbb;
    }

9 覆蓋equals時 總要覆蓋 hashCode

不覆蓋該方法,會導致該類無法配合HashMap、HashSet和Hashtable一起使用。
因爲equals()相等的話,hashCode()一定要相等。
一個好的散列函數通常傾向於“爲不相等的對象產生不相等的散列碼”。

10 始終要覆蓋 toString

11 謹慎地覆蓋clone

一般,x.clone() != x ,x.clone().getClass() == x.getClass(), x.clone().equals(x) (但也不強求)
實際上,clone方法就是另一個構造器;你必須確保它不會傷害到原始的對象,並確保正確地創建被克隆對象中的約束條件。
更好的辦法是類似集合類那樣,提供 轉換構造器 和 轉換工廠。且轉換構造器和工廠可以帶一個接口類型的參數。 例如你有一個HashSet,想得到TreeSet,clone方法無法提供這樣的功能,但是用轉換構造器很容易實現。 new TreeSet(s)。
專家級程序員不建議用clone。

12 考慮實現 Comparable 接口

實現compareTo()方法 。它的約定和equals方法相似。(自反、傳遞、對稱),但它不用進行類型檢查。比較時,順序很重要,先比較重要的,如果產生了非零的結果,則結束比較。

小於返回負整數,等於返回0,大於返回正整數。
牆裂建議,(x.compareTo(y)==0) == (x.equals(y))

用於TreeSet TreeMap,以及工具類Collections、Arrays,內部包含搜索和排序算法。

如果一個域沒有實現Comparable接口,或者需要使用一個非標準的排序關係。可以用Comparator來代替,或者使用已有的Comparator。例如:

public final class A implements Comparable<A>{
    private String s ;
    public int compareto(A a){
        return String.CASE_INSENSITIVE_ORDER_.compare(s,a.s);
    }
}

有些時候可以直接用 this.s - other.s 來作爲返回值,但是要確保 最小和最大的可能域值之差小於或等於INTEGER.MAX_VALUE。否則計算結果超過int型溢出時, 結果將相反。

第四章 類和接口

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

隱藏內部數據和實現細節。
儘可能的使每個類或者成員不被外界訪問
對於頂層的(非嵌套的)類和接口,只有兩種可能的訪問級別:包級私有(缺省) 和 公有的
如果一個包級私有的頂層類活接口 只是在某一個類的內部被用到,就應該考慮使他成爲那個類的私有嵌套類。
然而,降低不必要的共有類的可訪問性,比上一點更重要。

對於成員,依次有:private、包級私有(default)、protected(只有子類和包內可以訪問)、public
protected應該少用,它和public都作爲公開API的一部分。

如果方法覆蓋了超類中的一個方法,子類中的訪問級別就不允許低於超類中的訪問級別。這樣可以確保任何可使用超類的地方也可以使用子類。

實例域絕不能是公有的。如果域是非final的,則更不應該。如果final域包含可變對象的引用,它引用的對象是可以被修改的,這會導致災難性的後果。

長度非零的數組總是可變的。所以,類具有公有的靜態final數組域,或者返回這種域的訪問方法,幾乎總是錯誤的。因爲客戶端能修改數組中的內容,這是一個安全漏洞。
修正這個問題有兩種方法:

  1. 訪問權限設爲私有,同時增加一個公有的不可變列表
private static final A[] PRIVATE_VALUES = {.....};
public static final List<A> VALUES = 
    Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES ));
  1. 訪問權限設爲私有,增加一個公有方法,返回私有數組的一個備份
private static final A[] PRIVATE_VALUES = {.....};
public static final A[] values(){
    return PRIVATE_VALUES .clone();
}

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

如果類可以在它所在的包的外部進行訪問,就提供訪問方法。
如果類是包級私有的,或者是私有的嵌套類,直接暴漏它的數據域並沒有本質的錯誤。
公有類永遠都不應該暴露可變的域。偶爾會暴漏不可變域
有時候會暴露包級私有的或者私有的嵌套類的域,無論這個類可變還是不可變。

15 使可變性最小化

不可變類只是其實例不能被修改的類
每個實例中包含的所有信息都必須在創建該實例的時候就提供,並在對象的整個生命週期內固定不變。例如String、基本類型的包裝類、BigInteger、BigDecimal。

不可變類遵循下面五條規則:

  1. 不要提供任何會修改對象狀態的方法(改變對象屬性的方法)。
  2. 保證類不會被擴展。(final或者 私有、包級私有構造器,提供公有靜態工廠)
  3. 使所有的域都是final的。
  4. 使所有的域都成爲私有的。
  5. 確保對於任何可變組件的互斥訪問。確保客戶端無法獲取指向 可變對象的域 的引用。並且永遠不要用客戶端提供的對象引用來初始化這樣的域,也不要從任何訪問方法中返回該對象的引用。

大多數不可變類在修改域時,都是通過返回一個新的對象做的。只對操作數進行運算但不修改它
這被成爲 函數的 functional做法,與之對應的是過程的procedural 或者 命令式的 imperative,這些方式會導致狀態改變。
例如

public class ImmutableExampleTest {
    private final int value;

    public ImmutableExampleTest(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public ImmutableExampleTest add(ImmutableExampleTest b) {
        return new ImmutableExampleTest(value + b.value);
    }
}

不可變對象可以只有一種狀態,即被創建時的狀態。
不可變對象本質上是線程安全的,它們不要求同步。
不可變對象可以被自由地共享。鼓勵客戶端儘可能地重用現有的實例。可以對頻繁用到的值,提供公有的靜態final常量。
不可變對象永遠不需要保護性拷貝不應該提供clone方法或者拷貝構造器
不僅可以共享不可變對象,甚至也可以共享它們的內部信息。例如BigInteger類的negate()方法,返回的BigInteger對象中的數組指向原始實例中的同一個內部數組。
不可變對象爲其他對象提供了大量的構件(building blocks)
不強求所有的域都是final的,但必須保證 :沒有一個方法能夠對對象的狀態產生 外部可見 的改變。可以用非final域去緩存一些昂貴開銷的計算的結果(配合lazy initialization)。
如果不可變類實現了Serializable接口,並且包含一個或多個指向可變對象的域,需要特殊處理。

不可變對象真正唯一的缺點是,對於每個不同的值都需要一個單獨的對象。
如果你執行一個多步驟的操作,並且每個步驟都會產生一個新的對象,除了最後的結果之外其他的對象最終都會被丟棄,此時性能問題就會暴露出來。兩種解決辦法:

  1. 先猜測一下經常用到哪些多步驟操作,然後將它們作爲基本類型提供。
  2. 如果無法預測,最好的辦法就是提供一個公有的可變配套類。String類是不可變類,它的可變配套類是StringBuilder。BigInteger對應BigSet。

總結:
儘量將小的值對象,成爲不可變的。認真考慮將較大的值對象做成不可變的,例如String、BigInteger。只有當性能確實需要優化時,才爲不可變的類提供公有的可變配套類。
如果類不能被做成不可變的,仍然應該儘可能地限制它的可變性。除非有令人信服的理由,否則每個域都是final的
不應該提供“重新初始化”方法,它會增加複雜性。

16 複合優先於繼承(組合大於繼承)

繼承是實現代碼重用的有力手段,但它並非永遠是最佳工具。
包內部使用繼承非常安全,因爲它屬於同一個程序員的控制之下
對於專門爲繼承而設計、並具有很好的文檔說明的類,繼承也非常安全
(這裏的繼承代表 實現繼承 ,指的是一個類繼承另一個類。 對於類實現接口、或者接口繼承接口都不屬於這種情況)

與方法調用不同的是,繼承打破了封裝性。
子類依賴於其超類中特定功能的實現細節。超類的實現有可能隨着發行版本的不同而有所變化,如果發生了變化,子類可能遭到破壞,即使子類的代碼完全沒有改變。

只有當子類和超類之間確實存在 “is-a”關係時,才應該用繼承。
繼承機制會把超類API中的所有缺陷傳播到子類中,而複合(裝飾、代理)則允許設計新的API 來隱藏這些缺陷。

17 要麼爲繼承而設計,並提供文檔說明,要麼就禁止繼承。

構造器決不能調用可被覆蓋的方法。不管是直接的還是間接的調用。
如果類是爲了繼承而被設計的,無論實現CloneableSerializable都不是好主意。因爲它把一些實質性的負擔轉嫁給擴展這個類的程序員的身上。因爲clone()readObject()方法行爲上類似於構造器,所以也不可以調用可覆蓋的方法,不管以直接還是間接的形式。

如果必須從這種類繼承,一種辦法是 確保這個類永遠不會調用它的任何可覆蓋的方法。

18 接口優於抽象類

現有的類可以很容易被更新,以實現新的接口。
接口是定義mixin(混合類型)的理想選擇。
接口允許我們構造非層次(豎向)結構的類型框架。

雖然接口不允許包含方法的實現。但通過對每個重要接口都提供一個抽象的骨架實現(skeletal implementation)類,把接口和抽象類的優點結合起來。接口的作用仍然是定義類型,但是骨架實現類接管了所有與接口實現相關的工作。
按照慣例,骨架實現類稱爲AbstractInterface,例如AbstractCollectionAbstractSet等。

骨架實現上有個小小的不同,就是簡單實現。AbstractMap.SimpleEntry就是個例子,簡單實現類似於骨架實現類,因爲它實現了接口,並且也是爲了繼承而設計的區別在於它不是抽象的:它是最簡單的可能的有效實現。你可以原封不動地使用,也可以看情況將它子類化。

抽象類比接口有一個明顯的優勢:抽象類的演變比接口的演變要容易的多
如果在後續的版本中,希望在抽象類中增加新的方法,始終可以增加具體方法,包含合理的默認實現即可。
一般來說,想在公有接口中增加方法,而不破壞實現這個接口的所有現有的類,這是不可能的。可以通過在爲接口增加新方法的同時,也爲骨架實現類增加同樣的新方法,來一定程度上減小破壞。

因此,一個接口一旦公開發行,再修改基本不可能。

總結:
接口通常是定義允許多個實現(多態)的最佳途徑。
如果你想要演變的容易性比功能、靈活性更重要,而且你接受抽象類的侷限性,就用抽象類。
接口->骨架實現類->簡單實現類。

19 接口只用於定義類型

除此之外,爲了任何其他的目的而定義接口是不恰當的。
說白了,定義接口是爲了接口的行爲。
有一種接口是常量接口,這是對接口的不良使用。因爲常量是實現細節,不應該泄漏出去。對類的用戶來說沒有價值,反而會干擾。而且如果非final類實現了常量接口,它的所有子類的命名空間也會被接口中的常量所“污染”。
定義常量應該用枚舉或者不可實例化的工具類。

20 類層次優於標籤類

想到了自己項目裏 羣聊創建模塊。 原本就是一個標籤類,改造成了類層次。
類層次好處:
簡單清晰,沒有樣板代碼,不受無關數據域的拖累,多個程序員可獨立擴展層次結構。而且類層次可以反映類型之間本質上的層次關係,有助於增強靈活性。

標籤類有很多缺點:

  1. 可讀性差
  2. 內存佔用增加因爲實例承擔着屬於其他風格的不相關的域。
  3. 域不能是final的。
    總之它過於冗長,容易出錯,效率低下。

其實標籤類是對類層次的一種簡單的效仿。

將標籤類轉化成類層次:

  1. 爲標籤類的每個方法都定義一個包含抽象方法的抽象類,這裏的每個方法的行爲都依賴於 標籤值。
  2. 如果有行爲不依賴於標籤值的方法,就將這些方法放在抽象類裏。
  3. 如果所有的方法都用到了某些域,則將域也放在抽象類裏。
  4. 爲每種原始標籤書寫相應的子類。

21 用函數對象表示策略

如果一個類,它只有一個方法,這個方法執行其他對象(通過參數傳入)上的操作。
這個類的實例實際上等同於一個指向該方法的指針。
這樣的實例稱之爲函數對象。也是一個具體策略類。
我們在設計具體的策略類時,還需要定義一個策略接口。

總結:函數指針的主要用途就是實現策略模式。
爲了在java中實現這個模式,要聲明一個接口來表示該策略,並且爲每個具體策略實現該接口。
當具體策略只使用一次時,通常用匿名內部類來做。
當重複使用時,通常實現爲某個類私有的靜態成員類,並通過 公有 靜態 final 域 被導出,域類型是策略接口類型。

22 優先考慮靜態成員類

嵌套類指定義在另一個類內部的類。目的是 只爲它的外圍類服務。
如果嵌套類將來可能用於其他的環境中,它就應該是頂層類。
嵌套類四種:

  1. 靜態成員類(優先)
  2. 非靜態成員類
  3. 匿名類(不能擁有靜態成員,如果只有一處創建的地方)
  4. 局部類(有多處創建的地方)

後三種被稱爲內部類

靜態成員類是最簡單的一種嵌套類,最好把它看作是普通的類,只是碰巧被聲明在另一個類的內部而已。
如果聲明內部類不要求訪問外圍實例,就要聲明稱 靜態成員類。
內部類會包含一個額外的指向外圍對象的引用,保存這份引用要消耗時間和空間,也會影響GC.

匿名類的常見用法:

  1. 動態創建函數對象。(21條)
  2. 創建過程對象,比如Runnable、Thread。
  3. 靜態工廠方法的內部。

第五章 泛型

出錯之後應該儘快發現,最好是在編譯時就發現。

23 請不要在新代碼中使用原生態類型、

List<T>對應的原生態類型就是List,如果使用原生態類型,就丟掉了泛型在安全性和表述性方面的所有優勢。它只是爲了兼容遺留代碼的。
無限制的通配符類型:List<?>,等於List,它們和List<Object>都不相等。
在類文字 中必須使用原生態類型List.class String[].class ini.class都合法,List<String>.class 和List<?>.class都不合法。

24 消除非受檢警告

儘可能地消除每一個非受檢警告。 如果消除所有警告,則可以確保代碼是類型安全的。意味着在運行時不會出現ClassCastException異常。

如果無法消除警告,同時可以證明引起警告的代碼是類型安全的,只有在這種情況之下,纔可以用一個@SuppressWarnings(“unchecked”)註解來禁止這條警告。
應該在儘可能小的範圍裏使用該註解。並添加註釋解釋爲什麼這麼做是安全的。

25 列表優先於數組

數組與泛型相比,有兩個重要的不同:
一 數組是協變的,泛型是不可變的。
如果Sub是Super的子類型,那麼Sub[]就是Super[]的子類型。
然而泛型則不是。對於任意兩個不同的類型Type1,Type2,List<Type1>和List<Type>不可能是子類型或父類型的關係。
實際上,這不是泛型的缺陷,反而是數組的缺陷:

        //fails at runtime!
        Object[] objectArray = new Long[1];
        objectArray[0] = "I don't fit in";//Throws ArrayStoreException

        //Won't compile
        List<Object> listObject = new ArrayList<Long>();//Incompatible types

上述代碼,無論哪種方法,都不能將String放入Long容器中。但是利用列表,可以在編譯時發現錯誤。

二 數組是具體化的 在運行時才知道自己的類型,泛型通過擦除,在運行時丟棄類型信息

一般來說,數組和泛型不能很好地混合使用。
如果混合使用出現了編譯時錯誤,應該用列表替代數組。

26 優先考慮泛型

不能直接創建 new T[16],可以通過創建 (T[])new Object[16];強轉。也可以在 取出數據時強轉。

有些泛型如ArrayList,必須在數組上實現。
爲了提升性能,其他泛型如HashMap也在數組上實現。

27 優先考慮泛型方法

靜態工具方法尤其適用於泛型化。
泛型方法一個顯著特徵,可以配合 類型推導,無需明確指定類型參數的值。

28 利用 有限制通配符 來提升API的靈活性

PECS表示,producer(取)-extends,consumer(存)-super。
還要記住所有的comparable和comparator都是消費者。

爲了獲得最大限度的靈活性,要在表示生產者或者消費者的輸入參數使用通配符類型
如果某個輸入參數 即是生產者又是消費者,則通配符類型對你沒有好處,需要嚴格的類型匹配。
不要用通配符作爲返回類型。

如果類的用戶必須考慮通配符類型,類的API或許就會出錯。

顯式的類型參數:

Set<Number> numbers = Union.<Number>union(integers,doubles);

一個複雜的例子:

public static <T extends Comparable<? super T>> T max(List<? extends T> list)

類型參數和通配符之間具有雙重性,許多方法都可以利用其中一個或者另一個進行聲明。
一般來說,如果類型參數只在方法聲明中出現一次,就可以用通配符取代它
在公共API中,推薦第二種。
例如:

    //類型參數和通配符之間具有雙重性,許多方法都可以利用其中一個或者另一個進行聲明
    //一般來說,如果類型參數只在方法聲明中出現一次,就可以用通配符取代它。
    public static <E> void swap1(List<E> list, int i, int j) {
    }

    public static void swap2(List<?> list, int i, int j) {

    }

    public static <E> void swap3(E list, int i, int j) {

    }
    //這個是錯誤的, 因爲E不是類型參數
/*    public static void swap4(? list, int i, int j) {

    }*/

29 優先考慮 類型安全 的 異構容器

類型安全:當你向它請求String時,它從來不會返回一個Integer給你
異構容器:不像普通的map,它的所有key都是不同類型的。

泛型最常用於集合。
當一個類的字面文字(Class<T>)被用在方法中,來傳達編譯時和運行時的類型信息時,就被稱作type token

註解API廣泛利用了有限制的類型令牌。 被註解的元素,本質上是個類型安全的異構容器。

第六章 枚舉和註解

1.5中增加了 兩個新的引用類型:枚舉類型、註解類型

30 用enum代替 int 常量。

枚舉易讀,提供了編譯時的類型安全
允許添加任意的方法和域(本質上是個類)。

如果枚舉具有普適性,應該成爲一個頂層類。如果用在特定頂層類中,應該成爲該頂層類的一個成員類。

但枚舉有小小的性能缺點,裝載和初始化枚舉時會有空間和時間的成本。

31 用實例域 代替序數(ordinal())

永遠不要根據枚舉的序數導出與它關聯的值,而是要將它保存在一個實例域中。
最好完全避免ordinal方法的使用。

32 用EnumSet 代替位域

位域有更多缺點,打印時翻譯位域更困難,遍歷位域也困難。

EnumSet內部用單個long保存,(枚舉類型小於等於64個)

枚舉類型要用在集合中,所以沒有理由用位域來表示它。
用EnumSet,簡潔、性能也有優勢。

    public static void main(String[] args) {
        EnumSet<Style> bold = EnumSet.of(Style.BOLD, Style.ITALIC);
        System.out.println(bold);
    }

    enum Style {
        BOLD, ITALIC, UNDERLINE;
    }
    //[BOLD, ITALIC]

33 用EnumMap 代替序數(ordinal())索引

按Enum分組索引,key是Enum。
最好不要用序數來索引數組,而要使用EnumMap。

34 用接口模擬可伸縮的枚舉

用接口模擬可伸縮枚舉有個小小的不足,無法從一個枚舉類型繼承到另一個枚舉類型。
雖然無法便攜可擴展的枚舉類型,卻可以通過編寫接口以及實現該接口的基礎枚舉類型,對它進行模擬

35註解優先於 命名模式

命名模式缺點:

  1. 文字拼寫錯誤會導致失敗,且沒有任何提示。
  2. 無法確保它們只用於相應的程序元素上。
  3. 沒有提供將參數值與程序元素關聯起來的好辦法。

36堅持使用Override註解

37用標記接口定義類型

標記接口是沒有包含方法聲明的接口,而只是知名一個類實現了具有某種屬性的接口。
例如Serializable。

如果想要定義類型,一定要使用接口

第七章方法

38檢查參數的有效性

應該在文檔中清楚的指明參數的限制 以及 拋出的異常。

並在方法體的開頭檢查參數。
這是“應該在發生錯誤之後儘快檢測出錯誤”原則的具體情形。

檢查構造器參數的有效性是非常重要的。

39必要時進行保護性拷貝

編寫一些面對客戶的不良行爲時仍能保持健壯性的類,是非常值得的。

對構造器的每個可變參數進行保護性拷貝是必要的。

保護性拷貝是在檢查參數的有效性(上一條38)之前進行的,並且有效性檢查是針對拷貝之後的對象,而不是針對原始的對象。 這樣做可以避免在“危險階段”(從檢查參數開始,直到拷貝參數之間的時間段)期間從另一個線程改變類的參數。

對於參數類型可以被不可信任方子類化的參數,請不要使用clone方法進行保護性拷貝。

返回可變內部域的保護性拷貝

總結:
如果拷貝的成本受到限制,並且類信任它的客戶端不會不恰當的修改組件,就可以在文檔中說明客戶端的職責,代替保護性拷貝。

40謹慎設計方法簽名

本條目是若干API設計技巧的總結。

  • 謹慎地選擇方法的名稱。易於理解、風格一致、大衆認可,可參考Java類庫。
  • 不要過於追求提供便利的方法。方法太多會使類難以學習、使用、文檔化、測試和維護。爲每個動作提供一個功能齊全的方法,只有當一項操作被經常用到的時候,才考慮爲它提供快捷方式,如果不確定,還是不提供快捷爲好。
  • 避免過長的參數列表。目標是四個參數或更少。 相同類型的長參數序列格外有害。(拆分、輔助類、Builder)
  • 對於參數類型,要優先使用接口而不是類。
  • 對於boolean參數,要優先使用兩個元素的枚舉類型。易於閱讀和編寫,更易於後期添加更多的選項。

41 慎用重載

重載是發生在編譯時的,所以嚴格的說,它並不是多態。
要調用哪個重載方法是在編譯時做出決定的。
對於重載方法的選擇是靜態(編譯時的對象類型決定)的,對於覆蓋的方法的選擇則是動態(運行時的對象類型決定)的。

避免胡亂地使用重載機制。
安全而保守的策略是,永遠不要導出兩個具有相同參數數目的重載方法。
如果方法使用可變參數,保守的策略是根本不要重載它。
例如,考慮ObjectOutputStream類,對於不同類型,提供諸如writeBoolean(boolean)、writeInt(int)方法,好處還可以提供對應的readBoolean()readInt()方法。

導致混亂主要是由於 參數是 有繼承關係的類型。應該避免 同一組參數只許經過類型轉換就可以被傳遞給不同的重載方法。
對於每一對重載方法,至少有一個對應的參數在兩個重載方法中具有“根本不同”的類型
但是當自動裝箱和泛型成了Java語言的一部分之後,謹慎重載顯得更加必要了。
例如List的remove()方法。如果配合Integer,會搞不清是要按下標刪除還是刪除對應的元素。

如果被迫違反本規則,要保證讓更具體化的重載方法把調用轉發給更一般化的重載方法。
例如:

    public boolean contentEquals(StringBuffer sb) {
        return contentEquals((CharSequence)sb);
    }

42慎用可變參數

可變參數方法,接受,0個或多個指定類型的參數。

可變參數也會影響性能,每次方法調用都會導致數組進行一次分配和初始化。

43返回零長度的數組或者集合,而不是null

除非分析表明這個方法正是造成性能問題的真正源頭。
且返回同一零長度數組是可能的,因爲零長度數組不可變,可以被自由共享(15條)。
不可變的空集合:Collections.emptySet(),Collections.emptyList(),Collections.emptyMap(),

44爲所有導出的API元素編寫文檔註釋

第八章 通用程序設計

45將局部變量的作用域最小化

本條目與13條(使類和成員的可訪問性最小化)本質上是類似的。

  • 在第一次使用它的地方聲明。
  • 如果在循環終止之後不再需要循環變量的內容,for循環優先於while循環
  • 使方法小而集中。

46for-each循環優先於傳統的for循環

集合數組,以及任意實現Iterable接口的對象,都能用for-each。
嵌套迭代時,優勢更明顯。
三種常見情況無法使用for-each:

  1. 過濾—-遍歷集合時要刪除選定的元素,需要用迭代器。
  2. 轉換—-遍歷列表或數組時,需要取代它部分或者全部的元素值。需要用迭代器或者數組索引。
  3. 平行迭代—–故意的嵌套迭代(很少使用)

47:瞭解和使用類庫

48:如果需要精確的答案,請 避免使用 float和double

它們尤其不適合於貨幣就算。
使用BigDecimal、int、long進行貨幣計算

49:基本類型優先於裝箱基本類型

它們的區別:

  1. 基本類型只有值,裝箱基本類型是引用
  2. 基本類型只有值,而裝箱基本類型還有null
  3. 基本類型通常更節省時間、空間

對裝箱基本類型運用 == 操作符,幾乎總是錯誤的。
當在一項操作中混合使用基本類型和裝箱基本類型時,裝箱基本類型就會 自動拆箱。 null被拆箱就拋出NPE。

合理使用裝箱基本類型的地方:

  1. 集合中的元素、鍵、值。因爲基本類型不能放在集合中。
  2. 參數化類型中
  3. 反射的方法調用時

50:如果其他類型更適合,則儘量避免使用字符串。

  • 字符串不適合代替其他的值類型。(從網絡接受一段數據)
  • 字符串不適合代替枚舉類型
  • 字符串不適合代替聚集類型。例如String key = className + "#" + i.next();應該用一個類來描述這個數據集,通常是一個私有的靜態成員類。
  • 字符串也不適合代替能力表。

51:當心字符串連接的性能

爲連接n個字符串而重複地使用字符串連接操作符,需要n的平方級的時間。

52:通過接口引用對象

如果有合適的接口類型存在,對於 參數、返回值、變量、域來說,都應該使用接口類型進行聲明。
如果沒有,則使用類層次結構中提供了必要功能的最基礎的類
使得程序更靈活。

53:接口優先於反射機制

缺點:

  1. 喪失了編譯時類型檢查的好處
  2. 反射的代碼長
  3. 性能損失

54:謹慎使用本地方法

使用本地方法來提高性能的做法不值得提倡。
如果本地代碼只是做少量的工作,本地方法就可能降低性能。

55:謹慎地進行優化

不要因爲性能而犧牲合理的架構。要努力編寫好的程序而不是快的程序。
好的程序體現了信息隱藏,只要有可能可以改變單個決策,而不會影響到系統其他部分。

努力避免那些限制性能的設計決策。

爲了獲得好的性能而對API進行包裝,這是一種非常不好的想法。(性能因素可能隨着版本時間而被解決,但是API會一直留在那裏限制自己)

構建完系統之後,要測量性能。利用各種工具,找出瓶頸。優化前、優化後也要測量。

56:遵守普遍接受的命名慣例

類型參數(泛型),通常是 T 任意類型,E 集合元素,KV鍵值,X異常。

方法常用動詞、動詞短語。

第9章 異常

57:只針對異常的情況才使用異常。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章