標籤:讀取 who 資料類型 之間 過程 最佳化 回收 checked 硬碟
五、初始化與清理
1.用構造器確保初始化
在Java中,通過提供構造器,類的設計者可確保每個對象都會得到初始化。建立對象時,如果其類具有構造器,Java就會在使用者有能力操作對象之前自動調用相應的構造器,從而保證了初始化的進行。構造器的名稱與類的名稱相同。(“每個方法首字母小寫”的編碼風格並不適用於構造器)
構造器有助於減少錯誤,並使代碼更易於閱讀。從概念上講,“初始化”與“建立”是彼此和獨立的。在Java中,“初始化”和“建立”捆綁在一起,兩者不能分離。
構造器是一種特殊類型的方法,因為它沒有傳回值。這與傳回值為空白(void)明顯不同。構造器不會返回任何東西(new 運算式確實返回了對建立對象的引用,但構造器本身並沒有任何傳回值)。
2.方法重載
在Java裡,構造器是強制重載方法名的一個原因。既然構造器的名字已經由類名所決定,就只能有一個構造器名。那麼要想用多種方式建立一個對象該怎麼辦?假設你要建立一個類,即可以用標準方式進行初始化,也可以從檔案裡讀取資訊來初始化。這就需要兩個構造器:一個預設構造器,另一個取字串作為形式參數——該字串表示初始化對象所需的檔案名稱。由雩都是構造器,所它們必須有相同的名字,即類名。為了讓方法名相同而形式參數不同的構造器同時存在,必須用到方法重載。
①區分重載方法
每個重載方法都必須有一個獨一無二的參數類型列表。甚至參數順序的不同也足以區分兩個方法。不過,不建議這麼做。
②涉及基本類型的重載
基本類型能從一個“較小”的類型自動提升至一個“較大”的類型,此過程一旦牽涉到重載,可能會造成了一些混淆。
常數值會被當作int值處理。
如果傳入的資料類型(實際資料類型)小於方法中聲明的形式參數類型,資料類型就會被提升。char類型略有不同,如果無法找到恰好接受char參數的方法,就會把char直接提升至int型。
如果傳入的實際參數大於重載方法聲明的形式參數,就得通過類型轉換來執行窄化轉換。如果不這樣做,編譯器就會出錯。
③以傳回值區分重載方法
在區分重載方法時,為什麼只能以類名和方法的形參列表作為標準呢?能否用方法的傳回值來區分呢?
答案是不能。
因為有時,我們並不關心方法的傳回值,我們想要的是方法調用的其他效果,這時我們會調用方法而忽略傳回值。例如這樣調用方法: f();
3.預設構造器
預設構造器(又名“無參”構造器)是沒有形式參數的——它的作用是建立一個“預設對象”。如果你寫的類中沒有構造器,則編譯器會自動幫你建立一個預設構造器。但是,如果已經定義了一個構造器(無論是否有參數),編譯器就不會自動建立預設構造器。
4.this關鍵字
如果有同一類型的兩個對象,分別是a和b。你可能想知道,如何才能讓這兩個對象都能調用同一個方法呢:
class Banana { void peel(int i) { /*.....*/ } }public class BananaPeel { public static void main(String[] args){ Banana a = new Banana(), b = new Banana(); a.peel(1); b.peel(2); }}
如果只有一個peel方法,它如何知道是被a調用還是被b調用呢?
為了能用簡便、物件導向的文法來編寫代碼——即“發送訊息給對象”,編譯器做了一些幕後工作。它暗自把“所操作的對象的引用”作為第一個參數傳遞給peel()。所以上述兩個方法的調用就變成了這樣:
Banana.peel(a,1);
Banana.peel(b,2);
這時內部的表示形式。我們並不能這樣書寫代碼。
假設你希望在方法的內部獲得對當前對象的引用。由於這個引用是由編譯器“偷偷”傳入的,所以沒有標識符可用。但是,為此有個專門的關鍵字:this。this關鍵字只能在方法內部使用,表示對“調用方法的那個對象”的引用。this的用法和其他對象引用並無不同。但要注意,如果在方法內部調用用一個類的另一個方法,就不必使用this,直接調用即可。
只在必要處使用this。
①在構造器中使用構造器
可能為了一個類寫了多個構造器,有時可能想在一個構造器中調用另一個構造器,以避免重複代碼。可用this關鍵字做到這一點。
通常寫this的時候,都是指“這個對象”或“當前對象”,而且它本身表示對當前對象的引用。在構造器中,如果為this添加了參數列表,那麼就有了不同的含義。這將產生對符合此參數列表的某個構造器的明確調用。另外,必須把構造器調用置於最起始處,否則編譯器會報錯。
②static的含義
static方法就是沒有this的方法。在static方法的內部不能調用非靜態方法,反過來倒是可以的。(靜態方法可以建立自身的引用,和this效果一樣,通過這個引用可調用非靜態方法,例如main方法。)而且可以在沒有建立任何對象的前提下,僅僅通過類本身來調用static方法。
5.清理:終結處理和記憶體回收
Java有記憶體回收行程負責回收無用對象佔據的記憶體資源。但也有特殊情況:假定你的對象(並非使用new)獲得了一塊“特殊”的記憶體地區,由於記憶體回收行程只知道釋放那些經由new分配的記憶體,所以它不知道該如何釋放該對象的這塊“特殊”記憶體。所以,Java允許在類中定義一個名為finalize()的方法。它的工作原理“假定”是這樣的:一旦記憶體回收行程準備好釋放對象佔用的儲存空間,將首先調用finalize()方法,並且在下一次記憶體回收動作發生時,才真正回收對象佔用的記憶體。
這裡有一個潛在的編程陷阱,因為有些程式員(特別是C++程式員)剛開始可能會誤把finalize()當作C++中的折構函數(C++中銷毀對象必須用到這個函數)。所以有必要明確區分一下:在C++中,對象一定會被銷毀;而Java裡的對象卻並非總是被記憶體回收。或者換句話說:
1.對象可能不被記憶體回收。
2.記憶體回收不等於“折構”。
這意味著在你不再需要某個對象之前,如果必須執行某些動作,那麼你得自己去做。Java並未提供“折構函數”或相似的概念,要做類似的清理工作,必須自己動手建立一個執行清理工作的普通方法。例如,假設某個對象在建立過程中會將自己繪製到螢幕上,如果不是明確地從螢幕上擦除,它可能永遠都得不到清理。如果在finalize()裡加入某種擦除功能,當“記憶體回收”發生時(不能保證一定發生),finalize()得到了調用,映像就會被擦除。要是“記憶體回收”沒有發生,映像就會一直保留下來。
· 也許你會發現,只要程式沒有瀕臨儲存空間用完的那一刻,對象佔用的空間就不會被釋放。如果程式執行結束,並且記憶體回收行程一直都沒有釋放你建立的任何對象的儲存空間,則隨著程式的退出,那些資源也會全部交還給作業系統。這個策略是恰當的,因為記憶體回收本身也有開銷,要是不使用它,那就不用支付這部分的開銷了。
①finalize()的用途何在
finalize()的真正用途是什麼呢?
這就引出需要記住的第三點:
3.記憶體回收只與記憶體有關。
也就是說,使用記憶體回收行程的唯一原因是為了回收程式不用的記憶體。無論對象是怎樣建立的,記憶體回收行程都會負責釋放對象佔據的所以記憶體。這就將對finalize()的需求限制到一種特殊情況,即通過某種建立對象方式以外的方式為對象分配了儲存空間。但是,Java中一切皆為對象,這種特殊情況又是怎麼回事呢?
之所以要有finalize(),是由於在分配記憶體時可能採用了類似C語言中的做法。這種情況主要發生在使用“本地方法”的情況下,本地方法是一種在Java中調用非Java代碼的方式。本地方法目前只支援C和C++,但它們可以調用其他語言的代碼,所以實際上可以調用任何代碼。在非Java代碼中,可能會調用C的malloc()函數系列來分配儲存空間,而且除非調用了free()函數,否則儲存空間將得不到釋放,從而造成記憶體流失。當然,free()是C和C++中的函數,所以需要在finalize()中用本地方法調用它。
②你必須實施清理
要清理一個對象,使用者必須在需要清理的時刻調用執行清理動作的方法。
Java不允許建立局部對象,必須使用new建立對象。記住,無論是記憶體回收還是終結,都不保證一定發生。如果Java虛擬機器並未面臨記憶體耗盡的情形,它是不會浪費時間去執行記憶體回收的。
③終結條件
通常,不能指望finalize(),必須建立其他的“清理”方法,並且明確地調用它們。看來,finalize()只能存在於程式員很難用到的一些晦澀用法裡了。不過,finalize()還有一個有趣的用法,它並不依賴每次都要對finalize()進行調用,這就是對象終結條件的驗證。
當對某個對象不再感興趣——也就是它可以被清理了,這個對象應該處於某種狀態,使它佔用的記憶體可以被安全的釋放。例如,要是對象代表了一個開啟的檔案,在對象被回收前程式員應該關閉這個檔案。只要對象中存在沒有被適當清理的部分,程式就存在很隱晦的缺陷finalize()可以用來最終發現這種情況——儘管它並不總是被調用。如果某次finalize()的動作使得缺陷被發現,那麼就可據此找出問題所在——這才是人們真正關心的。
以下是個簡單的例子:
class Book { boolean checkedOut = false; Book(boolean checkedOut) { this.checkedOut = checkedOut; } void checkIn() { checkedOut = false; } protected void finalize() { if(checkedOut) { System.out.println("Error: checked out"); // 一般需要這麼做,假設基類版本的finalize()也要做某些事情 //由於需要異常處理,這裡省略// super.fianlize(); }}public class TerminationCondition { public static void main(String[] args) { Book novel = new Book(true); novel.checkIn(); new Book(true); Sysytem.gc(); }}
本例的終結條件是:所有的Book對象在被當作記憶體回收前都應該被簽入(check in)。但在main()方法中,由於程式員的錯誤,有一本書未被簽入。要是沒有finalize()來驗證終結條件,將很難發現這種缺陷。
注意,System.gc()用於強制進行終結動作。即使不這麼做,通過重複地執行程式,最終也能找出錯誤的Book對象。
④記憶體回收行程如何工作
Java從堆分配空間的速度,可以和其他語言從堆棧上分配空間的速度相媲美。
打個比方,你可以把C++裡的堆想象成一個院子,裡面每個對象都負責管理自己的地盤。一段時間後,對象可能被銷毀,但地盤必須加以重用。在某些Java虛擬機器中,堆的實現截然不同:它更像一個傳送帶,每分配一個新對象,它就往前移動一格。這意味著Object Storage Service空間的分配速度非常快。Java的“堆積指標”只是簡單地移動到尚未分配的地區,其效率比得上C++在堆棧上分配空間的效率。
事實上,Java中的堆未必完全像傳送帶那樣工作。要真是那樣的話,勢必會導致頻繁的記憶體頁面調度——將其移出移進硬碟,因此會顯得需要擁有比實際需要更多的記憶體。頁面調度會顯著地影響效能,最終,在建立了足夠多的對象之後,記憶體資源將耗盡。其中的秘密在於記憶體回收行程的介入。當它工作時,將一邊回收空間,一邊使堆中的對象緊湊排列,這樣“堆積指標”就可以很容易移動到更靠近傳送帶的開始處,也就盡量避免了分頁錯誤。通過記憶體回收行程對對象的重新排列,實現了一種高速的、有無限空間可供分配的堆模型。
“自適應的、分代的、停止-複製、標記-清掃”式記憶體回收行程。
Java虛擬機器中有許多附加技術用以提升速度。尤其是與載入器操作有關的,被稱為“即時”編譯器的技術。這種技術可以把程式全部或部分翻譯成本機機器碼,程式運行速度得以提升。當需要裝載某個類(通常是在為該類建立第一個對象)時,編譯器會先找到其.class檔案,然後將該類的位元組碼裝入記憶體。此時,有兩種方案可供選擇。一種是就讓即時編譯器編譯所有代碼,另一種則為即時編譯器只在需要時才編譯代碼,被稱為惰性評估。新版JDK中的Java HotSpot技術採用了類似方法,代碼每次被執行的時候都會做一些最佳化,所以執行的次數越多,它的速度越快。
6.成員初始化
Java儘力保證:所有變數在使用前都能得到恰當的初始話化。對於方法的局部變數,Java以編譯時間錯誤的形式來貫徹這種保證。
7.構造器初始化
可以用構造器來進行初始化。但要牢記:無法阻止自動初始化的進行,它將在構造器被調用之前發生。
①初始化的順序
在類的內部,變數定義的先後順序決定了初始化的順序。即使變數定義散佈於方法定義之間,它們仍舊會在任何方法(包括構造器)被調用之前得到初始化。
②待用資料的初始化
無論建立多少個對象,待用資料都只佔用一份儲存地區。static關鍵字不能應用於局部變數,因此它只能作用於域。
靜態初始化只在必要時才會進行。只有在第一次建立對象(或者第一次訪問待用資料)的時候,它們才會被初始化。此後,靜態對象不會再次被初始化。
初始化的順序是先靜態對象,而後是“非靜態”對象。
總結一下對象的建立過程,假設有個名為Dog的類:
1.即使沒有顯式地使用static關鍵字,構造器實際上也是靜態方法。因此,當首次建立類型為Dog的對象時(構造器可以看成靜態方法),或者Dog類的靜態方法/靜態域首次被訪問時,Java解譯器必須尋找類路徑,以定位Dog.class檔案。
2.然後載入Dog.class(這會建立一個Class對象),有關靜態初始化的所有動作都會執行。因此,靜態初始化只在Class對象首次載入的時候進行一次。
3.當用new Dog()建立對象時,首先在堆上為Dog對象分配足夠的儲存空間。
4.這Block Storage空間會被清零,這就自動地將Dog對象中的所有基本類型資料都設定成了預設值,而引用則設定成null。
5.執行所有出現於欄位定義處的初始化動作。
6.執行構造器。
③顯式的靜態初始化
Java允許將多個靜態初始化動作組織成一個特殊的“靜態子句”(有時叫做“靜態塊”):
public class fool { static int i; static { i = 666; }}
與其他靜態初始化動作一樣,這段代碼僅執行一次:當首次產生這個類的一個對象時,或者首次訪問屬於那個類的待用資料成員時(即使從未產生過那個類的對象)。
④非靜態執行個體初始化
Java中也有被稱為執行個體初始化的類似文法,用來初始化每一個對象的非靜態變數。
它與靜態初始化子句一模一樣,只不過少了static關鍵字。這種文法對於支援“匿名內部類”的初始化是必須的。
執行個體初始化子句是在構造器之前執行的。
8.數組初始化
數組只是相同類型的,用一個標識符名稱封裝到一起的一個對象序列或基本類型資料序列。數組是通過方括弧下標操作符[ ]來定義和使用的。
編譯器不允許指定數組的大小。這就又把我們帶回有關“引用”的問題上。現在擁有的只是對數組的一個引用(你已經為該引用分配了足夠的儲存空間),而且也沒給數組對象本身分配任何空間。可以用由一對花括弧括起來的值執行數組的初始化。
也可以用花括弧括起來的列表來初始化對象數組。
①可變參數列表
可變參數列表表現形式如下:
void f(int... a){ }
這樣就相當於有個int[] 的參數。有了可變參數列表,就再也不用顯式地編寫數組文法了,當你指定參數時,編譯器實際上會為你去填充數組。你擷取的仍舊是一個數組。
注意:你應該總是只在重載方法的一個版本上使用可變參數列表,或者壓根就不是用它。
9.枚舉類型
在Java SE5中添加了enum關鍵字,它使得我們在需要群組並使用枚舉類型時,可以很方便地處理。下面是一個簡單的例子:
public enum Spiciness { NOT, MILD, MEDIUM, HOT, FAMING}
這裡建立了一個名為Spiciness的枚舉類型,它具有5個具名值。由於枚舉類型的執行個體是常量,因此按照命名習慣它們都用大寫字母表示。
為了使用enum,需要建立一個該類型的引用,並將其賦值給某個執行個體:
public class SimpleEnumUse { public static void main(String[] args) { Spiciness howHot = Spiciness.MEDIUM; Sysytem.out.println(howHot); }}/*outputMEDIUM*/
枚舉類型可以使用ordinal()方法,用於表示某個特定enum常量的聲明順序,以及static value()方法,用來按照enum常量的聲明順序,產生這些常量值構成的數組。
儘管enum看起來像是一種新的資料類型,但是這個關鍵字只是為enum產生對應的類時,產生了某些編譯器行為,因此在很大程度上,你可以將enum當作其他任何類來處理。事實上,enum確即時類,並且具有自己的方法。
enum可以在switch語句內使用。
10.總結
初始化在Java中佔有至關重要的低位。
學好記憶體分析對深刻理解Java運行機制十分重要,本文總結的仍舊很不全面,只是取了一部分,任重而道遠啊!
Java編程思想 學習筆記5