初始化和清理

初始化和清理

初始化和清理正是涉及安全的兩個問題。在之前的程序中一大部分錯誤都源自于不正確的初始化以及清理工作。在Java中具有一系列的初始化機制保證數據對象的合理初始化,并且采用垃圾回收器機制保證對象內存的回收問題。

1. Java中的初始化機制

在Java的類定義中主要涉及到類屬性(即靜態域),對象屬性以及方法中的局部變量,對它們進行初始化主要有兩種方法:

  • 在變量定義時提供初始值;(基本數據類型初始化為false或0, 對象類型初始化為null)
  • 在變量定義時不提供初始值,并強制其在定義時就初始化(否則出現編譯期錯誤);

第一種方式的優點是提供初始值使得編程更加靈活,而缺點是如果忘記對其正確初始化,使用變量的默認初始值引起錯誤而且不易察覺。第二種方式的優點是強制對其正確初始化,缺點是在變量定義時就進行初始化,缺乏靈活性,而且定義時就初始化,后期變量在使用時再做變化,就會引起兩次初始化的開銷。在Java中,類屬性和對象屬性采用第一種方式,尤其是對象屬性,在實例化不同對象時,變量值需要不同值,采用第一種方式并引入構造器的方式在構造器中對對象屬性進行初始化,增加了編程的靈活性(實例化不同對象,其對象屬性不同,代表著不同對象的不同狀態),同時避免了第二種方式中兩次初始化的開銷(為變量提供初始值的初始化開銷較小)。而方法中的局部變量則采用第二種方式,局部變量在方法被調用時才會使用并初始化,并不代表對象的狀態,因此不需要多樣性,只需要滿足方法的邏輯即可,因此采用第二種方式還可以避免不正確的初始化帶來的錯誤。

方法的重載

方法重載是指方法名稱相同而參數列表不同,他們代表著同一類的業務邏輯,但作用對象可能不同。為什么引入方法的重載,是因為在程序語言中,名稱(變量或方法)代表一個內存地址或者函數的入口地址,是我們對內存操作的一個代號,由于內存地址不易記憶和識別,因此使用帶有一定含義的名稱替代,而在編程過程中經常出現相似的一類邏輯,邏輯大致相同而作用對象不同,需要定義不同的方法,為了區分則需要較長的方法名稱造成一定的編碼負擔,因此引入方法重載機制,使用相同方法名,參數列表不同即可區分不同函數,至于內部的方法簽名則是使用的方法名稱加參數列表,而編程過程中無需考慮減少了負擔。
引入方法重載的同時也帶來一定的問題,就是在方法調用時傳入參數,系統需要根據參數類型解析選擇正確的方法,如果參數類型與方法的參數列表中的類型一一對應時不會發生錯誤,選擇也很容易,但是如果出現需要類型轉換時,則會出現一定問題,而系統選擇的原則時,對方法調用的參數進行向上轉型,選擇最近可用方法進行調用。

最后需要注意的問題,參數列表中類型相同,但順序不同也可以作為重載,但是需要盡量避免此類的行為。

構造器

Java引入構造器保證對對象屬性的初始化。構造器用于定義的類進行實例化不同的對象,因此可以在對象屬性默認初始化以后,在構造器中傳入不同參數的方式對對象屬性進行不同的初始化,從而實例化出不同狀態的對象。構造器可以理解為一種靜態方法,沒有返回值,其名稱與類名相同,所以需要不同的構造器時必須重載,這也是引入重載的另一個原因(也是為什么在這里介紹方法的重載)。構造器的主要作用就是對象的初始化,主要針對對象屬性。
類的定義中,如果沒有定義構造器,則編譯器會為類構造一個默認構造器(即無參構造器,其內沒有任何初始化行為),而類中如果定義了構造器,則不再構造默認構造器(不代表沒有無參構造器,可以自己定義)。在構造器中,其邏輯為首先調用其父類構造器(沒有指明則調用父類無參構造器,如果父類沒有無參構造器則出現編譯期錯誤,也可以指定調用父類的哪一個構造器,使用super(args),且該句位于構造器的第一行,否則也會出現編譯期錯誤),然后編寫對象屬性進行初始化的邏輯,通過構造器的參數列表定義對象狀態的多樣性。

這里尤其注意父類構造器的調用問題,如果自定義的類繼承某個類,而該類沒有無參構造器,則自定義的類必須定義構造器,因為編譯期給添加的默認構造器為無參的且沒有任何執行邏輯的構造器,所以其內部必然默認調用父類的無參構造器,而父類不存在無參構造器,需要明確調用,因此自定義類必須定義一個構造器,至于參數列表是什么類型沒有限制,但是構造器內部的第一行必須明確調用父類的某個存在的構造器。

為了簡化編程在構造器的邏輯編寫過程中還可以調用重載的其他構造器,使用this(args)的方式,這個調用的位置沒有限定。

最后注意構造器可以理解為類的靜態方法,沒有返回值(即void),用于實例化對象,而實例化的對象并不是構造器的返回值。

初始化順序

對于初始化問題可以分為兩個過程,即類的初始化和對象的初始化。類的初始化即類實例的加載,就是在虛擬機中實例化該類的class對象,這個過程主要執行類屬性的初始化,即靜態域和靜態塊的初始化,其順序為首先父類的類初始化,然后按照聲明順序執行靜態域或者塊的初始化,類的初始化觸發條件包括兩個,一個是使用類的靜態域或靜態方法(這一種情況不會觸發對象初始化),第二個是對象實例化(即觸發對象的初始化);對象的初始化主要執行對象屬性的初始化,即屬性中的非靜態域,其順序為按照聲明首先執行父類的對象初始化,然后順序執行對象屬性的默認初始化或者顯示聲明的初始化(這里可以理解為在子類實例化一個對象時,需要首先實例化一個父類作為它的一個屬性包含其中),最后調用構造器。

