Java編程思想:第五章:初始化與清理

第五章:初始化與清理


    隨着計算機革命的發展,“不安全”的編程方式已逐漸成爲編程代價昂貴的代價之一。

    初始化與清理正式涉及安全的兩個問題,Java提供了構造器,垃圾回收器,對於不在使用的內存資源,垃圾回收器自動回收。


5.1 用構造器確保初始化

    Java中通過構造函數,類的設計者可以確保每個對象都會被初始化。創建對象時,如果其了具有構造器,Java就會在用戶有能力操作對象之前自動調用相應的構造器,從而保證了初始化的進行。

    接下來的問題就是如何命名這個方法,有兩種問題:

    • 所取的任何名字都可能與類的某個成員名稱相沖突。

    • 調用構造器是編譯器的責任,所以必須讓編譯器知道應該調用那個方法。

    Java中構造器用於類相同的名字,考慮到初始化時自動調用構造器。

    請注意,由於構造器的名字必須與類名相同,所以“每個方法首字母小寫“編程風格並不適合構造器。不接收任何參數的構造器稱爲:默認構造器。或者無參數構造器。構造器也可以有參數,以便制定如何創建對象。有了構造器形式參數,就可以在初始化對象時提供實際參數。

    構造器有助於減少錯誤,並使代碼更容易閱讀。從概念上講“初始化”和“創建“是彼此獨立的,然而在上面的代碼中,你卻早不到initialize()方法的明確調用。在Java中,”初始化“和”創建”捆綁在一起,兩者不能分離。

    構造器是一種特殊類型的方法,因爲它沒有返回值。這與返回值爲空(void)明顯不同,對於空返回值,儘管方法本身不會自動返回什麼,單仍可選擇它返回別的東西。構造器則不會返回任何東西。


5.2 方法重載

    任何程序設計語言都具備一項重要特性就是對名字的運用。當創建一個對象時,也就給此對象分配到了存儲空間取了一個名字。所謂方式則是給某個動作取的名字。通過使用名字,你可以引用所有的對象和方法。名字起的好可以使系統更易於理解和修改。

    相同的詞兒可以表達集中不同的含義,這就是重載。特別是含義之間差異特別小,這種方式十分有用。大多數人類語言有很強的冗餘性,所以即使漏掉了幾個詞兒,仍然可以推斷出含義。

    大多數程序設計語言要求每一個方法都提供一個獨一無二的標識。所以絕對不能用print()函數顯示了整數以後,又用一個名字爲print()的函數顯示浮點數。每個函數都要唯一的名稱。

    爲了讓方法名一樣,形參名字不一樣的構造器同時存在,必須用到方法重載。同時,方法重載是構造器所必須的。


區分重載方法

    有幾個方法有相同的名字,Java怎麼知道你指的是哪一個呢?其實規則很簡單:每個重載的方法都必須有一個獨一無二的參數類型列表。畢竟,對於名字相同的方法,除了參數類型的差異之外,還有什麼辦法把它們區分呢。


涉及基本類型的重載

    基本類型可以從一種較小的類型自動提升到較大的類型。此過程一旦涉及到重載,可能會造成混亂。

    方法接收較小的基本類型作爲參數,如果傳入的實際參數較大,就得通過類型轉換來進行窄化轉換,如果不這樣做,編譯器就會報錯。


以返回值來區分重載方法

    方法的返回值來區分重載方法是行不通的。


5.3 默認構造器

    默認構造器是沒有形式參數的,它的作用就是創建一個默認對象。如果你寫的類中沒有默認構造器,則編譯器會自動幫你創建一個默認構造器。如果已經定義了一個構造器(無論是有參還是無參),編譯器都不會幫你創建構造器。


