Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。java
愉快使用Java的緣由,它是一種安全的語言(safe language)。 這意味着在缺乏本地方法(native methods)的狀況下,它不受緩衝區溢出,數組溢出,野指針以及其餘困擾C和C ++等不安全語言的內存損壞錯誤的影響。 在一種安全的語言中,不管系統的任何其餘部分發生什麼,均可以編寫類並確切地知道它們的不變量會保持不變。 在將全部內存視爲一個巨大數組的語言中,這是不可能的。git
即便在一種安全的語言中,若是不付出一些努力,也不會與其餘類隔離。必須防護性地編寫程序,假定類的客戶端盡力摧毀類其不變量。隨着人們更加努力地試圖破壞系統的安全性,這種狀況變得愈來愈真實,但更常見的是,你的類將不得不處理因爲善意得程序員誠實錯誤而致使的意外行爲。無論怎樣,花時間編寫在客戶端行爲不佳的狀況下仍然保持健壯的類是值得的。程序員
若是沒有對象的幫助,另外一個類是不可能修改對象的內部狀態的,可是在無心的狀況下提供這樣的幫助卻很是地容易。例如,考慮如下類,表示一個不可變的時間期間:github
// Broken "immutable" time period class 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; } ... // Remainder omitted }
乍一看,這個相似乎是不可變的,並強制執行不變式,即period實例的開始時間並不在結束時間以後。然而,利用Date類是可變的這一事實很容易違反這個不變式:數組
// Attack the internals of a Period instance Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear(78); // Modifies internals of p!
從Java 8開始,解決此問題的顯而易見的方法是使用Instant
(或LocalDateTime
或ZonedDateTime
)代替Date
,由於Instant
和其餘java.time包下的類是不可變的(條目17)。Date
已過期,不該再在新代碼中使用。 也就是說,問題仍然存在:有時必須在API和內部表示中使用可變值類型,本條目中討論的技術也適用於這些時間。安全
爲了保護Period
實例的內部不受這種攻擊,必須將每一個可變參數的防護性拷貝應用到構造方法中,並將拷貝用做Period實例的組件,以替代原始實例:數據結構
// Repaired constructor - makes defensive copies of parameters 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); }
有了新的構造方法後,前面的攻擊將不會對Period
實例產生影響。注意,防護性拷貝是在檢查參數(條目49)的有效性以前進行的,有效性檢查是在拷貝上而不是在原始實例上進行的。雖然這看起來不天然,但倒是必要的。它在檢查參數和拷貝參數之間的漏洞窗口期間保護類不受其餘線程對參數的更改的影響。在計算機安全社區中,這稱爲 time-of-check/time-of-use或TOCTOU攻擊[Viega01]。函數
還請注意,咱們沒有使用Date的clone
方法來建立防護性拷貝。由於Date是非final的,因此clone方法不能保證返回類爲java.util.Date
的對象,它能夠返回一個不受信任的子類的實例,這個子類是專門爲惡意破壞而設計的。例如,這樣的子類能夠在建立時在私有靜態列表中記錄對每一個實例的引用,並容許攻擊者訪問該列表。這將使攻擊者能夠自由控制全部實例。爲了防止這類攻擊,不要使用clone方法對其類型可由不可信任子類化的參數進行防護性拷貝。性能
雖然替換構造方法成功地抵禦了先前的攻擊,可是仍然能夠對Period實例進行修改,由於它的訪問器提供了對其可變內部結構的訪問:this
// Second attack on the internals of a Period instance Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); p.end().setYear(78); // Modifies internals of p!
爲了抵禦第二次攻擊,只需修改訪問器以返回可變內部字屬性的防護性拷貝:
// Repaired accessors - make defensive copies of internal fields public Date start() { return new Date(start.getTime()); } public Date end() { return new Date(end.getTime()); }
使用新的構造方法和新的訪問器,Period是真正不可變的。 不管程序員多麼惡意或不稱職,根本沒有辦法違反一個period實例的開頭不跟隨其結束的不變量(不使用諸如本地方法和反射之類的語言外方法)。 這是正確的,由於除了period自己以外的任何類都沒法訪問period實例中的任何可變屬性。 這些屬性真正封裝在對象中。
在訪問器中,與構造方法不一樣,容許使用clone方法來製做防護性拷貝。 這是由於咱們知道Period的內部Date對象的類是java.util.Date,而不是一些不受信任的子類。 也就是說,因爲條目13中列出的緣由,一般最好使用構造方法或靜態工廠來拷貝實例。
參數的防護性拷貝不只僅適用於不可變類。 每次編寫在內部數據結構中存儲對客戶端提供的對象的引用的方法或構造函數時,請考慮客戶端提供的對象是否多是可變的。 若是是,請考慮在將對象輸入數據結構後,你的類是否能夠容忍對象的更改。 若是答案是否認的,則必須防護性地拷貝對象,並將拷貝輸入到數據結構中,以替代原始數據結構。 例如,若是你正在考慮使用客戶端提供的對象引用做爲內部set實例中的元素或做爲內部map實例中的鍵,您應該意識到若是對象被修改後插入,對象的set或map的不變量將被破壞。
在將內部組件返回給客戶端以前進行防護性拷貝也是如此。不管你的類是不是不可變的,在返回對可拜年的內部組件的引用以前,都應該三思。可能的狀況是,應該返回一個防護性拷貝。記住,非零長度數組老是可變的。所以,在將內部數組返回給客戶端以前,應該始終對其進行防護性拷貝。或者,能夠返回數組的不可變視圖。這兩項技術都記載於條目15。
能夠說,全部這些的真正教訓是,在可能的狀況下,應該使用不可變對象做爲對象的組件,這樣就沒必要擔憂防護性拷貝(條目17)。在咱們的Period示例中,使用Instant(或LocalDateTime或ZonedDateTime),除非使用的是Java 8以前的版本。若是使用的是較早的版本,則一個選項是存儲Date.getTime()
返回的基本類型long來代替Date引用。
可能存在與防護性拷貝相關的性能損失,而且它並不老是合理的。若是一個類信任它的調用者不修改內部組件,也許是由於這個類和它的客戶端都是同一個包的一部分,那麼它可能不須要防護性的拷貝。在這些狀況下,類文檔應該明確指出調用者不能修改受影響的參數或返回值。
即便跨越包邊界,在將可變參數集成到對象以前對其進行防護性拷貝也並不老是合適的。有些方法和構造方法的調用指示參數引用的對象的顯式切換。當調用這樣的方法時,客戶端承諾再也不直接修改對象。但願得到客戶端提供的可變對象的全部權的方法或構造方法必須在其文檔中明確說明這一點。
包含方法或構造方法的類,這些方法或構造方法的調用指示控制權的轉移,這些類沒法防護惡意客戶端。 只有當一個類和它的客戶之間存在相互信任,或者當對類的不變量形成損害時,除了客戶以外,任何人都不會受到損害。 後一種狀況的一個例子是包裝類模式(第18項)。 根據包裝類的性質,客戶端能夠經過在包裝後直接訪問對象來破壞類的不變性,但這一般只會損害客戶端。
總之,若是一個類有從它的客戶端獲取或返回的可變組件,那麼這個類必須防護性地拷貝這些組件。若是拷貝的成本過高,而且類信任它的客戶端不會不適當地修改組件,則能夠用文檔替換防護性拷貝,該文檔概述了客戶端不得修改受影響組件的責任。