因此,初始化的順序可以總結為:父類的類初始化,子類的類初始化,父類的對象初始化(先初始化對象屬性,然后調用構造器),子類的對象初始化(順序同樣也是先初始化屬性,在調用構造器)

代碼樣例:

package initialization;

public class OrderTest {

    
    //static int a = Child.sField1;   //執行語句1
    public static void main(String[] args) {
        System.out.println("before new child");
        //Child child = new Child();    //執行語句2
        
    }

}

class Parent{
    private int parentField = getParentField();
    static int sParentField =  getSParentField();
    static{
        System.out.println("parent static block");
    }
    
    
    public Parent(){
        System.out.println("parent constructor");
    }

    private static int getSParentField() {
        System.out.println("getSParentField");
        return 0;
    }

    private int getParentField() {
        System.out.println("getParentField");
        return 0;
    }
}

class Child extends Parent{
    private int field1 = getField1();
    static int sField1 =  getSField1();
    static{
        System.out.println("static block");
    }   
    static int sField2 =  getSField2();
    
    public Child(){
        System.out.println("Child constructor");
    }
    
    private int field2 = getField2();
    private static int getSField1() {
        System.out.println("getSField1");
        return 0;
    }
    
    private static int getSField2() {
        System.out.println("getSField2");
        return 0;
    }
    
    private int getField1(){
        System.out.println("getField1");
        return 0;
    }
    private int getField2(){
        System.out.println("getField2");
        return 0;
    }
    

}

在只有執行語句1時,即只會觸發類的初始化,其輸出結果為:

getSParentField
parent static block
getSField1
static block
getSField2
before new child

在只有執行語句2時,即實例化對象時,其輸出結果為:

before new child
getSParentField
parent static block
getSField1
static block
getSField2
getParentField
parent constructor
getField1
getField2
Child constructor

可以看出即使在構造器之后聲明的屬性也會在構造器之前初始化。

最后注意類的初始化,即靜態域和靜態塊的初始化只執行一次,后續的使用靜態域以及實例化對象都不會再次出發類的初始化,只執行后續的對象初始化。

2. Java中的終結處理和垃圾回收

在Java中,通過new操作生成的對象其內存都分配在堆空間中,Java通過垃圾回收器負責回收程序中不再使用的對象所占的內存。垃圾回收器只負責回收程序中廢棄對象占用的內存,然而程序運行過程中還會占用一些其他資源需要釋放,比如打開的文件,連接的網絡等。

不可使用的終結方法finalize()方法

類似于C++中的析構函數,Java中引入了finalize()方法,其工作原理假定為:一旦垃圾回收器準備好釋放對象所占用的內存空間,首先調用對象的finalize()方法,并且在下一次垃圾回收動作發生時釋放對象占用的內存空間。但是該方法并不一定得到調用,所以在該方法中清理除內存以外的資源,是不合理的。

Java中垃圾回收問題存在以下特點:

  • 對象可能不被垃圾回收
  • 垃圾回收并不等于析構
  • 垃圾回收只與內存有關

Java中垃圾回收器的工作是在內存面臨不夠用的時候才會觸發GC,此時回收不再使用對象的內存,而當一個對象不再使用時,并不能保證一定會被回收,也就不能保證finalize()函數會被調用。

public class FinalizeTest {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        new FinalizeTest();
        //System.gc();
    }
    
    @Override
    protected void finalize() throws Throwable {
        // TODO Auto-generated method stub
        super.finalize();
        System.out.println("finalize");
    }

}

如果不顯式調用Sysetem.gc(),此時就不會觸發GC,finalize()方法也不會被調用。垃圾回收只與內存有關,因此對象相關的其他資源需要我們通過合理的方式顯式清理。

垃圾回收器的工作方式

Java中使用垃圾回收器回收通過new操作生成的而不再使用的對象所占用的內存空間,從而簡化編程。而垃圾回收面臨三個問題:

  • 何時回收
  • 回收哪些對象的內存
  • 如何回收

對于第一個問題就是之前所說的在Java虛擬機的內存不夠用時就會觸發一次GC,回收不再使用對象的內存。而回收的自然是不再使用對象的內存,如何搜尋不再使用的對象,Java中沒有使用引用計數法,而是使用追溯其存活堆棧或靜態存儲區之中的引用,不再被引用的對象自然是待處理的“對象”,而對于回收算法則較為復雜,主要包括“停止-復制”和“標記-清掃”算法,前者可以使得回收之后的內存相對整齊,便于內存分配,但是復制過程的消耗較大,而“標記-清掃”則相對輕量級,但是一段時間以后內存則會變得碎片化,因此Java考慮到不同時期不同對象的內存分配以及分布特點,采用了自適應的,分代的,停止-復制,標記-清掃式垃圾回收器,具體的垃圾回收過程可以參考相關書籍。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 初始化和清理正是涉及安全的兩個問題 一、用構造器確保初始化構造器:在java中提供構造器,確保每個對象都會得到初始...
    whyshang閱讀 397評論 0 0
  • *構造器是特殊的方法,它沒有返回值。這個和返回值為空(void)明顯不同。 *區分重載的方法是必須有個獨一無二的參...
    e條蟲閱讀 223評論 0 0
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,759評論 18 399
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,360評論 11 349
  • Sunny飛鏡閱讀 97評論 0 0