EffectiveJava--創建和銷燬對象

[b]本章內容:[/b]
1. 考慮用靜態工廠方法代替構造器
2. 遇到多個構造器參數時要考慮用構建器(Builder模式)
3. 用私有構造器或者枚舉類型強化Singleton屬性
4. 通過私有構造器強化不可實例化的能力
5. 避免創建不必要的對象
6. 消除過期的對象引用
7. 避免使用終結方法

[b]1. 考慮用靜態工廠方法代替構造器[/b]
類可以通過靜態工廠方法來提供它的客戶端,而不是通過構造器。示例如下:
public class Gender{
private String description;
private Gender(String description){this.description=description;}
private static final Gender female=new Gender("女");
private static final Gender male=new Gender("男");

public static Gender getFemale(){
return female;
}
public static Gender getMale(){
return male;
}
public String getDescription(){return description;}
}


[b]優點:[/b]
(1)靜態方法有自己的名字,更加易於理解。
(2)不必在每次調用它們的時候都創建一個新對象。
(3)它可以返回原返回類型的的任何子類型的對象,如返回的父類的私有子類來隱藏實現類、而且返回的對象還可以隨着參數的變化而變化(只要是返回類型的子類型)、而且在編寫該靜態工廠方法時返回類型的子類可以不必存在(如服務提供者框架)。
(4)在創建參數化類型實例的時候代碼變得更加簡潔

[b]缺點:[/b]
(1)靜態工廠方法返回類型如果不含有公有的或者保護的構造器,那該類就不能子類化(有子類,被繼承),子類的構造函數需要首先調用父 類的構造函數,因爲父類的構造函數是private的,所以即使我們假設繼承成功的話,那麼子類也根本沒有權限去調用父類的私有構造函數,所以是無法被繼承的 。
(2)它們與其他的靜態方法實際上沒有任何區別。所以我們要遵守標準和命名習慣:
valueOf--類型轉換方法,該方法返回的實例與它的參數具有同樣的值
of--valueOf的一種更加簡潔的替代
getInstance--返回的實例是通過方法的參數來描述的,對於Singleton來說沒有參數
newInstance--像getInstance一樣,但newInstance能夠確保返回的每個實例都與其他實例不同
getType、newType--Type表示返回的對象類型

[b]2. 遇到多個構造器參數時要考慮用構建器(Builder模式)[/b]
靜態工廠類和構造器有個共同的侷限性,它們都不能很好地擴展到大量的可選參數。解決方法:
(1)重疊構造器模式,爲每個可選參數增加一個構造器。這種方法代碼難以閱讀,而且設置參數時很容易出錯。
(2)JavaBeans模式,用一個無參構造器來創建對象,然後調用setter方法來設置每個需要的參數。但是這種方法在構造過程中被分到幾個調用中,在構造過程中JavaBean可能處於不一致的狀態。JavaBean模式阻止了把類做成不可變的可能,這就需要程序員付出額外的努力來確保它的線程安全。
當對象的構造完成,並且不允許在解凍之前使用時,通過手工凍結對象,可以彌補這些不足,但是無法確保程序員會在使用之前先在對象上調用freeze方法。
(3)Builder模式,既能保證像重疊構造器模式那樣的安全性,也能保證像JavaBeans模式那麼好的友好性。示例如下:
Public class A{
private final int a;
private final int b;
public static class Builder{
private final int a;
private final int b;
// 必選參數
public Builder(int a){
this.a=a;
}
public Builder b(int v){
b = v;
return this;
}
public A build(){
return new A(this);
}
}
private A(Builder builder){
a = builder.a;
b = builder.b;
}
}

//使用
A o = new A.Builder(10000).b(50000).build();


Builder模式的確也有它自身的不足,爲了創建對象,必須先創建它的構建器。雖然構建器的開銷在實踐中可能不那麼明顯,但是在某些十分注重性能的情況下,可能就成問題了。

[b]3. 用私有構造器或者枚舉類型強化Singleton屬性[/b]
Singleton指僅僅被實例化一次的類,用來代表那些本質上唯一的系統組件,比如窗口管理器或者文件系統。
在Java1.5之前,實現Singleton有以下兩種方法:
方法一:
public class A{
public static final A a = new A();
private A(){...};
...
}
公有靜態成員是個final域,私有構造器僅被調用一次,用來實例化公有的靜態final域a。由於缺少公有的或者受保護的構造器,保證A只存在一個實例。但要注意一點,享有特權的客戶端可以藉助AccessibleObject.setAccessible方法,通過反射機制調用私有構造器,如果要抵禦這種攻擊,可以修改構造器,讓它在被要求創建第二個實例的時候拋出異常。

方法二:
public class A{
private static final A a = new A();
private A(){...};
public static A getInstance(){ return a; };
...
}
公有的成員是個靜態工廠方法,對於靜態方法A.getInstance的所有調用,都會返回同一個對象引用,所有也只有一個實例。注意同樣存在上面通過反射機制調用私有構造器的問題。

