Effective Java(六)

六、方法

1. 檢查參數的有效性

        絕大多數方法和構造器對於傳遞給它們的參數值都會有某些限制,應該在文檔中清楚地指明這些限制,並且在方法體的開頭處檢查參數,以強制施加這些限制。這樣做可以及早地發現並處理錯誤。

public BigInteger mod (BigInteger m) {
    if (m.signum() <= 0 ) {
        throw new ArithmeticException("Modulus <= 0: " + m)
    ...
}

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

private static void sort(long a[], int offset, int length) {
    assert a != null;
    assert offset >= 0 && offset <= a.length;
    assert length >= 0 && length <= a.length - offset;
    ...
}  


        對於有些參數,方法本身沒有用到,只是被保存起來供以後使用,檢驗這類參數的有效性尤爲重要。構造器就是這樣的方法,檢查構造器參數的有效性是非常重要的,可以避免構造出來的對象違反這個類的約束條件。
        對參數的有效性進行檢查,並不是說對參數的任何限制都是件好事。相反,在設計方法時,應該使它們儘可能地通用,並符合實際的需要。

2. 必要時進行保護性拷貝

        要防止對象的狀態被無意或惡意地修改,造成各種不可預期的行爲。

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;
    }
    
    public Date start() {
        return start;
    }
    
    public Date end() {
        return end;
    }
    ...
}

        上述代碼本意是表示一段不可變的時間週期,且週期的起始時間(start)不能在結束時間(end)之後。然而,因爲Date類本身是可變的,因此很容易違反約束條件而遭到攻擊: 

//攻擊方法一
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);

//攻擊方法二
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78);

        出現上述問題的原因是對象無意識地提供了使外界修改它內部狀態的方法。爲避免對象的內部狀態受到類似攻擊,對可變參數進行保護性拷貝是必要的。 

//修補一
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);
    }
}

//修補二
public Date start() {
    return new Date(start.getTime());
}

public Date end() {
    return new Date(end.getTime());
}

需要考慮進行保護性拷貝的情形如下:

(1)當編寫方法或構造器時,如果它允許客戶提供的對象進入到對象數據結構內部,則有必要考慮一下,客戶提供的對象是否可能是可變的,如果是可變的,且可能會對對象造成不可接受的影響,就必須對該對象進行保護性拷貝。

(2)在把一個指向內部可變組件的引用返回給客戶端之前,應該倍加認真地考慮,返回的引用是否應該進行保護性拷貝,防止客戶通過該引用修改對象內部的可變組件,對對象的功能造成破壞。

3. 謹慎設計方法簽名

下述幾條是關於API設計的技巧:
a. 謹慎地選擇方法的名稱
方法名應遵循命名習慣,易於理解,包內的命名風格保持一致。
b. 不要過於追求提供便利的方法
每個方法應該盡其所能,方法太多會使類難以學習、使用、文檔化、測試和維護。對於類和接口所支持的每個動作,都提供一個功能齊全的方法。只有當一項操作被經常用到的時候,才考慮爲它提供快捷方式。
c. 避免過長的參數列表
目標是四個參數,或者更少。
相同類型的長參數序列格外有害。
對於參數類型,要優先使用接口而不是類。
對於boolean參數,要優先使用兩個元素的枚舉類型。

有三種方法可以縮短過長的參數列表:
a. 把方法分解成多個方法,每個方法只需要這些參數的一個子集
b. 創建輔助類,用來保存參數的分組。輔助類一般爲靜態成員類。
c. 從對象構建到方法都採用Builder模式。

4. 慎用重載

        要區分清楚重載(overload)重寫(override)

        重載:在一個類裏面,方法名字相同,而參數不同,返回類型可以相同也可以不同。
        重寫:子類對父類允許訪問方法的實現過程進行重新編寫,返回值和形參都不能改變。

        程序運行時調用哪個重載方法是在編譯時決定的,選擇依據就是重載方法的編譯時參數類型
        程序運行時調用哪個重寫方法是在運行時決定的,選擇依據就是重寫方法所在對象的運行時類型

public class Classifier {
    
    public static String classify(Set<?> s) {
        return "Set";
    }
    