5.4 this關鍵字

    爲了面向對象方式來編寫代碼,編譯器幕後做了些工作,它暗自把“所操作對象的應用”作爲第一個參數傳遞給方法x()中,比如:引用名.方法名()。這是內部的表示形式,我們並不能這樣書寫代碼,並試圖通過編譯。但這種寫法的確幫你理解實際所發生的事情。

    假設,你希望在方法的內部獲得當前對象的引用。由於這個引用是編譯器“偷偷”傳入的,所以沒有標識符可用。但是,爲此有個專門的關鍵字:this。this關鍵字只能在方法的內部使用,表示對”調用方法的那個對象“的引用。this的用法和其他對象引用並無不同。但是要注意,如果再方法內部調用同一個類的另一個方法,就不必使用this,直接調用即可。當前方法中的this引用自動應用於同一個類中的其他方法。

    只要當需要明確指出對當前對象的引用時,才需要使用this關鍵字。例如需要返回當前對象引用時,常常return語句這樣寫:return this;


在構造器中調用構造器

    可能在一個類寫了多個構造器,有時可能想在一個構造器中調用另一個構造器,以避免重複代碼。this關鍵字可以做到這一點。

    通常寫this的時候,都是指“當前對象”,而且它本身表示對當前對象的引用。在構造器中,如果爲this添加了參數列表,那麼就用了不同的含義。這將產生對符合此參數列表的某個構造器的明確調用。這樣,調用其他構造器就有了直接的途徑。

    儘管用this可以調用另一個構造器,但是不能調用兩個,此處,必須將構造器調用置於第一行,否則編譯器出錯。

    除了構造器內之外,編譯器不允許其他任何地方調用構造器。


static的含義

    瞭解this關鍵字後,就能更全面的理解static(靜態)方法的含義。static方法就是沒有this的方法。在static方法的內部不能調用非靜態的方法。反過來倒是可以的。而且沒有創建任何對象的前提下,僅僅通過類本身來調用static方法。這實際上就是static方法的主要用途。它很像全局方法。Java中禁止使用全局方法,但你在類中置入static方法就可以訪問其他static方法和static域。

    有些人認爲static方法不是面向對象的,因爲它們的確具有全局函數的語義;使用static方法時,確實不存在this,所以不是通過“向對象發送消息”的方式來完成的。的確,代碼中出現了大量的static方法,就該重新考慮自己的設計了。然而,static的概念有其適用之處,許多時候需要用到它。至於它是否是面向對象的,就留給理論家去討論吧。


5.5 清理,終結處理和垃圾回收

    清理工作很重要,當然,垃圾回收器負責回收無用的內存資源,有些情況下(非new創建的對象)獲得了一個特殊的區域。由於垃圾回收器只回收new的方式創建出來的對象的資源,所以它不知道該如何釋放該對象的這一塊特殊的內容。爲了應對這種情況,Java允許在類中定義一個finalize()方法。它的工作原理假設是這樣的,一旦垃圾回收期準備好釋放對象佔用的存儲空間,將首先調用其finalize()方法,並且在下一次垃圾回收動作發生時,纔會真正回收對象佔用的內存。所以要是你打算用finalize()方法,就能在垃圾回收時刻做一些重要的清理工作。

    Java裏的對象卻並非總是被垃圾回收,或者換句話說:

    • 對象可能不被垃圾回收。

    • 垃圾回收並不等於“析構“。

    這意味着在你不在需要某個對象之前,如果必須執行某些動作,那麼你的自己去做。Java並未提供析構函數或者相似的概念,要做類似的清理工作,必須自己動手創建一個執行清理工作的普通方法。

    也許你會發現,程序沒有用完存儲空間,對象佔用的空間也得不到釋放。如果程序執行結束,並且垃圾回收期一直都沒有釋放你創建的任何對象的存儲空間,則隨着程序的退出,那些資源也會全部交換給操作系統。這個策略是恰當的,因爲垃圾回收本身也有開銷,要是不適用它,那就不用支付這些開銷了。


finalize()的用途何處

    垃圾回收至於內存有關,也就是說,使用垃圾回收器的唯一原因是爲了回收程序不再使用的內存。所以對垃圾回收器有關的任何行爲來說(尤其是finalize()方法),它們也必須同內存及其回收有關。

    這就將對finalize()的需求限制到了一種特殊情況,即通過某種創建對象方式以外的方式爲對象分配了存儲空間。在Java中,一切都是對象,這個特殊情況到底是什麼了呢?

    看來之所以要有finalize(),是由於在分配內存時可能採用了類似於C語言的做法,而非Java中的通常做法。這種情況主要發生在使用“本地方法”的情況下。本地方法是一種在Java中調用非Java代碼的方式。本地方法目前(jdk6)只支持C和C++。但是它們可以調用其他語言寫的代碼,所以實際上可以調用任何語言的代碼。


