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