Effective Java Item 50:需要時進行防禦性複製
整理 Effective Java 書中 Item 50: Make defensice copies心得筆記
主旨
就算 Java 是種安全語言,也不能完全保證類別的內部狀態不被外部破壞。尤其是當類別使用到可變的物件作為參數或欄位時,必須小心:若沒有做 defensive copy(防守性複製),有心人或粗心人都可能破壞你的類別不變條件(invariant)。這篇就是要提醒你:該防守時就防守。
點出問題
看看這個「看起來像是 immutable」的類別 Period:
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 是 final 就安全了?事實上並不是,因為 Date 是可變的:
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Boom!外部直接改變 p 的內容
即使 constructor 的檢查通過,只要建構完之後改變傳進去的參數,就能破壞這個類別的邏輯。
範例:如何修復這種破綻
正確做法是——複製進來,複製出去
建構子:複製進來
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(this.start + " after " + this.end);
}
注意順序:先複製、再檢查。這樣才能防止多執行緒造成的 TOCTOU(Time of Check to Time of Use)問題。
存取方法:複製出去
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
這樣別人即使呼叫 p.end().setYear(78),改的也是副本,對 Period 本身毫無影響。
劃重點:什麼時候該做 defensive copy?
- ✅ 若你的類別會 儲存外部提供的物件參考(例如建構子的參數),那你要 複製進來。
- ✅ 若你的類別會 傳回內部可變欄位,那你要 複製出去。
- ❌ 別用
clone()複製參數,因為可能被 subclass 利用來偷偷植入惡意邏輯。 - ✅ 可以用 constructor(如
new Date(obj.getTime()))或 static factory 來複製。 - ✅ 陣列也是可變物件,回傳時記得複製或給 immutable view。
- ✅ 有時候若信任 client(如同 package),可以不複製,但要清楚寫在文件裡。
- ✅ 少數情況下,會有「handoff」語意,例如某方法接手外部物件的控制權,那就不用複製,但請在文件中說明清楚責任。
真實世界範例
假設你在開發 SDK,提供類似以下 API:
public void setData(byte[] data);
若你只是把參數指派給內部欄位:
this.data = data;
那使用者改了原本的 data 陣列,你的類別就跟著改了!
正確做法應該是:
this.data = Arrays.copyOf(data, data.length);
如此一來,外部就沒辦法影響到你的類別。
小結
記得:Java 再怎麼安全,也擋不住你「自己開門讓壞人進來」。只要類別跟 可變物件 有接觸,就要提高警覺。defensive copy 雖然有點小麻煩,但可以大幅降低 debug 地獄的機率。
Read other posts