本文主題是創建和銷毀對象,關注一下幾個問題:
- 何時以及如何創建對象
- 何時以及如何避免創建對象
- 如何去報它們能夠適時銷毀
- 如何管理對象銷毀之前必須進行的各種清理動作
1.考慮使用靜態工廠方法代替構造器(靜態工廠模式)
創建類實例的方式有兩種:
- 公有的構造器
- 公有的靜態工廠方法
靜態工廠方法
-
優勢
- 靜態工廠方法與構造器不同的第一大優勢在于,它們有名稱,如果構造器的參數本身沒有確切的描述返回的對象,那么適當名稱的靜態工廠會更加合適
- 不必每次調用它們的時候都創建一個新對象,使得不可變對象可以使用預先構建好的實例,利用緩存實例進行復用,為重復的調用返回相同的對象,如果創建對象的代價很高,這個技術可以極大提升性能
- 可以返回類型的任何子類型的對象,在選擇返回對象時有更大的靈活性。
- 可以返回非公有對象,同時又不會使對象的類變成公有的,隱藏實現類
- 公有的靜態工廠方法所返回對象的類不僅可以是非公有的,而且該類可以對著每次調用而發生變化,取決于靜態工廠方法的參數值(工廠方法模式)
- 創建參數化類型(泛型)實例時,使代碼變得更加簡潔
-
缺點
- 類如果不含有公有或者受保護構造器,就不能被子類化
- 與其他的靜態方法實際上沒有任何區別
-
靜態工廠方法命名規范
- valueOf
- 該方法返回的實例與它的參數具有相同的值,這樣的靜態工廠方法實際上是類型轉換方法
- of
- getInstance
- 返回的實例通過方法的參數來描述,如果沒有參數,則返回唯一的單例
- newInstance
- 確保返回的實例都與其他實例不同
- getType
- newType
- valueOf
2.遇到多個構造器參數時要考慮用構建器(Builder創建者模式)
靜態工廠和構造器有個共同的局限性,不能很好地擴展到大量可選參數。
處理有大量可選參數的構造器的方式:
- 重疊構造器
- JavaBeans 模式
- Builder 模式
重疊構造器
提供第一個只有必要參數的構造器,第二個構造器有一個可選參數,第三個有兩個可選參數,以此類推,最后一個構造器包含所有可選參數。
public NutritionFact(int servingSize, int servings){}
public NutritionFact(int servingSize, int servings, int calories){}
public NutritionFact(int servingSize, int servings, int calories, int fat){}
public NutritionFact(int servingSize, int servings, int calories, int fat, int sodium){}
...
缺點:重疊構造器模式可行,但是當有許多參數的時候,客戶端代碼會很難編寫和難以閱讀
JavaBeans 模式
另一種替代方法,JavaBeans 模式,調用一個無參構造器來創建對象,然后調用setter方法設置每個必要參數,以及每個相關的可選參數。
NutritionFact cocoCola = new NutritionFact();
cocoCola.setServingSize(240);
cocoCola.setServings(8);
cocoCola.setCalories(100);
cocoCola.setSodium(35);
cocoCola.setCarbohydrate(27);
缺點:
- 構造過程被分到幾個調用中,構造過程 JavaBean 可能處于不一致的狀態。類無法僅僅通過校驗構造器參數的有效性來保證一致性
- JavaBean 模式阻止了把類做成不可變的可能,需要確保它的線程安全
Builder 模式
不直接生成想要的對象,客戶端利用多有必要的參數調用構造器(或靜態工廠)得到一個builder對象,然后客戶端再builder 對象上調用類似setter方法,來設置每個相關的可選參數,最后客戶端調用無參的build方法來生成不可變的對象,這個builder是類的靜態成員類。
public class NutritionFacts {
private final int calories = 0;
private final int fat = 0;
private final int sodium = 0;
// 靜態內部類 Builder 對象
public static class Builder {
private int calories = 0;
private int fat = 0;
private int sodium = 0;
// setter方法返回當前builder對象,方便鏈式調用
public Builder setCalories(int val) {
calories = val;
}
public Builder setFat(int val) {
fat = val;
}
public Builder setSodium(int val) {
sodium = val;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
// 傳入 Builder 對象的構造方法
public NutritionFacts(Builder builder) {
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
}
}
缺點:需要額外開銷
總結
如果類的構造器或者靜態工廠中具有多個參數,設計這種類時,Builder 模式就是不錯的選擇。
3.用私有構造器或者枚舉類型強化Singleton屬性(單例模式)
Singleton 指僅僅被實例化一次的類。
實現Singleton的方式有很多種
方式一
把構造器保持為私有的,并導出公有的靜態成員,并且靜態成員是個final的
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {...};
}
問題:無法抵御通過反射調用私有構造器的攻擊。
方案:可以修改構造器,讓它在要求創建第二個實例的時候拋出異常。
方式二
方式二中,公有成員不再是屬性,而是一個靜態方法getInstance
public class Elvis {
pvivate static final Elvis INSTANCE = new Elvis();
private Elvis() {...};
public static Elvis getInstance() {return INSTANCE;}
}
問題:如果此類實現了序列化,序列化之后的結果都會創建一個新的實例。
方案:重寫readResolve方法
private Object readResolve() {
return INSTANCE;
}
方式三
編寫一個包含單個元素的枚舉類型。
public enum Elvis {
INSTANCE;
}
與公有方法相近,但更加簡潔,無償的提供了序列化機制,并且防止序列到導致多次實例化,并且防止反射的攻擊。
總結
單元素的枚舉類型已經成為實現Singleton的最佳方法。
4.通過私有構造器強化不可實例化的能力
工具類不希望被實例化,實例對它沒有任何意義。
在缺少顯示構造器時,編譯器會自動提供一個公有的,無參的缺省構造器。
可通過創建私有構造器,并構造器中拋出異常,來避免實例化此類。
5.避免創建不必要的對象
一般來說,最好能重用對象而不是在每次需要的時候就創建一個相同功能的新對象。如果對象是不可變的,那么它就應該始終被重用。
舉例一
String s = new String("mystring") // 每次都會創建一個新的String實例
String s = "mystring" // 推薦,保證在同一臺虛擬機中運行的代碼,只要包含相同的字符串字面常量,就會被重用
舉例二:不可變類
對于不可變類,優先使用靜態工廠方法,每次調用可以重用,避免創建不必要的對象。
舉例三
通過靜態初始化器避免在每次調用方法時都會生成一些不必要的對象。
class Person {
static {
// 初始化整個類需要用到的不可變可重用對象
}
public boolean isBaby() {
// 這里使用到一些不可變的對象,無需每次都創建,把創建操作放到靜態初始化器中,這里直接使用即可
}
}
缺點:如果方法沒有被調用,那么初始化工作就沒有必要,可以通過延遲初始化,即把初始化工作放到方法初次調用時。
舉例四
自動裝箱會創建出多余的對象。
sum 聲明為 Long 類型,導致循環內部構造大量大于 Long 實例。
Long sum = 0;
for (long i = 0;i < Interger.MAX_VALUE; i ++) {
sum += i;
}
要優先使用基本類型而不是裝箱基本類型,要當心無意識的自動裝箱。
舉例五
通過維護自己的對象池來避免創建對象并不會一種好的做法,除非池中的對象是非常重量級的,現代JVM實現具有高度優化的垃圾回收器,其性能很容易就超過輕量級對象池的性能。
6.消除過期的對象引用
- 只要類自己管理內存,程序就應該警惕內存泄漏問題
- 內存泄漏另一個場景來源是緩存
- 另一個場景是監聽器和其他回調,如果注冊了回調,卻沒有顯式地取消注冊,那么會產生內存泄漏,確保回調立即被當做垃圾回收的最佳方法是只保持它們的弱引用。
7.避免使用終結方法
- 介紹
- 終結方法(finalizer)通常是不可預測的,也是很危險的,一般情況下是不必要的
- 使用終結方法導致行為不穩定,降低性能,以及可移植性問題
- C++的析構函數可以被用來回收其他的非內存資源,Java 中,一般用try-finally塊來完成類似工作
- 終結方法的缺點在于不能保證被及時執行,JVM會延遲執行終結方法,所以不要用來關閉已經打開的文件,程序不能依賴終結方法被執行的時間點
- 不應該依賴終結方法來更新重要的持久狀態
- System.gc 和 System.runFinalization 增加了終結方法執行的機會,但不能保證終結方法一定會被執行
- System.runFinalizersOnExit 可以保證終結方法被執行,當然此方法已被廢棄
- 如果被捕獲的異常在終結方法中被拋出,那么這種異常會被忽略
- 使用終結方法有非常嚴重的性能損失,即使什么也不做
- 終止資源(文件或線程資源)
- 顯示提供一個終止方法,在實例不再需要時,調用此方法
- 通常與try-finally結構結合,在finally中顯式調用終止方法