yiaz 讀書筆記,翻譯於 effective java 3th 英文版,可能有些地方有錯誤。歡迎指正。java
靜態工廠方法和構造器都有一個限制:當有許多參數的時候,它們不能很好的擴展。程序員
好比試想下以下場景:考慮使用一個類表示食品包裝袋上的養分成分標籤。這些標籤只有幾個是必須的——每份的含量、每罐的含量、每份的卡路里,除了這幾個必選的,還有超過 20
個可選的標籤——總脂肪量、飽和脂肪量等等。對於這些可選的標籤,大部分產品通常都只有幾個標籤的有值,不是每個標籤都用到。安全
(telescoping constructor
)重疊構造器模式ide
對於這種狀況,你應該選擇哪一種構造器或者靜態工廠方法。通常程序員的習慣是採用 (telescoping constructor
)重疊構造器模式。在這種模式中,提供一個包含必選參數的構造器,再提供其餘一些列包含可選參數的構造器,第一個包含一個能夠參數、第二個包含兩個可選參數,以此類推下去,直到包含全部的可選參數。性能
示例代碼:ui
// Telescoping constructor pattern - does not scale well! public class NutritionFacts { private final int servingSize; // (mL) required private final int servings; // (per container) required private final int calories; // (per serving) optional private final int fat; // (g/serving) optional private final int sodium; // (mg/serving) optional private final int carbohydrate; // (g/serving) optional public NutritionFacts(int servingSize, int servings) { this(servingSize, servings, 0); } public NutritionFacts(int servingSize, int servings,int calories) { this(servingSize, servings, calories, 0); } public NutritionFacts(int servingSize, int servings,int calories, int fat) { this(servingSize, servings, calories, fat, 0); } public NutritionFacts(int servingSize, int servings,int calories, int fat, int sodium) { this(servingSize, servings, calories, fat, sodium, 0); } public NutritionFacts(int servingSize, int servings,int calories, int fat, int sodium, int carbohydrate) { this.servingSize = servingSize; this.servings = servings; this.calories = calories; this.fat = fat; this.sodium = sodium; this.carbohydrate = carbohydrate; } }
當你想建立一個實例的時候,你只須要找包含你須要的而且是最短參數列表的構造器便可。this
這裏有一些問題,好比看下面的代碼:翻譯
NutritionFacts cocaCola = new NutritionFacts(240, 8, 0, 0, 35, 27);
設計
其中,第 1,2 個可選參數,咱們是不須要的,可是程序中沒有提供直接賦值第 3,4個可選參數的構造器,所以,咱們只能選擇包含了 1,2,3,4 個參數的構造器。這裏面要求了許多你不想設置的參數,可是你又被迫的設置它們,在這裏,傳入對應的屬性的默認值 0。而且這種模式,隨着參數的增長,將變得愈來愈難以忍受,不管是編寫程序的人,仍是調用程序的人。code
總而言之,(telescoping constructor
)重疊構造器模式,可使用,可是它對客戶端來講,很不友好,寫和讀都是一件困難的事情。它們很難搞懂那些參數對應的究竟是什麼屬性,必須好好的比對構造器代碼。而且當參數不少的時候,很容易出 bug
,若是使用的時候,無心間顛倒了兩個參數的位置,編譯器是不會出現警告的,由於這裏的類型同樣,都是 int
,直到運行的時候纔會暴露出。
Javabeans 模式
咱們還有一種選擇,使用 Javabeans 模式 。
在此模式中,咱們提供一個 無參構造器 建立實例,而後利用 setXXX
方法,設置每個必須的屬性和每個須要的可選屬性。
示例代碼:
// JavaBeans Pattern - allows inconsistency, mandates mutability public class NutritionFacts { // Parameters initialized to default values (if any) private int servingSize = -1; // Required; no default value private int servings = -1; // Required; no default value private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public NutritionFacts() { } // Setters public void setServingSize(int val) { servingSize = val; } public void setServings(int val) { servings = val; } public void setCalories(int val) { calories = val; } public void setFat(int val) { fat = val; } public void setSodium(int val) { sodium = val; } public void setCarbohydrate(int val) { carbohydrate = val; } }
Javabeans
模式,沒有 重疊構造器模式 的缺點,對於冗長的參數,使用它建立對象,會很容易,同時讀起來也是容易。正以下面看到的,咱們能夠清晰的看到,每個屬性的值。
NutritionFacts cocaCola = new NutritionFacts(); cocaCola.setServingSize(240); cocaCola.setServings(8); cocaCola.setCalories(100); cocaCola.setSodium(35); cocaCola.setCarbohydrate(27);
不幸運的是,Javabeans模式 自己有着嚴重的缺點:由於,建立對象被分割爲多個步驟,先是利用無參構造器建立對象,而後再依次設置屬性。這致使一個問題: Javabean 在其建立過程當中,可能處於不一致1的狀態。 類不能經過檢查構造器的參數,來保證對象的一致性。
另一個缺點是,將建立一個可變的類的難度提升了好幾個級別,由於有 setXXX
方法的存在。
能夠經過一些手段來減小不一致的問題,經過一些手段 凍結 對象,在對象被建立完成以前。而且不容許使用該對象,直到 解凍 。可是這種方式很是笨拙,在實踐中不多使用。由於,編譯器沒法確認程序員在使用一個對象以前,該對象是否已經 解凍 。
Builder
模式
幸運的是,這裏還有一種方法 Builder
模式,兼顧 重疊構造器 的安全以及 Javabean模式 的可讀性。
客戶端先經過調用構造器或者靜態工廠方法,傳入必須的參數,得到一個 builder
對象,代替直接獲取目標對象。而後客戶端在該 builder
對象上調用 setXXX
方法,爲每個感興趣的可選屬性賦值,最後客戶端調用一個 無參構造器 生成最終的目標對象,該對象通常是不可變的。其中 Builder
類是目標類的靜態內部類
示例代碼:
// Builder Pattern public class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public static class Builder { // Required parameters private final int servingSize; private final int servings; // Optional parameters - initialized to default values private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; } }
其中 NutritionFacts
類爲不可變類,類的成員變量所有被 final
修飾,參數的默認值被放在一個地方。Builder
類 setXXX
方法返回 Builder
自己,這種寫法,能夠將設置變成一個鏈,一直點下去(fluent APIs
):
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .calories(100) .sodium(35) .carbohydrate(27) .build();
這樣的客戶端代碼,容易編寫,更容易閱讀。
示例代碼中,爲了簡潔,省去了有效性的檢查。通常,爲了儘快的檢查到非法參數,咱們在 builder
的構造器和方法中,對其參數進行檢查。
還須要檢查 build
方法中調用的構造器的多個不可變參數2。此次檢查延遲到 object
中,爲了確保這些不可變參數不受到攻擊,在 builder
將屬性複製到 object
中的時候,再作一次檢查。若是檢驗失敗,則拋出 IllegalArgumentException
異常,異常信息中提示哪些參數不合法。
Bulider
模式很適合類的層次結構。可使用一個 builder
的平行結構,即每個 builder
嵌套在一個對應的類中,抽象類中有抽象的 builder
,具體類中有具體的 builder
。像下面的代碼所示:
// Builder pattern for class hierarchies abstract class Pizza { public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE } final Set<Topping> toppings; abstract static class Builder<T extends Builder<T>> { EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class); public T addTopping(Topping topping) { toppings.add(Objects.requireNonNull(topping)); return self(); } abstract Pizza build(); // Subclasses must override this method to return "this" protected abstract T self(); } Pizza(Builder<?> builder) { toppings = builder.toppings.clone(); // See Item 50 } } class NyPizza extends Pizza { public enum Size {SMALL, MEDIUM, LARGE} private final Size size; public static class Builder extends Pizza.Builder<Builder> { private final Size size; public Builder(Size size) { this.size = Objects.requireNonNull(size); } @Override public NyPizza build() { return new NyPizza(this); } @Override protected Builder self() { return this; } } private NyPizza(Builder builder) { super(builder); size = builder.size; } } class Calzone extends Pizza { private final boolean sauceInside; public static class Builder extends Pizza.Builder<Builder> { private boolean sauceInside = false; // Default public Builder sauceInside() { sauceInside = true; return this; } @Override public Calzone build() { return new Calzone(this); } @Override protected Builder self() { return this; } } private Calzone(Builder builder) { super(builder); sauceInside = builder.sauceInside; } }
注意,這裏的 Pizza.Builder
是類屬性,被 static
修飾的,而且泛型參數,是一個 遞歸 的泛型參數,繼承自己。和返回自身的抽象方法 self
,搭配一塊兒,能夠鏈式的調用下去,不須要進行類型的轉換,這樣作的緣由是,java
不直接支持 自類型 3,能夠模擬自類型 4。
若是不使用模擬自類型的話,調用 addTopping
方法,返回的其實就是抽象類中的 Builder
,這樣就致使沒法調用子類擴展方法,沒法使用 fluent APIS
。其中 build
方法,使用了 1.5
添加的 協變類型 ,它能夠不用 cast
轉換,就直接使用具體的類型,不然子類接收父類,是須要強轉的 。
builder
模式另一個小優勢:builder
能夠有多個 可變參數,由於,能夠將多個可變參數,放到各自對應的方法中5。另外 build
能夠將多個參數合併到一個字段上,就如上面代碼中 addTopping
的那樣6。
builder
模式是很是靈活的。一個單一的 builder
屢次調用,能夠建立出不一樣的對象7。builder
的參數,能夠在調用 build
方法的時候進行細微調整,以便修改建立出的對象8。builder
模式還能夠自動的填充 object
域的字段在建立對象的時候。好比爲每一個新建立的對象設置編號,只須要在 builder
中維護一個類變量便可。
builder
模式也是有缺點的。爲了建立一個對象,咱們首先須要建立它的 builder
對象。雖然,建立 builder
對象的開銷,在實踐中不是很明顯,可是在對性能要求很嚴格的場景下,這種開銷能會成爲一個問題。同時,builder
模式是很是冗雜的,對於比 重疊構造器 ,因此,builder
模式應該僅僅被用在構造器參數足夠多的狀況下,好比三個、四個或者更多,只有這樣,使用 builder
模式纔是值得的。可是,你要時刻記住,類在未來可能會添加新的參數,若是你一開始使用了構造器或者靜態工廠方法,隨着類的變化,類的屬性參數變得足夠多,這時候你再切換到 builder
模式,那麼一開始的構造器和靜態工廠方法就會被廢棄,這些廢棄的方法看起來很凸出,你還不能刪除它們,須要保存兼容性。所以,通常一開始就選擇 builder
模式是一個不錯的選擇。
總結,builder
模式是一個好的選擇,當設計一個類的時候,該類的構造器參數或者靜態工廠參數不止幾個參數,尤爲是許多參數是可選的或者同一個類型(可變參數)。這樣設計的類,客戶端代碼,與靜態工廠方法和重疊構造器比起來更加容易閱讀和編寫,和 Javabeans
模式比起來更加安全。
不一致的意思:正常對象的建立應該是一個完整的過程,這個過程控制在構造器中,能夠看作是一個 原子性 的操做。它在對象建立出來之後,對象的各項屬性已經被正確的初始化。可是 Javabean 模式,天生的背棄了這個原則,它的建立對象,不是一個 原子性 的操做,在構造器執行完畢之後,還有一些列的屬性賦值,在這期間任何引用該對象的地方,都將得到一個不正確的對象,直到對象建立完畢。能夠參考下 JavaBean disadvantage - inconsistent during construction 這裏還提到了重複錯誤對象的建立。↩
我理解爲構造器所在類的不可變屬性,在 builder
中的檢查相似於前臺頁面字段的合法性檢查,最後後臺(Object
)都要再次檢查一遍。↩
自類型 。在支持自類型的語言中,this
或者 self
的語義,誰調用該方法,則 this
表明誰。可是在 java
中,方法中的 this
指代的是定義該方法的類型,與調用無關,致使沒法很好的使用 fluent API
。可參考 java 的 self 類型。你能夠驗證下,打印控制檯看,類型確實是調用它的類型,可是你等號左邊用這個類型去接收,會提示你發現父類型,不能賦值給子類型,不知道 java
在這裏面作了什麼。↩
模擬自類型,這裏在抽象類中,使用泛型指定,避免使用指定的類型,致使 this
被綁定爲具體的。↩
構造器在建立對象的時候,構造器和普通方法同樣,只能接受一個 可變參數 。可是 builder
模式,能夠屢次調用不一樣的方法,添加 可變參數,直到全部的可變參數所有添加完畢,再 build
建立對象。↩
一樣的,構造器沒法作的緣由是,構造器一經調用,對象就會被建立,也就是建立對象的過程當中,只能夠調用一次構造器。可是 builder
模式能夠屢次調用方法,設置參數,直到最後所有添加完畢,調用 build
建立對象。↩
仍是由於 builder
模式,只有在調用 build
方法,對象纔會被建立,在建立以前,能夠在調用 builder
模式的方法,修改參數,建立出不一樣的對象。 能夠參考下 StackOverflow
的回答: A single builder can be used repeatedly to build multiple objects↩
仍是由於 builder
模式,只有在調用 build
方法,對象纔會被建立,在建立以前,能夠在調用 builder
模式的方法,修改參數,建立出不一樣的對象。 能夠參考下 StackOverflow
的回答: A single builder can be used repeatedly to build multiple objects↩