第二章 創建和銷燬對象
何時以及如何創建對象,何時以及如何避免創建對象,如何確保他們能夠適時地銷燬,以及如何管理對象銷燬之前必須進行的各種清理動作。
1 考慮用靜態工廠方法代替構造器
一般在某處獲取一個類的實例最常用的方法是提供一個共有的構造器,還有一種方法,就是提供一個共有的靜態工廠(static factory method),他只是一個返回類的實例的靜態方法。
例:
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE:Boolean.FALSE;
}
注意,靜態工廠方法與設計模式中的工廠方法模式不同。類可以通過靜態工廠方法來提供給它的客戶端,而不是通過構造器,提供靜態工廠方法而不是公有的構造器,這樣做具有幾大優勢:
- 靜態工廠方法,它們有名稱
例如構造器BIgInteger(int,int,Random)返回的BigInteter可能爲素數,如果用名爲BigInteger.probablePrime的靜態工廠方法來表示,顯然更爲清楚。- 不必在每次調用它們的時候都創建一個新的對象
這使得不可變類可以使用預先構建好的實例,或者將構建好的實例緩存起來,進行重複利用,從而避免常見不必要的重複對象,因爲程序經常請求創建相同的對象,那麼創建對象的代價會很高。Boolean.valueOf(boolean)方法說明了這項技術。靜態工廠方法也經常用於實現單例模式。- 它們可以返回原返回類型的任何子類型的對象
靈活的靜態工廠方法構成了服務提供者框架(Service Provider FrameWork)的基礎,例如JDBC。
對於JDBC,Connection就是它的服務接口,DriverManager.registerDriver是提供者註冊API,DriverManager.getConnection是服務訪問API,Driver就是服務提供者接口。
例,下列簡單的實現包含了一個服務提供者接口和一個默認提供者:
/**
* Created by Newtrek on 2017/10/31.
* 服務提供者接口
*/
public interface Provider {
// 提供服務實例,用服務接口返回
Service newService();
}
/**
* Created by Newtrek on 2017/10/31.
* 服務接口
*/
public interface Service {
// TODO: 2017/10/31 服務特有的方法 寫在這兒
}
/**
* Created by Newtrek on 2017/10/31.
*/
public class Services {
// 構造保護
private Services(){}
// provider映射表,保存註冊的Provider
private static final Map<String ,Provider> providers=new ConcurrentHashMap<>();
// 默認provider的名字
public static final String DEFAULT_PROVIDER_NAME="<def>";
// 註冊默認的Provider
public static void registerDefaultProvider(Provider p){
registerProvider(DEFAULT_PROVIDER_NAME,p);
}
// 註冊provider
public static void registerProvider(String name,Provider p){
providers.put(name,p);
}
/**
* 靜態工廠方法返回Service實例
*/
public static Service newInstance(){
return newInstance(DEFAULT_PROVIDER_NAME);
}
public static Service newInstance(String name){
Provider p = providers.get(name);
if (p==null){
throw new IllegalArgumentException("No provider registered with name:"+name);
}
return p.newService();
}
}
- 在創建參數化類型實例的時候,它們是代碼變得更加簡潔
例:假設HashMap提供了這個靜態工廠
public static <K,V> HashMap<K,V> newInstance(){
return new HashMap<K,V>();
}
那麼就可以用下面簡潔的代碼獲取實例了。
Map<String,List<String>> m=HashMap.newInstance();
把這些方法放在自己的工具類中是很實用的。不過現在java7,java8已經實現了HashMap構造的類型參數推測
缺點
- 類如果不含公有的或者受保護的構造器,就不能被子類化
- 它們與其他的靜態方法實際上沒有任何區別
- 在API文檔中,它們沒有像構造器那樣在API文檔中明確標識出來,因爲,對於提供了靜態工廠方法二不是構造器的類來說,要想查明如何實例化一個類,這是非常困難的。可以通過在類或者接口註釋中關注靜態工廠,並遵守標準的命名習慣,可以彌補這一劣勢。下面是靜態工廠方法的一些慣用名稱。
- valueOf:實際上是類型轉換方法。
- of:valueOf的一種更爲簡潔的代替
- getInstance:返回的實例是通過方法的參數來描述的,但是不能夠說與參數具有同樣的值。對於Singleton來說,該方法沒有參數,並返回唯一的實例。
- newInstance:像getInstance一樣,但newInstance能夠確保返回的每個實例都與所有其它實例不同。
- getType:像getInstance一樣,但是在工廠方法處於不同的類中的時候使用。Type表示工廠方法所返回的對象類型。
- newType:像newInstance一樣,但是在工廠方法處於不同的類中的時候使用,Type表示工廠方法所返回的對象類型。
- 在API文檔中,它們沒有像構造器那樣在API文檔中明確標識出來,因爲,對於提供了靜態工廠方法二不是構造器的類來說,要想查明如何實例化一個類,這是非常困難的。可以通過在類或者接口註釋中關注靜態工廠,並遵守標準的命名習慣,可以彌補這一劣勢。下面是靜態工廠方法的一些慣用名稱。
簡而言之,靜態工廠方法和公有構造器都各有用處,我們需要理解他們各自的長處。靜態工廠通常更加合適,因此切忌第一反應就是提供公有的構造器,而不先考慮靜態工廠。
2.遇到多個構造器參數時要考慮用構建器
這個就是Builder設計模式
3.用私有構造器或者枚舉類型強化Singleton屬性
這個是單例模式的注意事項,選擇最好的單例模式實現
4.通過私有構造器強化不可實例化的能力
有時候需要編寫一些只包含靜態方法和靜態域的類,這些類的名聲很不好,因爲有些人在面向對象的語言中濫用這樣的類來編寫過程化的程序,儘管如此,他們也確實有它們的好處,比如常見的工具類java.lang.Math等,都是這樣。方正這樣不可以實例化的類,最好把他的構造器設置爲私有。
例如:
public class UtilityClass{
private UtilityClass(){
throw new AssertionError();
}
}
5.避免創建不必要的對象
一般來說,最好能重用對象而不是再每次需要的時候就創建一個相同功能的新對象,如果對象是不可變的,他就始終可以被重用。
簡單的例子:字符串
String s = new String("stringtest");//不要這樣做,因爲該語句每次執行的時候都會創建一個新的String實例,沒必要
// 改進後的版本,這個版本只用了一個String實例,而不是每次執行的時候都創建一個新的實例。對於再同一臺虛擬機中運行的代碼,只要它們包含相同的字符串自字面常量,該對象就可以被重用。
String s = "stringtest";
對於同時提供了靜態方法和構造器方法的不可變類,通常可以使用靜態工廠方法而不是構造器,以避免創建不必要的對象。
優先使用基本類型,而不是裝箱基本類型,當心無意識的自動裝箱。
也不要錯誤地認爲本條目暗示着“創建對象的代價非常昂貴,我們應該儘可能地避免創建對象”,相反,由於小對象地構造器製作很少量地顯示工作,所以,小對象地創建和回收動作是非常廉價地。
通過維護自己的對象池來避免創建對象比不是一種好的做法,除非池中的對象是非常重量級的,一般數據庫連接池常用。
6 消除過期的對象引用
不要以爲Java有垃圾回收機制,能自動管理內存,自動回收垃圾,就可以不管了,其實不然。
內存泄漏的例子
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_CAPACITY = 16;
public Stack(){
elements = new Object[DEFAULT_CAPACITY];
}
public void push(Object object){
ensureCapacity();
elements[size++] = object;
}
public Object pup(){
if (size == 0){
throw new EmptyStackException();
}
return elements[size--];
}
private void ensureCapacity(){
if (elements.length == size){
elements = Arrays.copyOf(elements,size*2+1);
}
}
}
這段程序並沒有明顯的錯誤,如果是棧先是增長,然後再收縮,那麼,從棧中彈出來的對象將不會被當作垃圾回收,因爲裏面的數組裏引用着它,棧內部維護着這些對象的過期引用,過期引用就是指永遠也不會再被解除的引用。
這類問題的修復方法很簡單:一旦對象引用已經過期,只需清空這些應用即可。
沒必要對於每一個對象引用,一旦程序不再用到它,就把它清空。清空對象引用應該是一種例外,而不是一種規範行爲,消除過期引用最好的辦法是讓包含該對象的變量結束其生命週期。一般而言,只要類是自己管理內存,程序員就應該警惕內存泄漏問題,一旦元素被釋放掉,則該元素中包含的任何對象引用都應該被清空。
內存泄漏的另一個常見來源是緩存,一旦你把對象放在緩存中,他就很容易被遺忘掉,從而使得它不再有用之後很長一段時間內仍然留在緩存中。可以用WeakHashMap
內存泄漏的第三個常見來源是監聽器和其他回掉,一般都要取消註冊,或者用弱引用
內存泄漏通常不會表現成明顯的失敗,所以他們可以再一個系統中存在很多年,往往通過仔細檢查代碼,藉助於Heap刨析工具才能發現內存泄漏問題。
7 避免使用終結方法
終結方法(finalizer)通常是不可預測的,也是很危險的,一般情況下是不必要的。
C++的析構器是回收一個對象所佔用資源的常規方法,是構造器所必須的對應物,也可以用來回收其他的非內存資源,而在Java中,一般用try-finally塊來完成類似的工作