1. 程式人生 > >Java編程思想 學習筆記5

Java編程思想 學習筆記5

那一刻 基類 都得 char med 匿名內部類 nbsp lean 異常處理

五、初始化與清理

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);

  這時內部的表示形式。我們並不能這樣書寫代碼。

  假設你希望在方法的內部獲得對當前對象的引用。由於這個引用是由編譯器“偷偷”傳入的,所以沒有標識符可用。但是,為此有個專門的關鍵字:thisthis關鍵字只能在方法內部使用,表示對“調用方法的那個對象”的引用。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虛擬機中,堆的實現截然不同:它更像一個傳送帶,每分配一個新對象,它就往前移動一格。這意味著對象存儲空間的分配速度非常快。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.這塊存儲空間會被清零,這就自動地將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);
    }
}
/*output
MEDIUM
*/

  枚舉類型可以使用ordinal()方法,用於表示某個特定enum常量的聲明順序,以及static value()方法,用來按照enum常量的聲明順序,產生這些常量值構成的數組。

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

  enum可以在switch語句內使用。

10.總結

  初始化在Java中占有至關重要的低位。

  學好內存分析對深刻理解Java運行機制十分重要,本文總結的仍舊很不全面,只是取了一部分,任重而道遠啊!

  

  

Java編程思想 學習筆記5