您好,登錄后才能下訂單哦!
這篇文章主要介紹了Java如何創(chuàng)建和銷毀對象的相關(guān)知識,內(nèi)容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Java如何創(chuàng)建和銷毀對象文章都會有所收獲,下面我們一起來看看吧。
一、考慮用靜態(tài)工廠方法代替構(gòu)造器:
構(gòu)造器是創(chuàng)建一個對象實例最基本也最通用的方法,大部分開發(fā)者在使用某個class的時候,首先需要考慮的就是如何構(gòu)造和初始化一個對象示例,而構(gòu)造的方式首先考慮到的就是通過構(gòu)造函數(shù)來完成,因此在看javadoc中的文檔時首先關(guān)注的函數(shù)也是構(gòu)造器。然而在有些時候構(gòu)造器并非我們***的選擇,通過反射也是可以輕松達到的。我們這里主要提到的方式是通過靜態(tài)類工廠的方式來創(chuàng)建class的實例,如:
public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
靜態(tài)工廠方法和構(gòu)造器不同有以下主要優(yōu)勢:
1. 有意義的名稱。
在框架設(shè)計中,針對某些工具類通常會考慮dummy對象或者空對象以辨別該對象是否已經(jīng)被初始化,如我曾在我的C++基礎(chǔ)庫中實現(xiàn)了String類型,見如下代碼:
void showExample() { String strEmpty = String::empty(); String strEmpty2 = ""; String strData = String::prellocate(1024); if (strEmpty.isEmpty()) { //TODO: do something } } static String String::emptyString; String& String::empty() { return emptyString; } bool String::isEmpty() { if (this->_internal == &emptyString->_internal) return true; //TODO: do other justice to verify whether it is empty. }
在上面的代碼中,提供了兩個靜態(tài)工廠方法empty和preallocate用于分別創(chuàng)建一個空對象和一個帶有指定分配空間的String對象。從使用方式來看,這些靜態(tài)方法確實提供了有意義的名稱,使用者很容易就可以判斷出它們的作用和應用場景,而不必在一組重載的構(gòu)造器中去搜尋每一個構(gòu)造函數(shù)及其參數(shù)列表,以找出適合當前場景的構(gòu)造函數(shù)。從效率方面來講,由于提供了***的靜態(tài)空對象,當判讀對象實例是否為空時(isEmpty),直接使用預制靜態(tài)空對象(emptyString)的地址與當前對象進行比較,如果是同一地址,即可確認當前實例為空對象了。對于preallocate函數(shù),顧名思義,該函數(shù)預分配了指定大小的內(nèi)存空間,后面在使用該String實例時,不必擔心賦值或追加的字符過多而導致頻繁的realloc等操作。
2. 不必在每次調(diào)用它們的時候創(chuàng)建一個新的對象。
還是基于上面的代碼實例,由于所有的空對象都共享同一個靜態(tài)空對象,這樣也節(jié)省了更多的內(nèi)存開銷,如果是strEmpty2方式構(gòu)造出的空對象,在執(zhí)行比較等操作時會帶來更多的效率開銷。事實上,Java在String對象的實現(xiàn)中,使用了常量資源池也是基于了同樣的優(yōu)化策略。該優(yōu)勢同樣適用于單實例模式。
3. 可以返回原返回類型的任何子類型。
在Java Collections Framework的集合接口中,提供了大量的靜態(tài)方法返回集合接口類型的實現(xiàn)類型,如Collections.subList()、Collections.unmodifiableList()等。返回的接口是明確的,然而針對具體的實現(xiàn)類,函數(shù)的使用者并不也無需知曉。這樣不僅極大的減少了導出類的數(shù)量,而且在今后如果發(fā)現(xiàn)某個子類的實現(xiàn)效率較低或者發(fā)現(xiàn)更好的數(shù)據(jù)結(jié)構(gòu)和算法來替換當前實現(xiàn)子類時,對于集合接口的使用者來說,不會帶來任何的影響。本書在例子中提到EnumSet是通過靜態(tài)工廠方法返回對象實例的,沒有提供任何構(gòu)造函數(shù),其內(nèi)部在返回實現(xiàn)類時做了一個優(yōu)化,即如果枚舉的數(shù)量小于64,該工廠方法將返回一個經(jīng)過特殊優(yōu)化的實現(xiàn)類實(RegularEnumSet),其內(nèi)部使用long(64bits在Java中) 中的不同位來表示不同的枚舉值。如果枚舉的數(shù)量大于64,將使用long的數(shù)組作為底層支撐。然而這些內(nèi)部實現(xiàn)類的優(yōu)化對于使用者來說是透明的。
4. 在創(chuàng)建參數(shù)化類型實例的時候,它們使代碼變得更加簡潔。
Map<String,String> m = new HashMap<String,String>();
由于Java在構(gòu)造函數(shù)的調(diào)用中無法進行類型的推演,因此也就無法通過構(gòu)造器的參數(shù)類型來實例化指定類型參數(shù)的實例化對象。然而通過靜態(tài)工廠方法則可以利用參數(shù)類型推演的優(yōu)勢,避免了類型參數(shù)在一次聲明中被多次重寫所帶來的煩憂,見如下代碼:
public static <K,V> HashMap<K,V> newInstance() { return new HashMap<K,V>(); } Map<String,String> m = MyHashMap.newInstance();
二、遇到多個構(gòu)造參數(shù)時要考慮用構(gòu)建器(Builder模式):
如果一個class在構(gòu)造初始化的時候存在非常多的參數(shù),將會導致構(gòu)造函數(shù)或者靜態(tài)工廠函數(shù)帶有大量的、類型相同的函數(shù)參數(shù),特別是當一部分參數(shù)只是可選參數(shù)的時候,class的使用者不得不為這些可選參數(shù)也傳入缺省值,有的時候會發(fā)現(xiàn)使用者傳入的缺省值可能是有意義的,而并非class內(nèi)部實現(xiàn)所認可的缺省值,比如某個整型可選參數(shù),通常使用者會傳入0,然后class內(nèi)部的實現(xiàn)恰恰認為0是一種重要的狀態(tài),而該狀態(tài)并不是該調(diào)用者關(guān)心的,但是該狀態(tài)卻間接導致其他狀態(tài)的改變,因而帶來了一些潛在的狀態(tài)不一致問題。與此同時,過多的函數(shù)參數(shù)也給使用者的學習和使用帶來很多不必要的麻煩,我相信任何使用者都希望看到class的接口是簡單易用、函數(shù)功能清晰可見的。在Effective C++中針對接口的設(shè)計有這樣的一句話:"接口要完滿而最小化"。針對該類問題通常會考慮的方法是將所有的參數(shù)歸結(jié)到一個JavaBean對象中,實例化這個Bean對象,然后再將實例化的結(jié)果傳給這個class的構(gòu)造函數(shù),這種方法仍然沒有避免缺省值的問題。該條目推薦了Builder模式來創(chuàng)建這個帶有很多可選參數(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); }
對于Builder方式,可選參數(shù)的缺省值問題也將不再困擾著所有的使用者。這種方式還帶來了一個間接的好處是,不可變對象的初始化以及參數(shù)合法性的驗證等工作在構(gòu)造函數(shù)中原子性的完成了。
三、用私有構(gòu)造器或者枚舉類型強化Singleton屬性:
對于單實例模式,相信很多開發(fā)者并不陌生,然而如何更好更安全的創(chuàng)建單實例對象還是需要一些推敲和斟酌的,在Java中主要的創(chuàng)建方式有以下三種,我們分別作出解釋和適當?shù)谋容^。
1. 將構(gòu)造函數(shù)私有化,直接通過靜態(tài)公有的final域字段獲取單實例對象:
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elivs() { ... } public void leaveTheBuilding() { ... } }
這樣的方式主要優(yōu)勢在于簡潔高效,使用者很快就能判定當前類為單實例類,在調(diào)用時直接操作Elivs.INSTANCE即可,由于沒有函數(shù)的調(diào)用,因此效率也非常高效。然而事物是具有一定的雙面性的,這種設(shè)計方式在一個方向上走的過于極端了,因此他的缺點也會是非常明顯的。如果今后Elvis的使用代碼被遷移到多線程的應用環(huán)境下了,系統(tǒng)希望能夠做到每個線程使用同一個Elvis實例,不同線程之間則使用不同的對象實例。那么這種創(chuàng)建方式將無法實現(xiàn)該需求,因此需要修改接口以及接口的調(diào)用者代碼,這樣就帶來了更高的修改成本。
2. 通過公有域成員的方式返回單實例對象:
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elivs() { ... } public static Elvis getInstance() { return INSTANCE; } public void leaveTheBuilding() { ... } }
這種方法很好的彌補了***種方式的缺陷,如果今后需要適應多線程環(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)用而帶來的開銷。
3. 使用枚舉的方式(Java SE5):
public enum Elvis { INSTANCE; public void leaveTheBuilding() { ... } }
就目前而言,這種方法在功能上和公有域方式相近,但是他更加簡潔更加清晰,擴展性更強也更加安全。
我在設(shè)計自己的表達式解析器時,曾將所有的操作符設(shè)計為enum中不同的枚舉元素,同時提供了帶有參數(shù)的構(gòu)造函數(shù),傳入他們的優(yōu)先級、操作符名稱等信息。
四、通過私有構(gòu)造器強化不可實例化的能力:
對于有些工具類如java.lang.Math、java.util.Arrays等,其中只是包含了靜態(tài)方法和靜態(tài)域字段,因此對這樣的class實例化就顯得沒有任何意義了。然而在實際的使用中,如果不加任何特殊的處理,這樣的classes是可以像其他classes一樣被實例化的。這里介紹了一種方式,既將缺省構(gòu)造函數(shù)設(shè)置為private,這樣類的外部將無法實例化該類,與此同時,在這個私有的構(gòu)造函數(shù)的實現(xiàn)中直接拋出異常,從而也避免了類的內(nèi)部方法調(diào)用該構(gòu)造函數(shù)。
public class UtilityClass { //Suppress default constructor for noninstantiability. private UtilityClass() { throw new AssertionError(); } }
這樣定義之后,該類將不會再被外部實例化了,否則會產(chǎn)生編譯錯誤。然而這樣的定義帶來的最直接的負面影響是該類將不能再被子類化。
五、避免創(chuàng)建不必要的對象:
試比較以下兩行代碼在被多次反復執(zhí)行時的效率差異:由于String被實現(xiàn)為不可變對象,JVM底層將其實現(xiàn)為常量池,既所有值等于"stringette" 的String對象實例共享同一對象地址,而且還可以保證,對于所有在同一JVM中運行的代碼,只要他們包含相同的字符串字面常量,該對象就會被重用。
我們繼續(xù)比較下面的例子,并測試他們在運行時的效率差異:
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實例對象。這樣在多次調(diào)用后,***種靜態(tài)工廠方法將會避免大量不必要的Boolean對象被創(chuàng)建,從而提高了程序的運行效率,也降低了垃圾回收的負擔。
繼續(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; } }
改進后的Person類只是在初始化的時候創(chuàng)建Calender、TimeZone和Date實例一次,而不是在每次調(diào)用isBabyBoomer方法時都創(chuàng)建一次他們。如果該方法會被頻繁調(diào)用,效率的提升將會極為顯著。
集合框架中的Map接口提供keySet方法,該方法每次都將返回底層原始Map對象鍵數(shù)據(jù)的視圖,而并不會為該操作創(chuàng)建一個Set對象并填充底層Map所有鍵的對象拷貝。因此當多次調(diào)用該方法并返回不同的Set對象實例時,事實上他們底層指向的將是同一段數(shù)據(jù)的引用。
在該條目中還提到了自動裝箱行為給程序運行帶來的性能沖擊,如果可以通過原始類型完成的操作應該盡量避免使用裝箱類型以及他們之間的交互使用。見下例:
public static void main(String[] args) { Long sum = 0L; for (long i = 0; i < Integer.MAX_VALUE; ++i) { sum += i; } System.out.println(sum); }
本例中由于錯把long sum定義成Long sum,其效率降低了近10倍,這其中的主要原因便是該錯誤導致了2的31次方個臨時Long對象被創(chuàng)建了。
六、消除過期的對象引用:
盡管Java不像C/C++那樣需要手工管理內(nèi)存資源,而是通過更為方便、更為智能的垃圾回收機制來幫助開發(fā)者清理過期的資源。即便如此,內(nèi)存泄露問題仍然會發(fā)生在你的程序中,只是和C/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)生任何邏輯問題,然而隨著程序運行時間不斷加長,內(nèi)存泄露造成的副作用將會慢慢的顯現(xiàn)出來,如磁盤頁交換、OutOfMemoryError等。那么內(nèi)存泄露隱藏在程序中的什么地方呢?當我們調(diào)用pop方法是,該方法將返回當前棧頂?shù)膃lements,同時將該棧的活動區(qū)間(size)減一,然而此時被彈出的Object仍然保持至少兩處引用,一個是返回的對象,另一個則是該返回對象在elements數(shù)組中原有棧頂位置的引用。這樣即便外部對象在使用之后不再引用該Object,那么它仍然不會被垃圾收集器釋放,久而久之導致了更多類似對象的內(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)足夠只能和強大,因此沒有必要對所有不在需要的對象執(zhí)行obj = null的顯示置空操作,這樣反而會給程序代碼的閱讀帶來不必要的麻煩,該條目只是推薦在以下3中情形下需要考慮資源手工處理問題:
1) 類是自己管理內(nèi)存,如例子中的Stack類。
2) 使用對象緩存機制時,需要考慮被從緩存中換出的對象,或是長期不會被訪問到的對象。
3) 事件監(jiān)聽器和相關(guān)回調(diào)。用戶經(jīng)常會在需要時顯示的注冊,然而卻經(jīng)常會忘記在不用的時候注銷這些回調(diào)接口實現(xiàn)類。
七、避免使用終結(jié)方法:
任何事情都存在其一定的雙面性或者多面性,對于C++的開發(fā)者,內(nèi)存資源是需要手工分配和釋放的,而對于Java和C#這種資源托管的開發(fā)語言,更多的工作可以交給虛擬機的垃圾回收器來完成,由此C++程序得到了運行效率,卻失去了安全。在Java的實際開發(fā)中,并非所有的資源都是可以被垃圾回收器自動釋放的,如FileInputStream、Graphic2D等class中使用的底層操作系統(tǒng)資源句柄,并不會隨著對象實例被GC回收而被釋放,然而這些資源對于整個操作系統(tǒng)而言,都是非常重要的稀缺資源,更多的資源句柄泄露將會導致整個操作系統(tǒng)及其運行的各種服務(wù)程序的運行效率直線下降。那么如何保證系統(tǒng)資源不會被泄露了?在C++中,由于其資源完全交由開發(fā)者自行管理,因此在決定資源何時釋放的問題上有著很優(yōu)雅的支持,C++中的析構(gòu)函數(shù)可以說是完成這一工作的天然候選者。任何在棧上聲明的C++對象,當棧退出或者當前對象離開其作用域時,該對象實例的析構(gòu)函數(shù)都會被自動調(diào)用,因此當函數(shù)中有任何異常(Exception)發(fā)生時,在棧被銷毀之前,所有棧對象的析構(gòu)函數(shù)均會被自動調(diào)用。然而對于Java的開發(fā)者而言,從語言自身視角來看,Java本身并未提供析構(gòu)函數(shù)這樣的機制,當然這也是和其資源被JVM托管有一定關(guān)系的。
在Java中完成這樣的工作主要是依靠try-finally機制來協(xié)助完成的。然而Java中還提供了另外一種被稱為finalizer的機制,使用者僅僅需要重載Object對象提供的finalize方法,這樣當JVM的在進行垃圾回收時,就可以自動調(diào)用該方法。但是由于對象何時被垃圾收集的不確定性,以及finalizer給GC帶來的性能上的影響,因此并不推薦使用者依靠該方法來達到關(guān)鍵資源釋放的目的。比如,有數(shù)千個圖形句柄都在等待被終結(jié)和回收,可惜的是執(zhí)行終結(jié)方法的線程優(yōu)先級要低于普通的工作者線程,這樣就會有大量的圖形句柄資源停留在finalizer的隊列中而不能被及時的釋放,最終導致了系統(tǒng)運行效率的下降,甚至還會引發(fā)JVM報出OutOfMemoryError的錯誤。
Java的語言規(guī)范中并沒有保證該方法會被及時的執(zhí)行,甚至都沒有保證一定會被執(zhí)行。即便開發(fā)者在code中手工調(diào)用了System.gc和System.runFinalization這兩個方法,這僅僅是提高了finalizer被執(zhí)行的幾率而已。還有一點需要注意的是,被重載的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(); } }
那么在實際的開發(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 //下該方法并不存在,以避免其對運行時效率的影響。 System.out.println("The current status: " + _myStatus); } finally { //在finally中對超類finalize方法的調(diào)用是必須的,這樣可以保證整個class繼承 //體系中的finalize鏈都被執(zhí)行。 super.finalize(); } } }
關(guān)于“Java如何創(chuàng)建和銷毀對象”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對“Java如何創(chuàng)建和銷毀對象”知識都有一定的了解,大家如果還想學習更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。