用靜态工廠方法代替構造器
類可以提供一個靜态工廠方法(static factory method) :一個傳回類的執行個體的靜态方法。
下面是來自
Boolean
的簡單示例:
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
注意這與工廠方法設計模式不同
靜态工廠方法與構造器不同的第一大優勢有:
-
它們有名稱。
産生的用戶端代碼也更容易閱讀。當一個類需要多個帶有相同簽名的構造器時,就用靜态工廠方法代替構造器,并仔細地選擇名稱以便突出靜态工廠方法之間的差別。
-
不必每次調用它們的時候都建立一個新對象。
這使得不可變類可以使用預先建構好的執行個體,或将建構好的執行個體緩存起來,可以重複利用,進而避免建立不必要的對象。
Boolean.valueOf(boolean)
方法說明了這項技術:它從來不建立對象。這種方法類似于享元(Flyweight)模式。如果程式經常請求建立相同的對象,并且建立對象的代價很高,則這項技術可以極大地提高性能。
靜态工廠方法還能夠為重複的調用傳回相同對象,這樣能控制在某個時刻哪些執行個體應該存在。
-
它們可以傳回原類型的任何子類對象。
這樣在選擇傳回對象的類時就有了更大的靈活性。
-
所傳回的對象的類可以随着每次調用而發生變化,這取決于靜态工廠方法的參數值。
隻要是已聲明的傳回類型的子類型,都是允許的。
- 方法傳回的對象所屬的類,在編寫包含該靜态工廠方法的類時可以不存在。
靜态工廠方法的缺點有:
- 類如果不含有公有的或受保護的構造器,就不能被子類化。
-
程式員很難發現它們。
它們不像構造器那樣在API文檔中明确辨別出來。
下面是靜态工廠方法的一些慣用名稱:
-
——類型轉換方法,它隻有單個參數,傳回該類型的一個相對應的執行個體:from
Date d = Date.from(instant);
-
——聚合方法,帶有多個參數,傳回該類型的一個執行個體,把它們合并起來:of
Set<Rank> faceCards = EnumSet.of(JACK,QUEEN,KING);
-
——比valueOf
和from
更煩瑣的一種替代方法:of
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
-
或instance
——傳回的執行個體是通過方法的參數來描述的,但不能說與參數具有同樣的值:getInstance
StackWalker luke = StackWalker.getInstance(options);
-
或create
——每次調用都能傳回一個新的執行個體:newInstance
Object newArray = Array.newInstance(classObject,arrayLen);
- getType——像
一樣,但是在工廠方法處于不同的類中的時候使用。getInstance
表示工廠方法所傳回的對象類型:Type
FileStore fs = Files.getFileStore(path);
- newType——像
一樣,但是在工廠方法處于不同的類種的時候使用。newInstance
表示工廠方法所傳回的對象類型:Type
BufferedReader bf = Files.newBufferedReader(path);
-
——getType和newType的簡版:type
List<Complaint> litany = Collections.list(legacyLitany);
總之,靜态工廠方法和公有構造器都各有用處,我們需要了解它們各自的長處。切忌第一反應就是提供公有的構造器,應該考慮靜态工廠。
遇到多個構造器參數時要考慮使用建構器
靜态工廠和構造器有個共同的局限性:它們都不能很好地擴充到大量的可選參數。
比如用一個類表示包裝食品外面顯示的營養成分标簽。這些标簽中有幾個屬性是必需的:每份的含量、每罐的含量以及每份的卡路裡。這有超過20多個可選屬性:總脂肪量、膽固醇等等。
對于這樣的類,應該用哪種構造器或靜态工廠來編寫呢?通常想到的是重疊構造器模式,在這種模式下,提供的第一個構造器隻有必要的參數,第二個構造器有一個可選參數,第三個構造器有兩個可選參數,依此類推,最後一個構造器包含所有的可選參數。 這樣當可選參數很可觀的時候,代碼會很難編寫,同時難以閱讀。
還有第二種代替方法,
JavaBeans
模式,在這種模式下,先調用一個無參構造器來建立對象,然後再調用
setter
方法來設定每個必要的參數,以及每個相關的可選參數:
這個我要通過代碼來描述一下,因為我在工作中遇到了這種情況。
package com.java.effective.createobject;
/**
* @Author: Yinjingwei
* @Date: 2019/5/22/022 23:09
* @Description:
*/
public class NutritionFacts {
/**
* 每份的含量
*/
private int servingSize = -1;//必要參數;非預設值
/**
* 每罐的含量(每罐含有多少份)
*/
private int servings = -1;//必要參數;非預設值
/**
* 每份的卡路裡
*/
private int calories = 0;
/**
* 總脂肪量
*/
private int fat = 0;
/**
* 鈉
*/
private int sodium = 0;
/**
* 含糖量
*/
private int carbohydrate = 0;
public NutritionFacts(){}
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}
public void setServings(int servings) {
this.servings = servings;
}
public void setCalories(int calories) {
this.calories = calories;
}
public void setFat(int fat) {
this.fat = fat;
}
public void setSodium(int sodium) {
this.sodium = sodium;
}
public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
}
這種模式彌補了重疊構造器模式的不足,建立執行個體很容易,可讀性也OK:
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
但是,該模式有很嚴重的缺點。因為構造過程被分到了幾個調用中,在構造過程中
JavaBean
可能處于不一緻的狀态。另外,該模式不可能把類做成不可變的。
幸運的是,有第三種替代方案——建造者(Builder)模式。它不直接生成想要的對象,而是讓用戶端利用所有必要的參數調用構造器(或靜态工廠),得到一個
builder
對象。然後用戶端在
builder
對象上調用類似于
setter
方法來設定每個相關的可選參數。最後調用無參的
build
方法來生成通常不可變的對象。這個
builder
通常是它建構的類的靜态成員類。
package com.java.effective.createobject;
/**
* @Author: Yinjingwei
* @Date: 2019/5/22/022 23:09
* @Description:
*/
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;
private NutritionFacts(Builder builder) {
servings = builder.servings;
servingSize = builder.servingSize;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
public static class Builder {
//必要參數 加上final 使得必須要在構造函數中指派
private final int servingSize;
private final int servings;
//可選參數
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize,int servings) {
this.servings = servings;
this.servingSize = servingSize;
}
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);
}
}
}
注意到
NutritionFacts
本身是不可變的。
用戶端代碼:
= new Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();
Builder模式也适用于類層次結構。
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();
//子類必須覆寫該方法傳回 this
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
然後又兩個具體類型的披薩
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 sauceInsize;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false;
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);
sauceInsize = builder.sauceInside;
}
}
每個子類的建構器中的
build
方法,都聲明傳回正确的子類。子類方法聲明傳回超類中聲明的傳回類型的子類型,這被稱為協變傳回類型(covariant return type)
這些階層化建構器的用戶端代碼本質上與簡單的建構器一樣:
NyPizza pizza = new NyPizza.Builder(NyPizza.Size.SMALL)
.addTopping(Topping.SAUSAGE).addTopping(Topping.ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(Topping.HAM).sauceInside().build();
如果類的構造器或者靜态工廠中具有多個(超過5個吧)參數,設計這種類時,Builder模式就是一個不錯的選擇
另外,lombok插件的 @Builder
注解可以了解一下
用私有構造器或枚舉類型強化Singleton屬性
**Singleton(單例)**是指僅僅被執行個體化一次的類。通常用來代表一個無狀态(也就是無屬性)的對象。
實作Singleton有兩種常見的方法。這兩種方法都要保持構造器為私有的,并導出公有的靜态成員,使得用戶端能通路該類的唯一執行個體。
在第一種方法中,公有靜态成員是個
final
屬性:
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {
...
}
public void leaveTheBuilding(){...}
}
一旦
Elvis
類被執行個體化,将隻會存在一個
Elvis
執行個體。但是可以通過反射機制調用私有構造器。如果要防止這種攻擊,可以修改構造器,讓它在被要求建立第二個執行個體的是抛出異常。
在第二種方法中,公有的靜态成員是個靜态工廠方法:
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {...}
public static Elvis getInstance() {
return INSTANCE;
}
public void leaveTheBuilding(){...}
}
公有屬性實作的單例的主要優勢在于,API很清楚的表明了這個類是一個單例,第二個優勢在于它更簡單。
靜态工廠方法的優勢之一在于,它提供了靈活性:在不改變API的前提下,我們可以改變該類是否為單例的想法。第二個優勢在于,如果需要,可以編寫一個泛型的單例工廠。最後可以通過方法引用作為
Supplier
,比如
Elvis::instance
就是一個
Supplier<Elvis>
。除非滿足以上任意一種優勢,否則優先考慮第一種方法。
為了将利用上述方法實作的單例類變成可序列化的,僅僅在聲明中加上
implements Serializable
是不夠的。為了維護并保證單例,必須聲明所有執行個體都是
transient
,并提供一個
readResolve
方法。否則,每次反序列化一個序列化的執行個體時,都會建立一個新的執行個體。
private Object readResolve(){
return INSTANCE;
}
實作單例模式的第三種方法是聲明一個包含單個元素的枚舉類型:
public enum Elvis {
INSTANCE;
public void leaveTheBuilding(){...}
}
單元素的枚舉類型經常是實作單例的最佳方法。
通過私有構造器強化不可執行個體化的能力
我們編寫的工具類不想被别人執行個體化,該怎麼做呢?
由于隻有當類不包含顯示的構造器時,編譯器才會生成預設的構造器。是以隻要讓這個類包含一個私有構造器,它就不能被執行個體化。
public class UtilityClass {
private UtilityClass() {
}
//....
}
這種習慣用法也有副作用,它使得一個類不能被子類化。所有的構造器都必須顯示或隐式地調用超類構造器,在這種情況下,子類就沒有可通路的超類構造器可調用了。
優先考慮依賴注入來引用資源
有許多類會依賴一個或多個底層的資源。例如,拼寫檢查器需要依賴詞典。是以,像下面這樣把類實作為靜态工具類的做法并不少見:
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker(){} //不可執行個體化
public boolean isValid(String word){...}
public List<String> suggestions(String typo){...}
}
同樣,也将這些類實作為單例:
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker(...){}
public static INSTANCE = new SpellChecker(...);
public boolean isValid(String word){...}
public List<String> suggestions(String typo){...}
}
以上兩種方法都不理想,因為它們都是假定隻有一本詞典可用。但是可能需要用特殊的詞典進行測試。假定隻有一本詞典就能滿足所有需求是不可能的。
由此可見,靜态工具類和單例類不适合于需要引用底層資源的類。因為一般需要支援多個底層資源執行個體。
滿足該需求的最簡單的模式是,當建立一個新的執行個體時,就将資源傳到構造器中。這就是依賴注入的一種形式:
詞典是拼寫檢查器的一個依賴,當建立拼寫檢查器時就将詞典注入其中。
public class SpellChecker {
private static final Lexicon dictionary;
public SpellChecker(Lexicon dictionary){
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word){...}
public List<String> suggestions(String typo){...}
}
将不同的詞典注入(傳入)到構造函數中,就能執行個體化出不同的拼寫檢查器。
這種方式極大地提升了類的靈活性、可重用性和可測試性。
避免建立不必要的對象
一般來說,最好能重用單個對象,而不是在每次需要的時候就建立一個相同功能的新對象。
如果對象是不可變的,它就始終可以被重用。
String s = new String("hello");
該語句在每次诶執行的時候都會建立一個新的
String
執行個體。如果這種用法是在一個循環中,或在一個被頻繁調用的方法中,就會建立成千上萬個不必要的
String
執行個體。
改進後的版本:
String s = "hello";
對于同時提供了靜态工廠方法和構造器的不可變對象類,通常優先使用靜态工廠而不是構造器,以避免建立不必要的對象。例如,靜态工廠方法
Boolean.valueOf(String)
幾乎總是優于構造器
Boolean(String)
。構造器在每次被調用的時候都會建立一個新的對象,而靜态工廠方法則從來不要求這麼做,實際上也不會這麼做。
有些對象建立的成本比其他對象要高得多。如果重複地需要這類昂貴的對象,建議将它緩存下來重用。
遺憾的是,在建立這種對象的時候,并非總是那麼顯而易見。比如想要編寫一個方法,用來測試一個字元串是否為一個有效的羅馬數字:
static boolean isRomanNumeral(String s) {
return s.matches("<複雜的正規表達式>");
}
這樣實作的問題在于它依賴
String.matches
方法,但該方法并不适合在注重重用性能的情形中重複使用。它在内部會建立一個
Pattern
執行個體,卻隻用了一次,之後就進行垃圾回收了。而建立該執行個體的成本很高。為了提升性能,應該顯示地将正規表達式編譯成一個
Pattern
執行個體(不可變),讓它成為類初始化的一部分,并将它緩存起來。每次調用判斷方法時就重用同一個執行個體:
private static final Pattern ROMAN = Pattern.compile("<複雜的正規表達式>");
static boolean isRomanNumeral(String s) {
return ROMAN.machers(s).matches();
}
如果一個對象是不可變的,那麼它顯然能夠被安全地重用,但其他情形并不總是這麼明顯。
考慮擴充卡的情形,有時也叫視圖(view)。它把功能委托給一個支撐對象(backing object),進而為支撐對象提供一個可以替代的接口。由于擴充卡除了支撐對象之外,沒有其他的狀态資訊,是以針對某個給定對象的特定擴充卡而言,它不需要建立多個擴充卡執行個體。
例如,
Map
接口的
keySet
方法傳回該對象的
Set
視圖,其中包含該對象中所有的鍵。好像每次調用
keySet
都應該建立一個新的
Set
執行個體,但是,對于一個給定的
Map
對象,實際上每次調用
keySet
都傳回同樣的
Set
執行個體。雖然被傳回的
Set
執行個體一般是可改變的,但是所有傳回的對象哎功能上都是等同的:當其中一個傳回對象發生變化時,所有其他的傳回對象也要發生變化,因為它們是由同一個
Map
執行個體支撐的。
這是内部類的一個展現:内部類與建立它的外部類執行個體有聯系。從下面的代碼可以展現出來,最後輸出
set1
,能輸出3個鍵,說明是與外部類有聯系的。
HashMap<String,String> map = new HashMap<>();
map.put("A","a");
map.put("B","b");
Set<String> set1 = map.keySet();
Set<String> set2 = map.keySet();
System.out.println(set1 == set2);//true 其實内部傳回的是同一個set對象
System.out.println(set1);
map.put("C","c");
System.out.println(set1);//A,B,C
另外一種建立多餘對象的方法,稱作自動裝箱。自動裝箱會有所消耗,如果在頻繁需要用到自動裝箱的情況下,要優先使用基本類型而不是裝箱基本類型,要當心無意識的自動裝箱。
消除過期的對象引用
看下面這個簡單實的棧實作的例子:
package com.java.effective.createobject;
import java.util.Arrays;
import java.util.EmptyStackException;
/**
* @Author: Yinjingwei
* @Date: 2019/5/29/029 21:38
* @Description:
*/
class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
這段程式有一個記憶體洩露。如果一個棧先增長,再彈出元素,那麼從棧中彈出的對象将不會被當做垃圾回收,即使棧的程式不再引用這些對象,它們也不會被 回收。
這是因為棧内部維護着這些對象的過期引用(obsolete reference)——指永遠不會再被解除的引用。在本例中,凡是在
elements
數組的活動部分之外的任何引用都是過期的。活動部分指數組下标小于
size
的那些元素。也就是大于
size
那部分元素時過期引用,除非再次執行壓棧操作。
這類問題的修複方法很簡單:一旦對象引用已經過期,隻需清空這些引用即可。對于上述例子,隻要一個元素被彈出來,指向它的引用就過期了。
pop()
方法的修改版如下:
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
//size指向的是存在元素的後一個位置,--size可以同時将大小減1和指向棧頂元素
Object result = elements[--size];
elements[size] = null;
return result;
}
記憶體洩露的另一個常見的來源是緩存。對于這個問題,可以這樣:隻要在緩存之外存在對某個元素的鍵的引用,該元素就有意義,那麼就可以用
WeakHashMap
代表緩存;當緩存中的元素過期之後,它們就會自動被删除。**隻有當所要的緩存元素的生命周期是由該鍵的外部引用而不是由值決定時,
WeakHashMap
才有用處。
記憶體洩露的第三個常見來源是監聽器和其他回調。如果你實作了一個API,用戶端在這個API中注冊回調,卻沒有顯示地取消注冊,那麼除非你采取某些動作,否則它們就會不斷地堆積起來。確定回調立即被當做垃圾回收的最佳方法是隻儲存它們的弱引用。
避免使用終結方法和清除方法
終結方法通常是不可預測的,也是很危險的,一般情況下是不必要的。Java9中的清除方法雖然沒有那麼危險,但仍然是不可預測、運作緩慢,一般情況下也是不必要的。
這兩個方法的缺點在于不能保證被及時執行。注重實踐的任務不應該由終結方法或清除方法來完成。
永遠不應該依賴終結方法或清除方法來更新重要的持久狀态。
使用終結方法的另一個問題是:如果忽略在終結過程中被跑出來的未被捕獲的異常,該對象的終結過程也會終止。
那麼終結方法和清除方法有什麼好處呢?它們有兩種合法用途。當資源的所有者忘記調用它的
close()
方法是,終結方法可以充當安全網。第二種用途與對象的本地對等體有關。本地對等體是一個本地對象(非Java對象),普通對象通過本地方法委托給一個本地對象。如果本地對等體沒有關鍵資源,并且性能也可以接受的話,那麼清楚方法正是執行這項任務最合适的工具。
try-with-resoures優于try-finally
以前,
try-finally
語句時確定資源會被适時關閉的最佳方法,就算發生異常或者傳回也一樣:
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
但是如果有多個資源,或
br.close
中也抛出受檢異常,那麼代碼就會很"ugly"
幸好,Java7引入了
try-with-resource
。但是使用它的資源要實作
AutoCloseable
接口。
static String firstLineOfFile(String path) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
當有多個資源時:
static void copy(String src,String dst) throws IOException {
try(InputStream in = new FileInputStream(src));OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while((n = in.read(buf)) >= 0) {
out.write(buf,0,n);
}
}
}
static String firstLineOfFile(String path,String defaultVal) {
try(BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}catch(IOException e) {
return defaultVal;
}
}