Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。java
靜態工廠和構造方法都有一個限制:它們不能很好地擴展到不少可選參數的情景。請考慮一個表明包裝食品上的養分成分標籤的例子。這些標籤有幾個必需的屬性——每次建議的攝入量,每罐的分量和每份卡路里 ,以及超過20個可選的屬性——總脂肪、飽和脂肪、反式脂肪、膽固醇、鈉等等。大多數產品都有非零值,只有少數幾個可選屬性。git
應該爲這樣的類編寫什麼樣的構造方法或靜態工廠?傳統上,程序員使用了可伸縮(telescoping constructor)構造方法模式,在這種模式中,只提供了一個只所需參數的構造函數,另外一個只有一個可選參數,第三個有兩個可選參數,等等,最終在構造函數中包含全部可選參數。這就是它在實踐中的樣子。爲了簡便起見,只顯示了四個可選屬性:程序員
// 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; } }
當想要建立一個實例時,可使用包含全部要設置的參數的最短參數列表的構造方法:github
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
一般狀況下,這個構造方法的調用須要許多你不想設置的參數,可是你不得不爲它們傳遞一個值。 在這種狀況下,咱們爲fat
屬性傳遞了0值。 『只有』六個參數可能看起來並不那麼糟糕,但隨着參數數量的增長,它會很快失控。安全
簡而言之,可伸縮構造方法模式是有效的,可是當有不少參數時,很難編寫客戶端代碼,並且很難讀懂它。讀者不知道這些值是什麼意思,而且必須仔細地計算參數才能找到答案。一長串相同類型的參數可能會致使一些細微的bug。若是客戶端意外地顛倒了兩個這樣的參數,編譯器並不會抱怨,可是程序在運行時會出現錯誤行爲(條目51)。ide
當在構造方法中遇到許多可選參數時,另外一種選擇是JavaBeans模式,在這種模式中,調用一個無參數的構造函數來建立對象,而後調用setter方法來設置每一個必需的參數和可選參數:函數
// 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; } }
這種模式沒有伸縮構造方法模式的缺點。有點冗長,但建立實例很容易,而且易於閱讀所生成的代碼:性能
NutritionFacts cocaCola = new NutritionFacts(); cocaCola.setServingSize(240); cocaCola.setServings(8); cocaCola.setCalories(100); cocaCola.setSodium(35); cocaCola.setCarbohydrate(27);
不幸的是,JavaBeans模式自己有嚴重的缺陷。因爲構造方法在屢次調用中被分割,因此在構造過程當中JavaBean可能處於不一致的狀態。該類沒有經過檢查構造參數參數的有效性來執行一致性的選項。在不一致的狀態下嘗試使用對象可能會致使與包含bug的代碼截然不同的錯誤,所以很難調試。一個相關的缺點是,JavaBeans模式排除了讓類不可變的可能性(條目17),而且須要在程序員的部分增長工做以確保線程安全。ui
當它的構造完成時,手動「凍結」對象,而且不容許它在解凍以前使用,能夠減小這些缺點,可是這種變體在實踐中很難使用而且不多使用。 並且,在運行時會致使錯誤,由於編譯器沒法確保程序員在使用對象以前調用freeze
方法。this
幸運的是,還有第三種選擇,它結合了可伸縮構造方法模式的安全性和javabean模式的可讀性。 它是Builder模式[Gamma95]的一種形式。客戶端不直接調用所需的對象,而是調用構造方法(或靜態工廠),並使用全部必需的參數,並得到一個builder對象。而後,客戶端調用builder對象的setter
類似方法來設置每一個可選參數。最後,客戶端調用一個無參的build
方法來生成對象,該對象一般是不可變的。Builder一般是它所構建的類的一個靜態成員類(條目24)。如下是它在實踐中的示例:
// 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
類是不可變的,全部的參數默認值都在一個地方。builder的setter方法返回builder自己,這樣調用就能夠被連接起來,從而生成一個流暢的API。下面是客戶端代碼的示例:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .calories(100).sodium(35).carbohydrate(27).build();
這個客戶端代碼很容易編寫,更重要的是易於閱讀。 Builder模式模擬Python和Scala中的命名可選參數。
爲了簡潔起見,省略了有效性檢查。 要儘快檢測無效參數,檢查builder的構造方法和方法中的參數有效性。 在build
方法調用的構造方法中檢查包含多個參數的不變性。爲了確保這些不變性不受攻擊,在從builder複製參數後對對象屬性進行檢查(條目 50)。 若是檢查失敗,則拋出IllegalArgumentException
異常(條目 72),其詳細消息指示哪些參數無效(條目 75)。
Builder模式很是適合類層次結構。 使用平行層次的builder,每一個嵌套在相應的類中。 抽象類有抽象的builder; 具體的類有具體的builder。 例如,考慮表明各類比薩餅的根層次結構的抽象類:
// Builder pattern for class hierarchies import java.util.EnumSet; import java.util.Objects; import java.util.Set; public 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 } }
請注意,Pizza.Builder
是一個帶有遞歸類型參數( recursive type parameter)(條目 30)的泛型類型。 這與抽象的self
方法一塊兒,容許方法鏈在子類中正常工做,而不須要強制轉換。 Java缺少自我類型的這種變通解決方法被稱爲模擬自我類型(simulated self-type)的習慣用法。
這裏有兩個具體的Pizza
的子類,其中一個表明標準的紐約風格的披薩,另外一個是半圓形烤乳酪餡餅。前者有一個所需的尺寸參數,然後者則容許指定醬汁是否應該在裏面或在外面:
import java.util.Objects; public 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; } } public 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; } }
請注意,每一個子類builder中的build
方法被聲明爲返回正確的子類:NyPizza.Builder
的build
方法返回NyPizza
,而Calzone.Builder
中的build
方法返回Calzone
。 這種技術,其一個子類的方法被聲明爲返回在超類中聲明的返回類型的子類型,稱爲協變返回類型( covariant return typing)。 它容許客戶端使用這些builder,而不須要強制轉換。
這些「分層builder」的客戶端代碼基本上與簡單的NutritionFacts
builder的代碼相同。爲了簡潔起見,下面顯示的示例客戶端代碼假設枚舉常量的靜態導入:
NyPizza pizza = new NyPizza.Builder(SMALL) .addTopping(SAUSAGE).addTopping(ONION).build(); Calzone calzone = new Calzone.Builder() .addTopping(HAM).sauceInside().build();
builder對構造方法的一個微小的優點是,builder能夠有多個可變參數,由於每一個參數都是在它本身的方法中指定的。或者,builder能夠將傳遞給多個調用的參數聚合到單個屬性中,如前面的addTopping
方法所演示的那樣。
Builder模式很是靈活。 單個builder能夠重複使用來構建多個對象。 builder的參數能夠在構建方法的調用之間進行調整,以改變建立的對象。 builder能夠在建立對象時自動填充一些屬性,例如每次建立對象時增長的序列號。
Builder模式也有缺點。爲了建立對象,首先必須建立它的builder。雖然建立這個builder的成本在實踐中不太可能被注意到,但在性能關鍵的狀況下可能會出現問題。並且,builder模式比伸縮構造方法模式更冗長,所以只有在有足夠的參數時才值得使用它,好比四個或更多。可是請記住,若是但願在未來添加更多的參數。可是,若是從構造方法或靜態工廠開始,並切換到builder,當類演化到參數數量失控的時候,過期的構造方法或靜態工廠就會面臨尷尬的處境。所以,因此,最好從一開始就建立一個builder。
總而言之,當設計類的構造方法或靜態工廠的參數超過幾個時,Builder模式是一個不錯的選擇,特別是若是許多參數是可選的或相同類型的。客戶端代碼比使用伸縮構造方法(telescoping constructors)更容易讀寫,而且builder比JavaBeans更安全。