Java 設計方法的五條優秀實踐清單

本文結合《Effective Java》第七章《方法》和自己的理解及實踐,講解了設計Java方法的優秀指導原則,文章發佈於專欄Effective Java,歡迎讀者訂閱。


清單1: 檢查參數的有效性

在每個方法的開頭檢查方法的參數,遵循“應該在發生錯誤之後儘快檢測出錯誤”這一原則。

對於公有的方法,對於校驗失敗的入參,拋出異常,常見的有IllegalArgumentException(非法參數異常)、Arithmeticexception(運算條件異常)等,並在Javadoc裏進行說明。

對於私有方法,不像public方法需要防範外界的不可信任性,private方法是給創建者自己使用的,因此遵循的是一種契約關係,因此對於參數的校驗,可以使用assert斷言。使用斷言的好處是,在開發階段,可以使用-ea來開啓斷言,保證調用者入參的準確性,在生產環境,可以禁用斷言,去掉參數檢查,提高性能。關於java assert的更多信息,可以參考這篇博客,斷言絕對不是雞肋

當然,有時候,有效性檢查已經在方法後續的執行過程中完成,比如Collection.sort方法,會在排序中校驗對象是不是可以互相比較,那麼就不必在調用sort方法之前進行檢查了。


清單2: 必要時進行保護性拷貝

我們設計的方法,往往會在不經意間,給外界提供修改對象內部狀態的機會,破壞了類的不可變性(不可變性,參考專欄的另一篇文章Java 設計類和接口的八條優秀實踐清單——清單3 使類的可變性最小化)。

比如下面這個類,它聲稱可以表示一段不可變的時間週期:

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

    /**
     * @param  start the beginning of the period
     * @param  end the end of the period; must not precede start
     * @throws IllegalArgumentException if start is after end
     * @throws NullPointerException if start or end is null
     */
    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;
    }
}

雖然這個類給Date對象聲明瞭final,但如同專欄之前說的,final只是引用不可變,客戶端很容易去修改start和end:

        Date start = new Date();
        Date end = new Date();
        Period p = new Period(start, end);
        end.setYear(78);

同時由於類提供了返回start和end的方法,客戶端還能這樣修改:

        Date start = new Date();
        Date end = new Date();
        p = new Period(start, end);
        p.end().setYear(78);

解決這個問題的關鍵在於進行保護性拷貝,給客戶端返回一個新的對象,使得客戶端在修改它獲取到的對象時,不會影響原來的對象,加入保護性拷貝後的類如下:
public final class Period {
	private final Date start;
	private final Date end;

	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());
	}

	public String toString() {
		return start + " - " + end;
	}
}

修改後的類,在構造器中,不直接使用外部傳入的對象,而是進行一次拷貝;同時在每個返回內部屬性的方法,不直接返回原有對象,也做一次拷貝,這樣,這個類纔是真正的不可變類。

這裏的構造器,爲什麼要先拷貝再校驗呢?不能這樣寫嗎:

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

原因是,如果這樣寫,就會有一個漏洞——在執行完校驗和進行拷貝的這段時間,外部傳入的start和end對象有可能被修改,而不再滿足我們的校驗條件。這種在計算機安全術語中,叫做Time-Of-Check/Time-Of-Use或者TOCTOU攻擊。

到這裏,我們終於把這個類做到不可變了,但我們回頭想想,如果一開始我們不使用Date對象,而是直接使用Date.getTime()的long基本類型來表示的時間,不就不需要保護性拷貝了嗎?由此得到另一個啓示——儘量使用不可變對象作爲對象內部的組件。


清單3: 避免過長的參數列表

好的方法,參數不能超過4個,超過4個,方法就不方便使用。

有三種方法可以縮短過長的參數列表:

1、把方法分解成多個獨立功能的方法

很多時候,我們的方法參數過多,是因爲實現的功能太複雜。比如,java的List並沒有提供“在子列表中查找元素第一個索引和最後一個索引”的方法,如果提供,那麼方法就需要三個參數:子列表的開始索引、子列表的結束索引、要查找的元素;List方法提供了subList、indexOf和lastIndexOf方法,客戶端通過組合使用這三個防範,就能實現這個功能。

2、使用輔助類,存儲原來的參數,方法入參改爲輔助類即可

3、使用builder模式,參考專欄的另一篇文章  Java創建對象的方法清單 —— 原來還可以這樣創建對象 

清單4: 返回0長度的數組或者集合,而不是null

如果一個方法聲明返回的是一個數組或者集合,那麼當你打算返回null時,請返回一個長度爲0的數組或者集合,這樣能讓調用者省去null對象校驗。

有人可能會擔心每次都創建一個空的數組或者集合去返回會影響效率,那麼完全可以先創建好不可變的空的數組或者集合。

對於數組,只需要加上final修飾符,那麼,零長度的數組就是不可變的。

private static final String[] EMPTY_STR_ARRAY = new String[0];

對於集合,Collections類提供了emptyList、emptySet、emptyMap方法,同樣可以返回一個不可變的空集合。


清單5: 爲每個方法編寫Javadoc文檔註釋

工作這段經歷,讓我深刻地體會到,沒有註釋,讀懂代碼很辛苦;良好且正確的註釋,可以幫助更快更好地去讀懂代碼。

一個方法的註釋應該包含以下幾個部分:

方法概要描述:往往是一個簡單的動詞

方法使用後產生的一些副作用等詳細描述:比如是否會啓動了線程,是不是線程安全等

方法入參描述:使用@param

方法拋出的異常描述: 往往要在異常後面寫上什麼情況下會拋出

示例:

    /**
     * Returns the element at the specified position in this list.
     *
	 * This method is thread-safe, and it will start a thread. 
     * @param index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException if the index is out of range
     *         (<tt>index < 0 || index >= size()</tt>)
     */

關於Java註釋的更多指導,可以參考Oracle最權威的指導文檔  How to Write Doc Comments for the Javadoc Tool


以上,希望能對你設計方法有所幫助。


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