爲了使上面兩種方法實現的Singleton類變成可序列化的,僅僅在聲明中加上impliments Serializable是不夠的。爲了維護並保證Singleton,必須聲明所有實例域都是瞬時(transient)的,交提供一個readResolre方法,否則每次反序列化一個序列化的實例時,都會創建一個新的實例。
private Object readResolve(){
...
return a;
}

在Java1.5起,實現Singleton有了第三種方法
方法三:
public enum A{
INSTANCE;
}
這種方法只需編寫一個包含單個元素的枚舉類型,在功能上與公有域方法相近,但是它更加簡潔,無償地提供序列化機制,絕對防止多次實例化,即使是在面對複雜的序列化或者反射攻擊的時候。單元素的枚舉類型已經成爲實現Singleton的最佳方法。

[b]4. 通過私有構造器強化不可實例化的能力[/b]
有些工具類(utility class)不希望被實例化,實例化對它沒有任何意義。然而在缺少顯式構造器的情況下,編譯器會自動提供一個公有的、無參的缺少構造器,對用戶而言,這個構造器與其他的構造器沒有任何區別。
企圖通過將類做成抽象類來強制該類不可被實例化也是行不通的,該類可以被子類化,並且該子類也可以被實例化。這樣做甚至會誤導用戶,以爲這種類是專門爲了繼承而設計的。
由於只有當類不包含顯式的構造器時,編譯器纔會生成缺少的構造器,所以:
public class A{
private A(){
throw new AssertionError(); //不是必須,但可以避免不小心在類的內部調用構造器
}
...
}
這種用法也有副作用,它使得一個類不能被子類化。

[b]5. 避免創建不必要的對象[/b]
一般來說,最好能重用對象而不是在每次需要的時候就創建一個相同功能的新對象。如果對象是不可變的,它就始終可以被重用。

String s = new String("111"); //該語句每次被執行的時候都創建一個新的String實例,但是這些創建對象的動作全都是不必要的。
String s = "111"; //這個版本只存在一個String實例,而不是每次執行的時候都創建一個新的實例。而且對於所有在同一臺虛擬機中運行的代碼,只要它們包含相同的字符串字面常量,該對象就會被重用。

除了重用不可變的對象之外,也可以重用那些已知不會被修改的可變對象,如下:
    public class A{
private final Data birthData;
public boolean is BabyBoomer(){
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Data start = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Data end = gmtCal.getTime();
return birthData.compareTo(start) >=0 && birthDate.compareTo(end) < 0;
}
}

上面方法每次調用都會創建一個Calendar、一個TimeZone和兩個Date實例。改進如下:
    public class A{
private final Data birthData;
private final Data start;
private final Data emd;
static{
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Data start = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Data end = gmtCal.getTime();
}
public boolean is BabyBoomer(){
return birthData.compareTo(start) >=0 && birthDate.compareTo(end) < 0;
}
}

改進後的A類只在初始化的時候創建Calendar、TimeZone、Date實例一次,而不是每次都去創建這些實例(速度大給快了250倍)。而且代碼更容易讓人理解。

要優先使用基本類型而不是裝箱基本類型,防止無意識的自動裝箱創建多餘的對象。
不要錯誤的認爲“創建對象的代價非常昂貴,我們應該要儘可能地避免創建對象”。相反,由於小對象的構造器只做很少量的顯式工作,它的創建和回收是非常廉價的,特別是在現代的JVM實現上更是如此。可以通過創建附加的對象來提升程序的清晰性、簡潔性和功能性。反之,通過維護自己的對象池來避免創建對象並不是一種好的做法,除非池中的對象是非常重量級的,維護自己的對象池必定會把代碼弄得很亂,同時增加內存佔用,並且還會損害性能。現代的JVM實現且有調度優化的垃圾回收器,其性能很容易就會超過輕量級對象池的性能。

[b]6. 消除過期的對象引用[/b]
在支持垃圾回收的語言中,內存泄漏是很隱藏的(稱這類內存泄漏爲無意識的對象保持)。如果一個對象引用被無意識地保留起來了,那麼垃圾回收機制不僅不會處理這個對象,而且也不會處理被這個對象所引用的所有其他對象。從而對性能造成潛在的重大影響。
這類問題的修復方法很簡單:一旦對象引用已經過期,只需清空這些引用即可(o = null;)。清空過期引用的另一個好處是,如果它們以後又被錯誤地解除引用,程序就會立即拋出NullPointerException異常,而不是悄悄地錯誤運行下去。
當程序員第一次被類似這樣的問題困擾的時候,他們往往會過分小心:對於每一個對象引用,一旦程序不再用到它,就把它清空。其實這樣做既沒必要,也不是我們所期望的,因爲這樣做會把程序代碼弄得很亂。清空對象引用應該是一種例外,而不是一種規範行爲。
一般而言,只要類是自己管理內存,程序員就應該警惕內存泄漏問題。一旦元素被釋放掉,則該元素中包含的任何對象引用都應該被清空。

