摘要:一考慮用靜態(tài)工廠方法代替構(gòu)造器構(gòu)造器是創(chuàng)建一個對象實(shí)例的最基本最常用的方法。開發(fā)者在使用某個類的時候,通常會使用一個構(gòu)造器來實(shí)現(xiàn),其實(shí)也有其他方式可以實(shí)現(xiàn)的,如利用發(fā)射機(jī)制。
一、考慮用靜態(tài)工廠方法代替構(gòu)造器
構(gòu)造器是創(chuàng)建一個對象實(shí)例的最基本最常用的方法。開發(fā)者在使用某個類的時候,通常會使用new一個構(gòu)造器來實(shí)現(xiàn),其實(shí)也有其他方式可以實(shí)現(xiàn)的,如利用發(fā)射機(jī)制。這里主要說的是通過靜態(tài)類工廠的方式來創(chuàng)建class的實(shí)例,如:
public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
靜態(tài)工廠方法和構(gòu)造器不同有以下主要優(yōu)勢:
1. 有意義的名稱可能有多個構(gòu)造器,不同構(gòu)造器有不同的參數(shù),而參數(shù)本身并不能確切地描述被返回的對象,所以顯得有點(diǎn)模糊,而具有適當(dāng)名稱的靜態(tài)工廠可讀性更強(qiáng),表達(dá)也更清晰。
如,構(gòu)造器BigInteger(int, int, Random)返回一個BigInteger可能是一個素數(shù),改名為BigInteger.probablePrime的靜態(tài)工廠方法表示也就更加清晰。
2. 不必在每次調(diào)用的時候創(chuàng)建一個新的對象這樣可以避免創(chuàng)建不必要的重復(fù)對象,提高程序效率。
3. 可以返回原返回類型的任何子類型的對象Java的很多服務(wù)提供者框架(ServiceProvider Framework,三個主要組件:服務(wù)接口(Service Interface)這是提供者實(shí)現(xiàn)的;提供者注冊API(Provider Registration API),這是系統(tǒng)用來注冊實(shí)現(xiàn),讓客戶端訪問它的;服務(wù)訪問API(Service Access API),是客戶端用來獲取服務(wù)實(shí)例的??蛇x組件:服務(wù)提供者接口(Service Provider Interface),提供者負(fù)責(zé)創(chuàng)建其服務(wù)實(shí)現(xiàn)的實(shí)例)都運(yùn)用到這個特性,如JDBC的API。
下面是一個包含一個服務(wù)提供者接口和一個默認(rèn)提供者:
//Service interface public interface Service{ //...service methods } //Service provider interface public interface Provider{ Service newService(); } //noninstantiable class for service registration and access public class Service{ private Service(){} //Maps service names to services private static final Map4. 在創(chuàng)建參數(shù)化類型實(shí)例的時候,它們使代碼變得更加簡潔providers=new ConcurrentHashMap (); public static final String DEFAULT_PROVIDER_NAME=" "; //Provider registration API public static void registerDefaultProvider(Provider p){ registerProvider(DEFAULT_PROVIDER_NAME); } public static void registerProvider(String name,Provider p){ providers.put(name, p); } //Service access API public static Service newInstance(){ return newInstace(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(); } }
原來:
Map> map=new HashMap >();
改為靜態(tài)工廠方法,可以利用參數(shù)類型推演的優(yōu)勢,避免了類型參數(shù)在一次聲明中被多次重寫所帶來的煩憂:
public staticHashMap newInstance() { return new HashMap (); } Map m = MyHashMap.newInstance();
當(dāng)然,靜態(tài)方法也存在缺點(diǎn):
類如果不含公有的或者受保護(hù)的構(gòu)造器,就不能被子類化;
與其他的靜態(tài)方法實(shí)際上沒有任何區(qū)別(API沒有像構(gòu)造器那樣標(biāo)識出來)
不過,對于靜態(tài)工廠方法和構(gòu)造器,通常優(yōu)先考慮靜態(tài)工廠方法。
二、遇到多個構(gòu)造參數(shù)時要考慮用構(gòu)建器(Builder模式)靜態(tài)工廠和構(gòu)造器有一個共同的局限性:它們都不能很好地擴(kuò)展到大量的可選參數(shù)。當(dāng)然可以通過以下方法解決:
方法一:利用重疊構(gòu)造器模式(就是需要多少個參數(shù)就在參數(shù)列表添加多少個),但是當(dāng)有很多個參數(shù)時,客戶端代碼會很難編寫,并且難以閱讀;
方法二:JavaBeans模式,調(diào)用一個無參構(gòu)造函數(shù),然后調(diào)用setter方法來設(shè)置每個必要的參數(shù),但調(diào)用的過程中可能會出現(xiàn)不一致的狀態(tài),調(diào)試比較麻煩;
方法三:Builder模式。不直接生成想要的對象,而是讓客戶端利用所有必要的參數(shù)調(diào)用構(gòu)造器(或靜態(tài)方法),得到一個builder對象,然后再在builder對象對每個參數(shù)對應(yīng)的方法進(jìn)行調(diào)用來設(shè)置,如下:
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; public static class Builder { //對象的必選參數(shù) private final int servingSize; private final int servings; //對象的可選參數(shù)的缺省值初始化 private int calories = 0; private int fat = 0; private int carbohydrate = 0; private int sodium = 0; //只用少數(shù)的必選參數(shù)作為構(gòu)造器的函數(shù)參數(shù) public Builder(int servingSize,int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; } } //使用方式 public static void main(String[] args) { NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100) .sodium(35).carbohydrate(27).build(); System.out.println(cocaCola); }
所以,如果類的構(gòu)造器或者靜態(tài)工廠中具有多個參數(shù),設(shè)計這種類時,Builder模式就是種不錯的選擇!
三、用私有構(gòu)造器或者枚舉類型強(qiáng)化Singleton屬性Singleton模式經(jīng)常會被用到,它被用來代表那些本質(zhì)上唯一的系統(tǒng)組件,如窗口管理器或者文件系統(tǒng)。在Java中實(shí)現(xiàn)單例模式主要有三種:
將構(gòu)造函數(shù)私有化,直接通過靜態(tài)公有的final域字段獲取單實(shí)例對象:
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elivs() { ... } public void leaveTheBuilding() { ... } }
這樣的方式主要優(yōu)勢在于簡潔高效,使用者很快就能判定當(dāng)前類為單實(shí)例類,在調(diào)用時直接操作Elivs.INSTANCE即可,由于沒有函數(shù)的調(diào)用,因此效率也非常高效。然而事物是具有一定的雙面性的,這種設(shè)計方式在一個方向上走的過于極端了,因此他的缺點(diǎn)也會是非常明顯的。如果今后Elvis的使用代碼被遷移到多線程的應(yīng)用環(huán)境下了,系統(tǒng)希望能夠做到每個線程使用同一個Elvis實(shí)例,不同線程之間則使用不同的對象實(shí)例。那么這種創(chuàng)建方式將無法實(shí)現(xiàn)該需求,因此需要修改接口以及接口的調(diào)用者代碼,這樣就帶來了更高的修改成本。
通過公有域成員的方式返回單實(shí)例對象:
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)境的對象創(chuàng)建邏輯,僅需要修改Elvis的getInstance()方法內(nèi)部即可,對用調(diào)用者而言則是不變的,這樣便極大的縮小了影響的范圍。至于效率問題,現(xiàn)今的JVM針對該種函數(shù)都做了很好的內(nèi)聯(lián)優(yōu)化,因此不會產(chǎn)生因函數(shù)頻繁調(diào)用而帶來的開銷。
使用枚舉的方式(Java SE5):
public enum Elvis { INSTANCE; public void leaveTheBuilding() { ... } }
就目前而言,這種方法在功能上和公有域方式相近,但是他更加簡潔更加清晰,擴(kuò)展性更強(qiáng)也更加安全。雖然這種方法還沒被廣泛采用,但單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法。
四、通過私有構(gòu)造器強(qiáng)化不可實(shí)例化的能力對于有些工具類如java.lang.Math、java.util.Arrays等,其中只是包含了靜態(tài)方法和靜態(tài)域字段,因此對這樣的class實(shí)例化就顯得沒有任何意義了。然而在實(shí)際的使用中,如果不加任何特殊的處理,這樣的classes是可以像其他classes一樣被實(shí)例化的。這里介紹了一種方式,既將缺省構(gòu)造函數(shù)設(shè)置為private,這樣類的外部將無法實(shí)例化該類,與此同時,在這個私有的構(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(); } }
這樣定義之后,該類將不會再被外部實(shí)例化了,否則會產(chǎn)生編譯錯誤。然而這樣的定義帶來的最直接的負(fù)面影響是該類將不能再被子類化。
五、避免創(chuàng)建不必要的對象一般來說,最好能重用對象而不是在每次需要的時候創(chuàng)建一個相同功能的新對象。
試比較以下兩行代碼在被多次反復(fù)執(zhí)行時的效率差異:
String s = new String("stringette"); //don"t do this String s = "stringette";
由于String被實(shí)現(xiàn)為不可變對象,JVM底層將其實(shí)現(xiàn)為常量池,既所有值等于"stringette" 的String對象實(shí)例共享同一對象地址,而且還可以保證,對于所有在同一JVM中運(yùn)行的代碼,只要他們包含相同的字符串字面常量,該對象就會被重用。
我們繼續(xù)比較下面的例子,并測試他們在運(yùn)行時的效率差異:
Boolean b = Boolean.valueOf("true"); Boolean b = new Boolean("true");
前者通過靜態(tài)工廠方法保證了每次返回的對象,如果他們都是true或false,那么他們將返回相同的對象。換句話說,valueOf將只會返回Boolean.TRUE或Boolean.FALSE兩個靜態(tài)域字段之一。而后面的Boolean構(gòu)造方式,每次都會構(gòu)造出一個新的Boolean實(shí)例對象。這樣在多次調(diào)用后,第一種靜態(tài)工廠方法將會避免大量不必要的Boolean對象被創(chuàng)建,從而提高了程序的運(yùn)行效率,也降低了垃圾回收的負(fù)擔(dān)。
繼續(xù)比較下面的代碼:
public class Person { private final Date birthDate; //判斷該嬰兒是否是在生育高峰期出生的。 public boolean isBabyBoomer { Calender c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); c.set(1946,Calendar.JANUARY,1,0,0,0); Date dstart = c.getTime(); c.set(1965,Calendar.JANUARY,1,0,0,0); Date dend = c.getTime(); return birthDate.compareTo(dstart) >= 0 && birthDate.compareTo(dend) < 0; } } //修改后 public class Person { private static final Date BOOM_START; private static final Date BOOM_END; static { Calender c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); c.set(1946,Calendar.JANUARY,1,0,0,0); BOOM_START = c.getTime(); c.set(1965,Calendar.JANUARY,1,0,0,0); BOOM_END = c.getTime(); } public boolean isBabyBoomer() { return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0; } }
改進(jìn)后的Person類只是在初始化的時候創(chuàng)建Calender、TimeZone和Date實(shí)例一次,而不是在每次調(diào)用isBabyBoomer方法時都創(chuàng)建一次他們。如果該方法會被頻繁調(diào)用,效率的提升將會極為顯著。
集合框架中的Map接口提供keySet方法,該方法每次都將返回底層原始Map對象鍵數(shù)據(jù)的視圖,而并不會為該操作創(chuàng)建一個Set對象并填充底層Map所有鍵的對象拷貝。因此當(dāng)多次調(diào)用該方法并返回不同的Set對象實(shí)例時,事實(shí)上他們底層指向的將是同一段數(shù)據(jù)的引用。
在該條目中還提到了自動裝箱行為給程序運(yùn)行帶來的性能沖擊,如果可以通過原始類型完成的操作應(yīng)該盡量避免使用裝箱類型以及他們之間的交互使用。見下例:
public static void main(String[] args) { Long sum = 0L; //注意Long與long for (long i = 0; i < Integer.MAX_VALUE; ++i) { sum += i; } System.out.println(sum); }
本例中由于錯把long sum定義成Long sum,其效率降低了近10倍,這其中的主要原因便是該錯誤導(dǎo)致了2的31次方個臨時Long對象被創(chuàng)建了。要優(yōu)先使用基本類型而不是裝箱基本類型,要當(dāng)心無意識的自動裝箱。
六、消除過期的對象引用盡管Java的JVM垃圾回收機(jī)制對內(nèi)存進(jìn)行智能管理了,不像C++那樣需要手動管理,但只是因為如此,Java中內(nèi)存泄露變得更加隱匿,更加難以發(fā)現(xià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); } }
以上示例代碼,在正常的使用中不會產(chǎn)生任何邏輯問題,然而隨著程序運(yùn)行時間不斷加長,內(nèi)存泄露造成的副作用將會慢慢的顯現(xiàn)出來,如磁盤頁交換、OutOfMemoryError等。那么內(nèi)存泄露隱藏在程序中的什么地方呢?當(dāng)我們調(diào)用pop方法是,該方法將返回當(dāng)前棧頂?shù)膃lements,同時將該棧的活動區(qū)間(size)減一,然而此時被彈出的Object仍然保持至少兩處引用,一個是返回的對象,另一個則是該返回對象在elements數(shù)組中原有棧頂位置的引用。這樣即便外部對象在使用之后不再引用該Object,那么它仍然不會被垃圾收集器釋放,久而久之導(dǎo)致了更多類似對象的內(nèi)存泄露。修改方式如下:
public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; //手工將數(shù)組中的該對象置空 return result; }
由于現(xiàn)有的Java垃圾收集器已經(jīng)足夠只能和強(qiáng)大,因此沒有必要對所有不在需要的對象執(zhí)行obj = null的顯示置空操作,這樣反而會給程序代碼的閱讀帶來不必要的麻煩,該條目只是推薦在以下3中情形下需要考慮資源手工處理問題:
類是自己管理內(nèi)存,如例子中的Stack類。
使用對象緩存機(jī)制時,需要考慮被從緩存中換出的對象,或是長期不會被訪問到的對象。
事件監(jiān)聽器和相關(guān)回調(diào)。用戶經(jīng)常會在需要時顯示的注冊,然而卻經(jīng)常會忘記在不用的時候注銷這些回調(diào)接口實(shí)現(xiàn)類。
七、避免使用終結(jié)方法終結(jié)方法(finalizer)通常是不可預(yù)測的,也是很危險的,一般情況下是不必要的。使用終結(jié)方法會導(dǎo)致行為不穩(wěn)定、降低性能,以及可移植性問題。
在Java中完成這樣的工作主要是依靠try-finally機(jī)制來協(xié)助完成的。然而Java中還提供了另外一種被稱為finalizer的機(jī)制,使用者僅僅需要重載Object對象提供的finalize方法,這樣當(dāng)JVM的在進(jìn)行垃圾回收時,就可以自動調(diào)用該方法。但是由于對象何時被垃圾收集的不確定性,以及finalizer給GC帶來的性能上的影響,因此并不推薦使用者依靠該方法來達(dá)到關(guān)鍵資源釋放的目的。比如,有數(shù)千個圖形句柄都在等待被終結(jié)和回收,可惜的是執(zhí)行終結(jié)方法的線程優(yōu)先級要低于普通的工作者線程,這樣就會有大量的圖形句柄資源停留在finalizer的隊列中而不能被及時的釋放,最終導(dǎo)致了系統(tǒng)運(yùn)行效率的下降,甚至還會引發(fā)JVM報出OutOfMemoryError的錯誤。
Java的語言規(guī)范中并沒有保證該方法會被及時的執(zhí)行,甚至都沒有保證一定會被執(zhí)行。即便開發(fā)者在code中手工調(diào)用了 System.gc 和 System.runFinalization 這兩個方法,這僅僅是提高了finalizer被執(zhí)行的幾率而已。還有一點(diǎn)需要注意的是,被重載的finalize()方法中如果拋出異常,其棧幀軌跡是不會被打印出來的。在Java中被推薦的資源釋放方法為,提供顯式的具有良好命名的接口方法,如 FileInputStream.close() 和 Graphic2D.dispose() 等。然后使用者在finally區(qū)塊中調(diào)用該方法,見如下代碼:
public void test() { FileInputStream fin = null; try { fin = new FileInputStream(filename); //do something. } finally { fin.close(); } }
在實(shí)際的開發(fā)中,利用finalizer又能給我們帶來什么樣的幫助呢?見下例:
public class FinalizeTest { //@Override protected void finalize() throws Throwable { try { //在調(diào)試過程中通過該方法,打印對象在被收集前的各種狀態(tài), //如判斷是否仍有資源未被釋放,或者是否有狀態(tài)不一致的現(xiàn)象存在。 //推薦將該finalize方法設(shè)計成僅在debug狀態(tài)下可用,而在release //下該方法并不存在,以避免其對運(yùn)行時效率的影響。 System.out.println("The current status: " + _myStatus); } finally { //在finally中對超類finalize方法的調(diào)用是必須的,這樣可以保證整個class繼承 //體系中的finalize鏈都被執(zhí)行。 super.finalize(); } } }
整理參考自《Effective Java》和 Effective Java (創(chuàng)建和銷毀對象)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/64244.html
摘要:第二章創(chuàng)建和銷毀對象何時以及如何創(chuàng)建對象,何時以及如何避免創(chuàng)建對象,如何確保他們能夠適時地銷毀,以及如何管理對象銷毀之前必須進(jìn)行的各種清理動作。表示工廠方法所返回的對象類型。 第二章 創(chuàng)建和銷毀對象 何時以及如何創(chuàng)建對象,何時以及如何避免創(chuàng)建對象,如何確保他們能夠適時地銷毀,以及如何管理對象銷毀之前必須進(jìn)行的各種清理動作。 1 考慮用靜態(tài)工廠方法代替構(gòu)造器 一般在某處獲取一個類的實(shí)例最...
摘要:本章中的大部分內(nèi)容適用于構(gòu)造函數(shù)和方法。第項其他方法優(yōu)先于序列化第項謹(jǐn)慎地實(shí)現(xiàn)接口第項考慮使用自定義的序列化形式第項保護(hù)性地編寫方法第項對于實(shí)例控制,枚舉類型優(yōu)先于第項考慮用序列化代理代替序列化實(shí)例附錄與第版中項目的對應(yīng)關(guān)系參考文獻(xiàn) effective-java-third-edition 介紹 Effective Java 第三版全文翻譯,純屬個人業(yè)余翻譯,不合理的地方,望指正,感激...
摘要:構(gòu)造器的參數(shù)沒有確切地描述其返回的對象,適當(dāng)名稱的靜態(tài)工廠方法更容易使用,也易于閱讀。在文檔中,沒有像構(gòu)造器那樣明確標(biāo)識出來,因此,對于提供了靜態(tài)工廠方法而不是構(gòu)造器的類來說,要查明如何實(shí)例化一個類,有點(diǎn)困難。 第二章 創(chuàng)建和銷毀對象 第1條 考慮用靜態(tài)工廠方法代替構(gòu)造器 兩者創(chuàng)建對象的形式,例如:構(gòu)造器是new Boolean();靜態(tài)工廠方法是 public static Bool...
摘要:模式下使用來設(shè)置各個參數(shù),無法僅通過檢驗構(gòu)造器參數(shù)的有效性來保證一致性,會試圖使用不一致狀態(tài)的對象。消除過期的對象引用緩存時優(yōu)先使用,這些數(shù)據(jù)結(jié)構(gòu),及時清掉沒用的項。顯示取消監(jiān)聽器和回調(diào),或進(jìn)行弱引用。 創(chuàng)建和銷毀對象 1、靜態(tài)工廠方法代替構(gòu)造器 靜態(tài)工廠方法有名稱,能確切地描述正被返回的對象。 不必每次調(diào)用都創(chuàng)建一個新的對象。 可以返回原返回類型的任何子類對象。 創(chuàng)建參數(shù)化類型實(shí)例...
閱讀 2038·2023-04-25 14:50
閱讀 2917·2021-11-17 09:33
閱讀 2621·2019-08-30 13:07
閱讀 2847·2019-08-29 16:57
閱讀 914·2019-08-29 15:26
閱讀 3556·2019-08-29 13:08
閱讀 2001·2019-08-29 12:32
閱讀 3394·2019-08-26 13:57