你必須實施清理

    要清理一個對象,用戶必須在需要清理的時刻調用執行清理動作的方法。如果希望進行除釋放存儲空間之外的清理工作,還是得明確調用某個恰當的Java方法,這就等於使用析構函數了,就沒它方便而已。

記住,無論是垃圾回收還是終結,都不保證一定會發生。如果Java虛擬機並未面臨內存消耗的情況,它是不會浪費時間去執行垃圾回收以恢復內存的。


終結條件

    通常,不能指望finalize(),必須創建其他的清理方法,並且明確的調用它們。不過,finalize()還有一個有趣的用法,它並不依賴於每次都要對finalie()進行調用,這就是對象終結條件的驗證。

如果某個對象不再使用時,這個對象處於某種狀態,使它專用的內存可以被安全的釋放。

垃圾回收器如何工作

    垃圾回收器對於提高對象的創建速度,卻具有明顯的效果。聽起來很奇怪,存儲空間的釋放會影響存儲空間的分配,但這確實某些Java虛擬機的工作方式。這也意味着,Java從堆分配空間的速度,可以和其他語言從堆棧上分配空間的速度相近。

    垃圾回收算法多,Java採用自適應的選擇一個執行。取決於不同的虛擬機實現。

 

5.6 成員初始化

    Java盡力保證,所有的變量在使用前都能得到恰當的初始化。對於方法的局部變量,Java以編譯時錯誤的形式來保證必須初始化。

    類的基本類型的成員變量,情況就會變得有些不同,類的每一個基本類型數據成員保證都會有一個初始值。

    類中定義了的對象應用,如果不將其初始化,此引用就會獲得一個特殊的null值。


指定初始化

    如果想某個變量賦初值,該怎麼做呢?有一種很直接的辦法,就是在定義類成員變量的地方爲其賦值,比如 String x="alpha";,成員對象初始化也類似。

    定義,調用按照順序出現,先定義,後調用,反了編譯器就會發出警告。


5.7 構造器初始化

    可以用構造器來進行初始化。在運行時刻,可以調用方法或執行某些動作來決定初值,這爲編程帶來了更多的靈活性。記住,無法阻止自動初始化的進行,它將在構造器被調用之前發生。

    包括所有基本類型和對象引用,包括在定義時已經指定初始值的變量,這種情況都是成立的;因此,編譯器不會強制你一定要在構造器的某個地方或使用它們之前對元素進行初始化:因爲初始化早已得到了保證。


初始化順序

    在類內部,變量定義的先後順序決定了初始化的順序。即使變量定義散步月方法定義之間,它們仍舊會在任何方法被調用之前得到初始化。


靜態數據的初始化

    無論創建多少個對象,靜態數據都只佔用一份存儲空間。static關鍵字不能用於局部變量,因此它只能作用於域。

    如果一個域是基本類型的,且也沒有對它進行初始化,那麼它就會獲得基本類型的標準賦值。

    如果它是一個對象引用,那麼它的默認初始化值就是null了。

    靜態初始化只有在必要時刻才進行,只一次初始化的。初始化的順序是靜態對象,而後面是非靜態對象。

    總結下對象的創建過程,如下:

    • 首次創建對象時,或者靜態方法或者靜態域首次訪問時,Java解析器必須查找類路徑,定位Class文件。

    • 加載Class文件,有關靜態初始化的所有動作都會執行。因此,靜態初始化僅在Class對象首次加載時執行一次。

    • 當創建對象的時候,首先在堆上給該對象分配內存空間。

    • 這塊存儲空間會被清零,這就自動的對象中的所有的基本類型數據設置成了默認值。而引用被設置成null。

    • 執行所有出現於字段定義處的初始化動作。

    • 執行構造器。