內存泄漏的另一個常見來源是緩存。一旦你把對象引用放到緩存中,它就很容易被遺忘掉。對於這個問題有幾種可能的解決方案:只要在緩存之外存在對某個項的鍵的引用,該項就有意義,那麼就可以用WeakHashMap代表緩存;當緩存中的項過期之後,它們就會自動被刪除。記住只有當所要的緩存項的生命週期是由該鍵的外部引用而不是由值決定時,WeakHashMap纔有用處。
更爲常見的情形則是,“緩存項的生命週期是否有意義”並不是很容易確定,隨着時間的推移,其中的項會變得越來越沒有價值。在這種情況下,緩存應該時不時地清除掉沒用的項。這項清除工作可以由一個後臺線程(可能是Timer或者ScheduleThreadPoolExecutor)來完成,或者也可以給緩存添加新條目的時候順便進行清理。

內存泄漏的第三個常見來源是監聽器和其他回調。如果你實現了一個API,客戶端在這個API中註冊回調,卻沒有顯式地取消註冊,它們就會積聚。確保回調立即被當作垃圾回收的最佳方法是隻保存它們的弱引用。
藉助Heap剖析工具(Heap Profiler)檢測內存泄漏問題。

[b]7. 避免使用終結方法[/b]
終結方法(finalizer)通常是不可預測的,也是很危險的,一般情況下是不必要的。 在Java中,一般用try-finally塊來完成類似的工作。
終結方法的缺點在於不能保證會被及時地執行。從一個對象變得不可到達開始,到它的終結方法被執行,所花費的這段時間是任意長的。由於JVM會延遲執行終結方法,所以大量的文件會保留在打開狀態,當一個程序再不能打開文件的時候,它可能會運行失敗。如果程序依賴於終結方法被執行的時間點,那麼這個程序的行爲在不同的JVM實現中會大相徑庭。 終結方法線程的優先級比該應用程序的其他線程的要低得多,如果終結速度達不到它們進入隊列的速度就會出現OutOfMemoryError。
Java語言規範不僅不保證終結方法會被及時的執行,而且根本不保證他們會被執行。當一個程序終止的時候,某些已經無法訪問的對象上的終結方法卻根本沒有被執行。所以不應該依賴終結方法來更新重要的持久狀態。
不要被System.gc和System.runFinalization這兩個方法所誘惑,他們確實增加了終結方法被執行的機會,但是他們不保證終結方法一定被執行。唯一聲稱保證終結方法被執行的方法是System.runFinalizersOnExit,以及他臭名昭著的孿生兄弟Runtime.runFinalizersOnExit。這兩個方法都有致命的缺陷,都被廢棄了。
如果未被被捕獲的異常在終結過程中被拋出來,那麼這種異常可以被忽略,並且該對象的終結過程也會終止。正常情況下,未被捕獲的異常將會使線程終止,並打印出棧軌跡,但是如果異常發生在終結方法之中,則不會如此,甚至連警告都不會打印出來。

使用終結方法有一個非常嚴重的(Severe)性能損失。用終結方法創建和銷燬對象慢了大約430倍。

正確的終結方法爲:提供一個顯示的終止方法,並要求該類的客戶端在每個實例不再有用的時候調用這個方法。該實例必須記錄下自己是否已經被終止了,顯式的終止方法必須在一個私有域中記錄下“該對象已經不再有效”。如果這些方法是在對象已經終止之後被調用,其他的方法就必須檢查這個域,並拋出IllegalStateException異常。
顯示終止方法的典型例子是InputStream、OutputStream和java.sql.Connection上的close方法。另一個例子是java.util.Timer上的cancel方法,它執行必要的狀態改變,使得與Timer實例相關聯的該線程溫和的終止自己。java.awt中的例子還包括Graphics.dispose和Window.dispose。Image.flush,他會釋放所有與Image實例相關聯的資源,但是該實例仍然處於可用的狀態,如果有必要的話,會重新分配資源。
顯式的終止方法通常與try-finally結構結合起來使用,終結方法可以充當“安全網”,遲一點釋放關鍵資源總比永遠不釋放要好,如果終結方法發現資源還未被終止則應該在日誌中記錄一條警告,應及時修復。
“終結方法鏈”並不會被自動執行,如果類有終結方法,並且子類覆蓋了終結方法,子類的終結方法就必須手工調用超類的終結方法。
總結:除非作爲安全網,或者是爲了終止非關鍵的本地資源,否則請不要使用終結方法。在這些很少見的情況下,既然使用了終結方法,就要記住調用super.finalize。如果用終結方法作爲安全網,要記得記錄終結方法的非法用法。最後,如果需要吧終結方法與公有的非final類關聯起來,請考慮使用終結方法守護者,以確保即使子類的終結方法未能調用super.finalize,該終結方法也會被執行。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章