1. 程式人生 > >Effective Java 讀書筆記——39:必要時進行保護性拷貝

Effective Java 讀書筆記——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;
	}

	public Date start() {
		return start;
	}

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

可以看到,在構造器中加入了約束條件,開始時間要小於結束時間,這似乎不可變。但是Date類本身是可變的,因此很容易在使上面的類違背約束條件。
        Date start = new Date();
        Date end = new Date();
        Period p = new Period(start, end);
        end.setYear(78);  // Modifies internals of p!
        System.out.println(p);

由於Date傳的是引用,通過這種方式,很容易修改Period類內部的Date的資料。

對構造器的每個可變引數進行保護性拷貝

在這種方法中,構造器中不直接接受原物件的引用。而是,對原物件中的資料進行拷貝,使用備份物件作為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);
	}

注意,這裡的保護性拷貝是在檢查引數的有效性之前進行的
(原因是其他執行緒可能會修改原始物件中的資料,詳細見38條),並且有效性檢查是針對拷貝以後的物件,而不是原始物件。這樣做可以避免在危險階段(window of vulnerability)期間從另一個執行緒改變的引數,這裡的危險階段指的是構造器中的,從檢查引數階段開始,到拷貝引數階段。因此,通過反向這個過程,來避免這個階段。 值得注意的是,這裡用的是獲取需要的Date資料來進行拷貝,才不是使用clone進行拷貝,是因為,Date本身不是final的,不能保證返回的一定是一個安全的java.util.Date類。

另一種方式

上一種方式 仍然不能完全避免修改Period例項的可能,請看下面的程式碼:
        start = new Date();
        end = new Date();
        p = new Period(start, end);
        p.end().setYear(78);  // Modifies internals of p!
        System.out.println(p);
可以看到,通過獲取返回值的引用,仍然可以間接修改Period內的資料。 因此,只需要修改這兩個訪問方法,使其返回的是內部資料的保護性拷貝即可。
	public Date start() {
		return new Date(start.getTime());
	}

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

改成這樣一樣,就真正實現了Period不可變,無論使用什麼方法,都不能違背開始時間不落後於結束時間的約束。

總結

引數的保護性拷貝不僅僅侷限於不可變類,每當編寫方法和構造器的時候,如果允許客戶端提供的物件進入到內部的資料,就需要考慮,客戶端的物件是否是可變,它的可變是否會對內部資料產生影響。 如同上面第二種方式,當內部資料返回客戶端的時候,同樣需要考慮這個問題,是否需要返回內部資料的保護性拷貝。一般來說,將內部引用返回給客戶端的時候,都需要進行保護性拷貝(見13條)。 因此,只要有可能,都應該使用不可變物件作為物件的內部元件,這樣就不必進行保護性拷貝了。常常,程式設計師會使用Date.getTime()返回的long型別作為內部的時間表示法,因為Date是可變的。