顯示的靜態初始化

    Java允許將多個靜態初始化組織成一個特殊的靜態子句(靜態塊),如:static {}。與其他靜態初始化動作一樣,這類代碼也執行一次;當首次生成這個類的對象時,或者首次訪問這個類的靜態數據成員時(即便從未生成過這個了的對象)。就會被執行。


非靜態實例初始化

    Java中也有類似於實例初始化的語法,用來初始化每一個對象的非靜態變量。比如:{}。

    看起來它與靜態初始化語句一模一樣,只不過少了個static關鍵字,這種語法對於支持“匿名內部類"的初始化是必須的,但是他也使得你可以保證無論調用了哪個顯示構造器,某些操作都會發生。

    實例初始化子句在構造器執行之前執行。


5.8 數組初始化

    數組只是相同類型的,用一個標識符名稱封裝到一起的一個對象序列或基本數據類型序列。數組是通過方括號[]來定義和使用,要定義一個數組,只需要在類型後加上一對空方括號即可。比如:int[] a1;

    編譯器不允許制定數組的大小,這就又把我們帶回到有關“引用”的問題上了。現在擁有的只是對數組的一個引用(你已經該數組分配了足有的內存空間),而且也沒有給數組對象本身分配任何內存空間。爲了給數組創建響應的存儲空間,必須寫初始化表達式。對於數組,初始化動作可以出現在代碼的任何地方,但也可以使用一種特殊的初始化方式,它必須在創建數組的地方出現,這種特殊的初始化是由一對花括號{}括起來的值組成的,在這種情況下,存儲空間的分配(等價於使用new)將由編譯器負責。如:int[] a = {1, 2, 3, 4};

    所有的數組(無論他們的元素是基本類型的還是對象)都有一個固定的成員,可以通過它獲知數組內包含了多少個元素,但不能其修改。這個成員就是length。Java中的數組計數也是0開始的,所以能使用的最大下標數是length-1。要是超出這個邊界,就拋出運行時錯誤。

    如果編寫程序時不能確定數組到底有多少個元素時,那麼該怎麼辦呢?可以直接用new在數組裏創建元素。儘管創建的是基本類型的數組,new仍然可以工作(不用new創建單個的基本類型數據)。比如:int[] a= new int[];

    數組的創建是運行時進行的。數據元素中的基本類型自動初始化爲空值(對於數字和字符,是0,布爾類型是,false)。

    如果你創建了一個非基本類型的數組,那麼你就創建了一個已用數組,比如:Integer i=new Integer[20];,如果忘記初始化直接調用,會產生運行時異常。


可變參數列表

    參數類型和個數不確定的場合,在Java中所有的類直接或者間接的繼承Object類,所以可以創建Object數組,比如:Object[] obj;

    可變參數,如下:Object... obj;

    有了可變參數,再也不用顯示的編寫數組語法了,當你指定參數時,編譯器實際上會爲你去填充數組。你獲得的仍然是一個數組。但是,不僅僅是從元素列表到數組的自動轉換。因此,如果你有一組事務,可以把它當做列表傳遞,而如果你已經有了一個數組,該方法可以把它們當做可變參數來接收。

    可變參數把重載變複雜了,你可以在重載方法的一個版本上使用可變參數,當然有可以不適用。


5.9 枚舉類型

    enum關鍵字,它使得我們在需要羣組並用枚舉類型集時,可以很方便地處理。比如: public enum Enumber {N, A, C, D}。

    由於枚舉類型的實例是常量,因此按照命名慣例用大寫字母表示(多個單詞下劃線分割哦)。

    爲了使用enum,必須創建一個該類型的應用,比如:Enumber  h = new Enumber.N;

    在你創建enum時,編譯器會自動添加一些有用的特性。如:toString()方法,ordinal()方法,values方法。

    儘管enum看起來是新的數據類型,但是這個關鍵字只是爲enum生成對應的類時,產生了某些編譯器行爲,因此在很大程度上,你可以將enum當做其他任何類來處理,事實上,enum確實是類,並且具有自己的方法。

    由於switch在要在有限的可能值集合中進行選擇,因此它與enum正式絕對組合,大體上,你可以enum做用另一種創建數據類型的方式,然後直接將所得到的類型拿來用。這正是關鍵所在,因此你不必過度的考慮他們。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章