    public static String classify(List<?> l) {
        return "list";
    }
    
    public static String classify(Collection<?> c) {
        return "Collection";
    }
    
    public static void main(String[] args) {
        Collection<?>[] colls = {new HashSet<String>(), new ArrayList<String>(), new HashMap<String, String>().values()};
        for(Collection<?> c : colls) {
            System.out.println(classify(c));
        }
    }
}

執行結果爲:
Collection
Collection
Collection

        上述程序的執行結果與期望是不一樣的,原因就是這裏使用的方法的重載,而重載方法的調用是根據參數的編譯時類型決定的,程序在編譯時,參數類型都是Collection,這就決定了程序運行時調用的方法都是classify(Collection<?> c)
對於重載來說,下面三種使用方式是容易讓人產生混淆,也是可能會導致錯誤發生的:
        a. 兩個重載方法具有相同參數數目
        b. 方法使用可變參數
        c. 兩個重載方法參數數目相同,且類型可以轉換

        對於上述情形,安全而保守的方式就是不要去重載它。像ObjectOutputStream類,採用不同的命名方式而並沒有使用重載方法去寫出不同的數據類型:

//利用重載的方式
write(long val);
write(float val);
write(double val);
...

//採用不同的命名方式,採用這種方式,客戶是不可能調用錯方法的
writeLong(long val);
writeFloat(float val);
writeDouble(double val);
...

        構造器的重載不能使用命名的方式避免重載,可以選擇使用靜態工廠模式

5. 慎用可變參數

        JDK1.5版本增加了對可變參數方法的支持。可變參數方法接受0個或者多個指定類型的參數。它的實現機制是:先創建一個數組,數組的大小爲在調用位置所傳遞的參數數量,然後將參數傳到數組中,最後將數組傳遞給方法。

static int sum(int... args) {
    int sum = 0;
    for (int arg : args) {
        sum += arg;
    }
    return sum;
}

        在重視性能的情況下,使用可變參數機制要特別小心。可變參數方法的每次調用都會導致進行一次數組分配和初始化。如果憑經驗確定無法承受這一成本,但又需要可變參數的靈活性,可採用下面這種實現模式:

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) {}

        如JDK源碼中EnumSet類對它的靜態工廠使用這種方法,最大限度地減少創建枚舉集合的成本。

        

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

        對於一個返回null而不是零長度數組或集合的方法,客戶端中必須要有額外的代碼來處理null返回值。這樣做很容易出錯,因爲很可能會忘記寫這種專門的代碼來處理null返回值。

//返回零長度數組
private final List<Cheese> cheesesInStock = ...;
public Cheese[] getCheeses() {
    if (cheesesInStock.size() == 0) {
        return null;  //應該返回 Cheese[0]
    }
    ...
}

//返回零長度集合
public List<Cheese> getCheeseList() {
    if ( cheesesInStock.isEmpty() ) {
        return null; //應該返回 Collections.emptyList()
    } else {
        return new ArrayList<Cheese>(cheesesInStock);
    }
}

 7. 爲所有導出的API元素編寫文檔註解

        如果要想使一個API真正可用,就必須爲其編寫文檔。Java提供了Javadoc工具,利用特殊格式的文檔註釋,根據源代碼自動產生API文檔
        爲了正確地編寫API文檔,必須在每個被導出的類、接口、構造器、方法和域聲明之前增加一個文檔註釋。
        如果類是可序列化的,也應該對它的序列化形式編寫文檔。
        方法的文檔註釋應該簡潔地描述出它和客戶端之間的約定。這個約定應該說明這個方法做了什麼,而不是說明它是如何完成這項工作的。

    /**
     * Returns the elements at the specified position in this list.
     * 
     * <p>This method is <i>not</i>guaranteed to run in constant
     * time. In some implementations it may run in time proportional
     * to the element position.
     * 
     * @param index index of element to return; must be
     *        non-negative and less than the size of this list
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException if the index is out of range
     *         ({@code index < 0 || index >= this.size()}})
     *         
     */
    E get(int index);

 

發佈了66 篇原創文章 · 獲贊 66 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章