對(duì)于類而言,為了讓客服端獲得它的一個(gè)實(shí)例最常用的的一個(gè)方法就是提供一個(gè)公有的構(gòu)造器。還有一種方法,類可以提供一個(gè)公有的靜態(tài)工廠方法(static factory method),它只是一個(gè)返回類實(shí)例的靜態(tài)方法。
通過(guò)靜態(tài)工廠方法構(gòu)造對(duì)象的優(yōu)勢(shì):
靜態(tài)工廠方法與構(gòu)造器不同的第一大優(yōu)勢(shì)在于,它們有名稱,使客服端代碼更加容易被閱讀。 不必在每次調(diào)用的它們的時(shí)候都創(chuàng)建一個(gè)新的對(duì)象(這個(gè)完全取決于具體的實(shí)現(xiàn))。 它們可以返回原返回類型的任何子類型的對(duì)象。 這種靈活性的一種應(yīng)用:API可以返回對(duì)象,同時(shí)又不會(huì)使對(duì)象的類變成公有的。公有的靜態(tài)方法所返回的對(duì)象的類不僅可以是非公有的,而且該類還可以隨著每次調(diào)用而發(fā)生變化著取決于靜態(tài)工廠方法的參數(shù)值,只要是已聲明返回類型的子類型,都是允許的。在創(chuàng)建參數(shù)化類型(也就是泛型,jdk1.5新特性)實(shí)例的時(shí)候,它們是的代碼變得更加簡(jiǎn)潔。/**普通創(chuàng)建****/ Map<String,List<String>> m=new HashMap<String,List<String>>; /**有了靜態(tài)方法過(guò)后***/ Map<String,List<String>> m=HashMap.newInstance(); //前提HashMap提供了這個(gè)靜態(tài)工廠方法 public static <k,v> HashMap<k,v> newInstance(){ return new HashMap<K,V>(); }靜態(tài)工廠方法的主要缺點(diǎn)在于:
類如果不含有他的公有或者受保護(hù)的構(gòu)造器,就不能被子類化(即被繼承)。它們與其他靜態(tài)方法實(shí)際上沒(méi)有任何區(qū)別。常用的靜態(tài)工廠名稱:valueOf,of,getInstance,newInstance,getType,newType.
對(duì)于Builder方式,可選參數(shù)的缺省值問(wèn)題也將不再困擾著所有的使用者。這種方式還帶來(lái)了一個(gè)間接的好處是,不可變對(duì)象的初始化以及參數(shù)合法性的驗(yàn)證等工作在構(gòu)造函數(shù)中原子性的完成了。
1、將構(gòu)造函數(shù)私有化,直接通過(guò)靜態(tài)公有的final域字段獲取單實(shí)例對(duì)象:
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elivs() { ... } public void leaveTheBuilding() { ... } }這樣的方式主要優(yōu)勢(shì)在于簡(jiǎn)潔高效,使用者很快就能判定當(dāng)前類為單實(shí)例類,在調(diào)用時(shí)直接操作Elivs.INSTANCE即可,由于沒(méi)有函數(shù)的調(diào)用,因此效率也非常高效。然而事物是具有一定的雙面性的,這種設(shè)計(jì)方式在一個(gè)方向上走的過(guò)于極端了,因此他的缺點(diǎn)也會(huì)是非常明顯的。如果今后Elvis的使用代碼被遷移到多線程的應(yīng)用環(huán)境下了,系統(tǒng)希望能夠做到每個(gè)線程使用同一個(gè)Elvis實(shí)例,不同線程之間則使用不同的對(duì)象實(shí)例。那么這種創(chuàng)建方式將無(wú)法實(shí)現(xiàn)該需求,因此需要修改接口以及接口的調(diào)用者代碼,這樣就帶來(lái)了更高的修改成本。
2、 通過(guò)公有域成員的方式返回單實(shí)例對(duì)象:
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elivs() { ... } public static Elvis getInstance() { return INSTANCE; } public void leaveTheBuilding() { ... } }這種方法很好的彌補(bǔ)了第一種方式的缺陷,如果今后需要適應(yīng)多線程環(huán)境的對(duì)象創(chuàng)建邏輯,僅需要修改Elvis的getInstance()方法內(nèi)部即可,對(duì)用調(diào)用者而言則是不變的,這樣便極大的縮小了影響的范圍。至于效率問(wèn)題,現(xiàn)今的JVM針對(duì)該種函數(shù)都做了很好的內(nèi)聯(lián)優(yōu)化,因此不會(huì)產(chǎn)生因函數(shù)頻繁調(diào)用而帶來(lái)的開(kāi)銷。
3、使用枚舉的方式(java SE5):
public enum Elvis { INSTANCE; public void leaveTheBuilding() { ... } }就目前而言,這種方法在功能上和公有域方式相近,但是他更加簡(jiǎn)潔更加清晰,擴(kuò)展性更強(qiáng)也更加安全。
對(duì)于有些工具類如java.lang.Math、java.util.Arrays等,其中只是包含了靜態(tài)方法和靜態(tài)域字段,因此對(duì)這樣的class實(shí)例化就顯得沒(méi)有任何意義了。然而在實(shí)際的使用中,如果不加任何特殊的處理,這樣的classes是可以像其他classes一樣被實(shí)例化的。這里介紹了一種方式,既將缺省構(gòu)造函數(shù)設(shè)置為private,這樣類的外部將無(wú)法實(shí)例化該類,與此同時(shí),在這個(gè)私有的構(gòu)造函數(shù)的實(shí)現(xiàn)中直接拋出異常,從而也避免了類的內(nèi)部方法調(diào)用該構(gòu)造函數(shù)。
public class UtilityClass { //Suppress default constructor for noninstantiability. private UtilityClass() { throw new AssertionError(); } }這樣定義之后,該類將不會(huì)再被外部實(shí)例化了,否則會(huì)產(chǎn)生編譯錯(cuò)誤。然而這樣的定義帶來(lái)的最直接的負(fù)面影響是該類將不能再被子類化。
一般來(lái)說(shuō),最好能重用對(duì)象而不是在每次需要的時(shí)候就創(chuàng)建一個(gè)相同功能的新對(duì)象。如果對(duì)象是不可變的,它就始終可以被重用。 反例:
String s = new String(“stringette”);該語(yǔ)句每次被執(zhí)行的時(shí)候都創(chuàng)建一個(gè)新的String實(shí)例,但是這些創(chuàng)建對(duì)象的動(dòng)作全都是不必要的,傳遞給String構(gòu)造器的參數(shù)(“stringette”)本身就是一個(gè)String實(shí)例,功能方面等同于構(gòu)造器創(chuàng)建的對(duì)象。如果這種用法是在一個(gè)循環(huán)中,或者是在一個(gè)被頻繁調(diào)用的方法中,就會(huì)創(chuàng)建出很多不必要的String實(shí)例。 改進(jìn):
String s = “stringette”;改進(jìn)后,只用一個(gè)String實(shí)例,而不是每次執(zhí)行的時(shí)候都創(chuàng)建一個(gè)新的實(shí)例,而且,它可以保證,對(duì)于所有在同一虛擬機(jī)中運(yùn)行的代碼,只要它們包含相同的字符串字面常量,該對(duì)象就會(huì)被重用。
對(duì)于同時(shí)提供了靜態(tài)工廠方法和構(gòu)造器的不可變類,通常可以使用靜態(tài)工廠方法而不是構(gòu)造器,以避免創(chuàng)建不必要的對(duì)象。 例如,靜態(tài)工廠方法Boolean.valueOf(String)幾乎總是比構(gòu)造器Boolean(String)好,構(gòu)造器在每次被調(diào)用的時(shí)候都會(huì)創(chuàng)建一個(gè)新的對(duì)象,而靜態(tài)工廠方法則從來(lái)不要求這樣做,實(shí)際上也不會(huì)這樣做。
要優(yōu)先使用基本類型而不是裝箱基本類型,要當(dāng)心無(wú)意識(shí)的自動(dòng)裝箱:
public static void main(String[] args) { Long sum = 0L; for (long i = 0; i < Integer.MAX_VALUE; i++) { sum += i; } System.out.println(sum);}這段程序算出的答案是正確的,但是比實(shí)際情況要更慢一些,只因?yàn)榇蝈e(cuò)了一個(gè)字符。變量sum被聲明成Long而不是long,這就意味著程序構(gòu)造了大約2^31個(gè)多的Long實(shí)例。
Java共有9中基本類型,同別的語(yǔ)言有重要區(qū)別的是這9中類型所占存儲(chǔ)空間大小與機(jī)器硬件架構(gòu)無(wú)關(guān),這使得Java程序有很強(qiáng)的可移植性,如下圖:
不要錯(cuò)誤地認(rèn)為“創(chuàng)建對(duì)象的代價(jià)非常昂貴,我們應(yīng)該要盡可能地避免創(chuàng)建對(duì)象,而不是不創(chuàng)建對(duì)象”,相反,由于小對(duì)象的構(gòu)造器只做很少量的工作,所以,小對(duì)象的創(chuàng)建和回收動(dòng)作是非常廉價(jià)的,特別是在現(xiàn)代的JVM實(shí)現(xiàn)上更是如此。通過(guò)創(chuàng)建附加的對(duì)象,提升程序的清晰性、簡(jiǎn)潔性和功能性,這通常也是件好事。
反之,通過(guò)維護(hù)自己的對(duì)象池來(lái)創(chuàng)建對(duì)象并不是一種好的做法,除非池中的對(duì)象是非常重量級(jí)的。真正正確使用對(duì)象池的典型對(duì)象示例就是數(shù)據(jù)庫(kù)連接池。建立數(shù)據(jù)庫(kù)連接的代價(jià)是非常昂貴的,因此重用這些對(duì)象非常有意義。
另外,在1.5版本里,對(duì)基本類型的整形包裝類型使用時(shí),要使用形如 Byte.valueOf來(lái)創(chuàng)建包裝類型,因?yàn)?128~127的數(shù)會(huì)緩存起來(lái),所以我們要從緩沖池中取,Short、Integer、Long也是這樣。
盡管Java不像C/C++那樣需要手工管理內(nèi)存資源,而是通過(guò)更為方便、更為智能的垃圾回收機(jī)制來(lái)幫助開(kāi)發(fā)者清理過(guò)期的資源。即便如此,內(nèi)存泄露問(wèn)題仍然會(huì)發(fā)生在你的程序中,只是和C/C++相比,Java中內(nèi)存泄露更加隱匿,更加難以發(fā)現(xiàn),見(jiàn)如下代碼:
public 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.copys(elements,2*size+1); } }這段程序有一個(gè)“內(nèi)存泄漏”問(wèn)題,如果一個(gè)棧先是增長(zhǎng),然后再收縮,那么,從棧中彈出來(lái)的對(duì)象不會(huì)被當(dāng)做垃圾回收,即使使用棧的程序不再引用這些對(duì)象,它們也不會(huì)被回收。這是因?yàn)椋瑮?nèi)部維護(hù)這對(duì)這些對(duì)象的過(guò)期使用(obsolete reference),過(guò)期引用指永遠(yuǎn)也不會(huì)被解除的引用。 修復(fù)的方法很簡(jiǎn)單:一旦對(duì)象引用已經(jīng)過(guò)期,只需要清空這些引用即可。對(duì)于上述例子中的Stack類而言,只要一個(gè)單元彈出棧,指向它的引用就過(guò)期了,就可以將它清空。
修改方式如下:
public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; //手工將數(shù)組中的該對(duì)象置空 return result; }Stock為什么會(huì)有內(nèi)存泄漏問(wèn)題呢? 問(wèn)題在于,Stock類自己管理內(nèi)存。存儲(chǔ)池中包含了elements數(shù)組(對(duì)象引用單元,而不是對(duì)象本身)的元素。數(shù)組活動(dòng)區(qū)域的元素是已分配的,而數(shù)組其余部分的元素是自由的。但是垃圾回收器并不知道這一點(diǎn),就需要手動(dòng)清空這些數(shù)組元素。 一般而言,只要類是自己管理內(nèi)存,就應(yīng)該警惕內(nèi)存泄漏問(wèn)題。一旦元素被釋放掉,則該元素中包含的任何對(duì)象引用都應(yīng)該被清空。
由于現(xiàn)有的Java垃圾收集器已經(jīng)足夠只能和強(qiáng)大,因此沒(méi)有必要對(duì)所有不在需要的對(duì)象執(zhí)行obj = null的顯示置空操作,這樣反而會(huì)給程序代碼的閱讀帶來(lái)不必要的麻煩,該條目只是推薦在以下3中情形下需要考慮資源手工處理問(wèn)題:
類是自己管理內(nèi)存,如例子中的Stack類。使用對(duì)象緩存機(jī)制時(shí),需要考慮被從緩存中換出的對(duì)象,或是長(zhǎng)期不會(huì)被訪問(wèn)到的對(duì)象。事件監(jiān)聽(tīng)器和相關(guān)回調(diào)。用戶經(jīng)常會(huì)在需要時(shí)顯示的注冊(cè),然而卻經(jīng)常會(huì)忘記在不用的時(shí)候注銷這些回調(diào)接口實(shí)現(xiàn)類。Java的語(yǔ)言規(guī)范中并沒(méi)有保證終結(jié)方法會(huì)被及時(shí)的執(zhí)行,甚至都沒(méi)有保證一定會(huì)被執(zhí)行。即便開(kāi)發(fā)者在code中手工調(diào)用了System.gc和System.runFinalization這兩個(gè)方法,這僅僅是提高了finalizer被執(zhí)行的幾率而已。還有一點(diǎn)需要注意的是,被重載的finalize()方法中如果拋出異常,其棧幀軌跡是不會(huì)被打印出來(lái)的。
《Effective Java中文版 第2版》PDF版下載: http://download.csdn.net/detail/xunzaosiyecao/9745699
作者:jiankunking 出處:http://blog.csdn.net/jiankunking
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注