《Effective Java》——學習筆記(方法&通用程序設計)

方法

第38條:檢查參數的有效性

在方法體的開頭處檢查參數,對於公有的方法,要用Javadoc的@throws標籤在文檔中說明違反參數值限制時會拋出的異常

/**
 * @throws ArithmeticException if m is less than or equal to 0
 /
public BigInteger mod(BigInteger m){
    if(m.signum() <= 0){
        throw new ArithmeticException("Modulus <= 0: " + m);
    }
}

非公有的方法通常應該使用斷言來檢查它們的參數

// Private helper function for a recursive sort
private static void sort(long a[], int offset, int length){
    assert a != null;
    assert offset >= 0 && offset <= a.length;
}

斷言一旦失敗將會拋出AssertionError

對於有些參數,方法本身沒有用到,卻被保存起來供以後使用,檢驗這類參數的有效性尤爲重要(如構造器參數的檢驗)

在方法執行它的計算任務之前,應該先檢查它的參數,這一規則也有例外,一個很重要的例外是,在有些情況下,有效性檢查工作非常昂貴,或者根本是不切實際的,而且有效性檢查已隱含在計算過程中完成,例如Collections.sort(List),其中的某個比較操作就會拋出ClassCastException

有時候,某些計算會隱式地執行必要的有效性檢查,但是如果檢查不成功,就會拋出錯誤的異常,這種情況下,應該將計算過程中拋出的異常轉換爲正確的異常(文檔中標明的異常)

第39條:必要時進行保護性拷貝

沒有對象的幫助時,雖然另一個類不可能修改對象的內部狀態,但是對象很容易在無意識的情況下提供這種幫助,如下類

public final class Period {
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        if (start.compareTo(end) > 0)
            throw new IllegalArgumentException(
                start + " after " + end);
        this.start = start;
        this.end   = end;
    }
}

這個類似乎是不可變的,然而因爲Date類本身是可變的

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!

爲了保護Period實例的內部信息避免受到這種攻擊,對於構造器的每個可變參數進行保護性拷貝是必要的

public Period(Date start, Date end) {
    this.start = new Date(start.getTime());
    this.end   = new Date(end.getTime());

    if (this.start.compareTo(this.end) > 0)
      throw new IllegalArgumentException(start +" after "+ end);
}

保護性拷貝是在檢查參數的有效性之前進行的,並且有效性檢查是針對拷貝之後的對象,而不是針對原始的對象

第40條:謹慎設計方法簽名

  • 謹慎地選擇方法的名稱。命名規範
  • 不要過於追求提供便利的方法。每個方法都應該盡其所能,方法太多會使類難以學習、使用、文檔化、測試和維護
  • 避免過長的參數列表。目標是四個參數,或者更少,可以採用Builder模式,允許客戶端進行多次“setter”調用

第41條:慎用重載

public class CollectionClassifier {

    public static String classify(Set<?> s){
        return "Set";
    }

    public static String classify(List<?> s){
        return "List";
    }

    public static String classify(Collection<?> s){
        return "Unknown Collection";
    }

    public static void main(String[] args) {
        Collection<?>[] collections = {
                new HashSet<String>(),
                new ArrayList<BigInteger>(),
                new HashMap<String, String>().values()
        };

        for (Collection<?> c : collections) {
            System.out.println(classify(c));
        }
    }
}

上例中期望程序會打印出“Set”、“List”以及“Unknown Collection”,實際上卻打印了“Unknown Collection”三次

原因是classify方法被重載了,而要調用哪個重載方法是在編譯時做出決定的,上例中的for (Collection<?> c : collections) 會導致每次調用的都是classify(Collection<?>)這個重載方法

而被覆蓋的方法(override)的選擇則是依據被調用方法所在對象的運行時類型

對於重載,安全而保守的策略是,永遠不要導出兩個具有相同參數數目的重載方法

第42條:慎用可變參數

可變參數方法接受0個或者多個指定類型的參數,可變參數機制通過先創建一個數組,數組的大小爲在調用位置所傳遞的參數數量,然後將參數值傳到數組中,最後將數組傳遞給方法

假設確定對某個方法95%的調用會有3個或者更少的參數,就聲明方法的5個重載,每個重載方法帶有0至3個普通參數,當參數的數目超過3個時,就使用一個可變參數方法

public void foo() {}
public void foo(int a1) {}
public void foo(int a1, int a2) {}
public void foo(int a1, int a2, int a3) {}
public void foo(int a1, int a2, int a3, int ... rest) {}

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

對於一個返回null而不是零長度數組或者集合的方法,幾乎每次用到該方法時都需要額外的代碼來處理null返回值

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

爲了正確地編寫API文檔,必須在每個被導出的類、接口、構造器、方法和域聲明之前增加一個文檔註釋

方法的文檔註釋應該簡潔地描述出它和客戶端之間的約定,說明這個方法做了什麼,每個參數都應該有一個@param標籤,以及一個@return標籤(除非返回類型爲void),以及對於該方法拋出的每個異常,無論是受檢的還是未受檢的,都有一個@throws標籤,如果方法啓動了後臺線程,文檔中也應該說明這一點

通用程序設計

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

將局部變量的作用域最小化,可以增強代碼的可讀性和可維護性,並降低出錯的可能性

要使局部變量的作用域最小化,最有利的方法就是在第一次使用它的地方聲明

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

for-each循環通過完全隱藏迭代器或者索引變量,避免了混亂和出錯的可能,並且沒有性能損失

有三種常見的情況無法使用for-each循環:

  • 1.過濾——如果需要遍歷集合,並刪除選定的元素,就需要使用顯式的迭代器,以便可以調用它的remove方法
  • 2.轉換——如果需要遍歷列表或者數組,並取代它部分或者全部的元素值,就需要列表迭代器或者數組索引,以便設定元素的值
  • 3.平行迭代——如果需要並行地遍歷多個集合,就需要顯式地控制迭代器或者索引變量,以便所有迭代器或者索引變量都可以得到同步前移

第47條:瞭解和使用類庫

通過使用標準類庫,可以充分利用這些編寫標準類庫的專家的知識,以及其他人的使用經驗

程序員應該把時間花在應用程序上,而不是底層的細節上

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

float和double類型主要是爲了科學計算和工程計算而設計的,它們執行二進制浮點運算,是爲了在廣泛的數值範圍上提供較爲精確的快速近似計算而精心設計的。因此,它們並沒有提供完全精確的結果,所以不應該被用於需要精確結果的場合

System.out.println(1.00 - 0.42);

的結果爲:0.5800000000000001

解決這個問題的正確辦法是使用BigDecimal、int或者long

BigDecimal a = new BigDecimal(1.00);
BigDecimal b = new BigDecimal(0.42);
System.out.println(a.subtract(b));

// 輸出結果爲 0.58

使用BigDecimal有兩個缺點:與使用基本運算類型相比,這樣做很不方便,而且很慢

除了使用BigDecimal之外,還有一種辦法是使用int或者long,並自己記錄十進制小數點,如果數值範圍沒有超過9位十進制數字,就可以使用int;如果不超過18位數字,就可以使用long;如果數值可能超過18位數字,就必須使用BigDecimal

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

基本類型(int、double、boolean)和裝箱基本類型(Integer、Double、Boolean)之間有三個主要區別

  • 第一,基本類型只有值,而裝箱基本類型則具有與它們的值不同的同一性
  • 第二,基本類型只有功能完備的值,而每個裝箱基本類型還有個非功能值:null
  • 第三,基本類型通常比裝箱基本類型更節省時間和空間

當在一項操作中混合使用基本類型和裝箱基本類型時,裝箱基本類型就會自動拆箱

應該使用裝箱基本類型的情況:

  • 作爲集合中的元素、鍵和值List<Integer>

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

  • 字符串不適合代替其他的值類型,如int、boolean
  • 字符串不適合代替枚舉類型
  • 字符串不適合代替聚集類型

    String compoundKey = className + "#" + i.next();
    // 這種方法有許多缺點,如爲了訪問單獨的域,必須解析該字符串。更好的做法是,簡單地編寫一個類來描述這個數據集,通常是一個私有的靜態成員類
    
  • 字符串也不適合代替能力表(字符串被用於對某種功能進行授權訪問)

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

字符串連接操作符(+)是把多個字符串合併爲一個字符串的便利途徑,但它不適合運用在大規模的場景中。爲連接n個字符串而重複地使用字符串連接操作符,需要n的平方級的時間。這是因爲兩個字符串被連接在一起時,它們的內容都要被拷貝

建議使用StringBuilder替代String

第52條:通過接口引用對象

如果有合適的接口類型存在,那麼對於參數、返回值、變量和域來說,就都應該使用接口類型進行聲明

如果對象的基本類型是類,不是接口,應該用相關的基類(往往是抽象類)來引用這個對象,而不是用它的實現類

第53條:接口優先於反射機制

核心反射機制java.lang.reflect,提供了“通過程序來訪問關於已裝載的類的信息”的能力。給定一個Class實例,可以獲得Constructor、Method和Field實例

反射機制(reflection)允許一個類使用另一個類,即使當前者被編譯的時候後者還根本不存在。然而,這種能力也要付出代價:

  • 喪失了編譯時類型檢查的好處,包括異常檢查
  • 執行反射訪問所需要的代碼非常笨拙和冗長
  • 性能損失,反射方法調用比普通方法調用慢了許多

通常,普通應用程序在運行時不應該以反射方式訪問對象

對於有些程序,它們必須用到在編譯時無法獲取的類,但是在編譯時存在適當的接口或者超類,通過它們可以引用這個類。這種情況,就可以以反射方式創建實例,然後通過它們的接口或者超類,以正常的方式訪問這些實例,如下例

public static void main(String[] args) {
    // Translate the class name into a Class object
    Class<?> cl = null;
    try {
        cl = Class.forName(args[0]);
    } catch (ClassNotFoundException e) {
        System.err.println("Class not found.");
        System.exit(1);
    }

    // Instantiate the class
    Set<String> s = null;
    try {
        s = (Set<String>) cl.newInstance();
    } catch (IllegalAccessException e) {
        System.err.println("Class not accessible.");
        System.exit(1);
    } catch (InstantiationException e) {
        System.err.println("Class not instantiable.");
        System.exit(1);
    }

    // Exercise the set
    s.addAll(Arrays.asList(args).subList(1, args.length));
    System.out.println(s);
}

上述程序創建了一個Set<String>實例,它的類是由第一個命令行參數指定的,該程序把其餘的命令行參數插入到這個集合中

這個程序可以很容易地變成一個通用的集合測試器,通過侵入式地操作一個或者多個集合實例,並檢查是否遵守Set接口的約定,以此來驗證指定的Set實現。絕大多數情況下,使用反射機制時需要的正是這種方法

反射機制是一種功能強大的機制,對於特定的複雜系統編程任務,它是非常必要的,如有可能,就應該僅僅使用反射機制來實例化對象,而訪問對象時則使用編譯時已知的某個接口或者超類

第54條:謹慎地使用本地方法

使用本地方法來提高性能的做法不值得提倡

使用本地方法有一些嚴重的缺點,因爲本地語言不是安全的,所以使用本地方法的應用程序也不再能免受內存毀壞錯誤的影響,在進入和退出本地代碼時,需要相關的固定開銷

第55條:謹慎地進行優化

要努力編寫好的程序而不是快的程序,好的程序體現了信息隱藏的原則:只要有可能,它們就會把設計決策集中在單個模塊中

努力避免那些限制性能的設計決策,模塊之間交互關係以及模塊與外界交互關係的組件都有可能對系統本該達到的性能產生嚴重的限制

要考慮API設計決策的性能後果,如使公有的類型成爲可變的,這可能會導致大量不必要的保護性拷貝

性能剖析工具有助於你決定應該把優化的重心放在哪裏,這樣的工具可以爲你提供運行時的信息,比如每個方法大致上花費了多少時間、它被調用多少次,甚至還可以警告你是否需要改變算法

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

標識符類型 例子
com.google.inject
類或者接口 Timer,FutureTask
方法或者域 remove,isDigit,getCrc
常量域 MIN_VALUE
局部變量 i,xref
類型參數 T,E,K,V
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章