1. 程式人生 > >類加載和初始化順序

類加載和初始化順序

pat 類構造 private table 講解 構造器 [] 十個 類加載

這個博客是我看Thinking In Java的筆記與記錄

簡單介紹類加載:

在很多編程語言中,程序是作為啟動過程的一部分立刻被加載出來的,然後是初始化工作,然後是程序開始。 這些語言必須嚴格控制初始化的過程,這樣才能保證static變量的初始化不會出問題。比如像C++,就有可能出現一個static變量在初始化的過程中,需要另一個static變量已經成功初始化並已經有效,不然就會有問題。而Java不會出現這樣的問題。因為它采用一個比較特別的方法去加載。

萬物在Java中都是object,每個類的編譯代碼都在自己的那個獨立的文件中,也就是每個.class文件,這些文件或者說是編譯代碼只有在代碼第一次被使用的時候會加載。

什麽叫第一次被使用?第一個這個類的對象被構造出來,或者是類中的static方法或者是static變量被調用或者是使用了,這兩個情況隨便發生一個,類加載就發生了。(但其實一個對象要被構造出來,就要調用這個類的構造函數,而構造函數也是static,所以其實可以說只要有static方法或者變量的調用或使用,類就被加載)

然後有個一定要清楚的是,初次使用的時候,也是static變量初始化的時候。在類加載的時候,所有的static對象會按照你在代碼中定義的順序一次初始化,可以看作static對象的初始化和類加載是一起進行的。當然static對象只會初始化一次。

類變量的自動初始化和初始化順序:

Java為了保證類變量在使用之前已經得到初始化,有個自動賦值的機制。就如果你在定義這個類變量的時候沒有給它初始化,new出來這個對象後這些類變量會被自動初始化賦值。基本數據類型會被自動賦值為默認值(這裏就不說明了),對象的引用會被自動賦值為null。

構造器的其中一個用處之一,就是為類變量提供初始化的服務。但註意,構造器的初始化也就是構造器中為類變量賦值的這個動作,並不能阻止類變量自動賦值的機制,也就是說,在進入構造器之前,類變量其實已經賦值完畢了。

public class Counter {
  int i;
  Counter() { i = 7; }
  // ...
} 

如果你new一個Counter對象,那麽i先被自動賦值為0,再是進入構造器,被賦值為7.也就是說,類變量的初始化(自動)在構造器之前就完成了。

在一個類中,類變量的初始化順序就是代碼中他們的位置,這些類變量初始化完之後,就到構造器中的工作。這個例子可以很好得體現類變量的初始化順序。

class Window {
    Window(int marker) { print("Window(" + marker + ")"); }
}
class House { Window w1 = new Window(1); // Before constructor House() { // Show that we’re in the constructor:     print("House()");     w3 = new Window(33); // Reinitialize w3   }   Window w2 = new Window(2); // After constructor   void f() { print("f()"); }   Window w3 = new Window(3); // At end }
public class OrderOfInitialization {   public static void main(String[] args) {     House h = new House();     h.f(); // Shows that construction is done   } }

/* Output: Window(1) Window(2) Window(3) House() Window(33) f()

static對象的初始化:

static對象只有一塊公共的內存,static關鍵字只能用於類變量,所以static也會被自動初始化,初始化的規則也和普通的類變量一樣,這裏就不重復講了。

然後就是static對象的初始化是在類加載的時候就進行的,下面看個比較長的例子,來深刻理解下static對象的初始化:

class Bowl {
    Bowl(int marker) {
        print("Bowl(" + marker + ")");
    }
    void f1(int marker) {
        print("f1(" + marker + ")");
    }
}

class Table {
    static Bowl bowl1 = new Bowl(1);
    Table() {
        print("Table()");
        bowl2.f1(1);
    }
    void f2(int marker) {
        print("f2(" + marker + ")");
    }
    static Bowl bowl2 = new Bowl(2);
}

class Cupboard {
    Bowl bowl3 = new Bowl(3);
    static Bowl bowl4 = new Bowl(4);
    Cupboard() {
        print("Cupboard()");
        bowl4.f1(2);
    }
    void f3(int marker) {
        print("f3(" + marker + ")");
    }
    static Bowl bowl5 = new Bowl(5);
}

public class StaticInitialization {
    public static void main(String[] args) {
        print("Creating new Cupboard() in main");
        new Cupboard();
        print("Creating new Cupboard() in main");
        new Cupboard();
        table.f2(1);
        cupboard.f3(1);
}
    static Table table = new Table();
    static Cupboard cupboard = new Cupboard();
} 

輸出是:

/* Output:
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)
*///:~

現在我們來一步一步說明下這些輸出:

首先是類StaticInitialization類中的main函數,這是程序啟動的入口。而這個main函數是個static的對象,也就是說,現在是第一次運行了StaticInitialization類的一個static對象,這個類便要加載,然後static變量按順序初始化。 我們看到,這個類的static對象有:main函數,table(Table類的對象引用),cupboard(Cupboard的對象引用),也就是說要加載StaticInitialization類,需要先初始化table和cupboard。

而要也就是說現在要創建Table對象還有Cupboard對象。

new Table(),這個語句就是第一次構造Table對象,或者說要調用Table的構造器,static函數(第一次),所以構造這個對象,要先加載Table類和初始化Table類中的static對象:static Bowl bowl1和static Bowl bowl2

而這個時候我們又要第一次構造Bowl對象,調用了Bowl的static構造器,又是要加載Bowl類,初始化Bowl類的static對象,Bowl類中沒有static對象,然後加載就完成,進入構造對象階段。先初始化類變量,先賦值為默認值,再執行賦值語句。但Bowl中沒有非static的類變量,所以接著進入Bowl的構造器——於是便有了我們的第一和第二個輸出——Bowl(1)和Bowl(2)。

好的現在Table的兩個static變量初始化完成了,也就是說Table類的加載完成,進入構造對象階段,Table類也沒有能夠初始化的非static類變量,所以進入Table的構造器——第三個輸出——Table()。構造器中還有個語句是bowl2.f1(1);,這個是個非static的方法,於是第四個輸出——f1(1)

然後是new Cupboard()語句,一樣的,因為是第一次構造Cupboard對象,所以先加載Cupboard類和初始化Cupboard類中的static對象——static Bowl bowl4和static Bowl bowl5。由於這個時候已經不是第一次用到Bowl類的對象,所以不用加載Bowl類了,也就是跳過類加載階段,直接進入構造對象階段,一樣沒有類變量要初始化,直接構造器——第五和第六個輸出——Bowl(4)和Bowl(5)。然後Cupboard的類加載就結束了,進入構造Cupboard的階段,先是初始化類變量,先賦值為默認值,也就是bowl3 = null; 然後再執行賦值語句,也就是Bowl bowl3 = new Bowl(3),為這個普通類變量了bowl3賦值。Bowl已經加載類並且沒有普通類變量,所以直接構造器打印——第六個輸出——bowl(3)。Cupboard的類變量初始化之後,便進入Cupboard的構造器,一個pirnt語句和bowl4.f1(1),於是有了第七個和第八個輸出——Cupboard()和f1(2)

好了,到了這裏,StaticInitialization類的兩個static變量都初始化成功了,也就是說StaticInitialization類的加載完成了。(現在思路回到這個類調用main方法那)因為這個類加載是因為運行了static方法,而不是new對象(調用構造器),所以不用構造對象。接下來就是進入到main函數裏面了。先第個輸出——Creating new Cupboard in main()然後main方法裏面new了個Cupboard對象,因為這裏已經不是第一次用這個Cupboard類,所以不用類加載,直接構造Cupboard對象。一樣先初始化類變量,然後構造器,所以有了第十個、十一個和十二個輸出——Bowl(3)和Cupboard()還有f1(2)。後面又new了個Cupboard對象和直接調用對象的方法,後面幾個輸出就不講解了。

所以這裏來稍微總結下普通類的加載與初始化順序:

  1. 用到static變量或者是static的方法(註意構造器也是static方法),第一次用new創建某個類的對象(也就是第一次用static的構造器方法),Java編譯器會找到這個類的.class文件也就是編譯代碼(在classpath中),然後加載這個類。
  2. static變量的初始化立刻開始,可以看作類中static變量的初始化也是類加載的一部分。
  3. 如果你是用new Dog()的方法,也就是創建了一個對象,或者說是static的構造器方法觸發了構造對象這個動作,那麽首先會在堆上為這個對象分配足夠的內存,然後這塊內存會被清零。這麽做的後果就是,類變量會被自動地賦值為默認值,像0和null。
  4. 然後是執行類變量的賦值語句,像是Leg leg = new Leg()類似的,就是為類變量賦值。
  5. 類變量的初始化結束後,便進入構造器中,跑構造器中的代碼。

(我覺得3,4步可以就簡單看作是:進行類變量的初始化工作,定義時沒賦值的就自動賦默認值,有手動賦值的就執行賦值操作~)

涉及繼承的類加載和初始化順序:

首先要知道的一個概念是:當你創建一個子類的的對象的時候,裏面其實也包含了一個基類的對象。(可能不止一個基類)所以,基類正確的初始化就變成了一個很重要的工作。Java怎樣保證基類的初始化呢?通過基類的構造器。Java會自動在子類構造器中第一時間call基類的構造器。(當然是自動調用無參的那個構造器,如果你自己重載了一個有參的構造器,然後又沒有寫無參的構造器,那麽你要在子類構造器的第一行用super顯示調用你自己寫的那個構造器)

好的知道這些信息後,就直接看這個例子吧:

package www.com.thinkInJava.reusdingClasses;


class Insect {
    private int i = 9;
    
    protected int j;
    
    protected int h = printInit("Insect.h initialized and j=" + j);
    
    Insect() {
        System.out.println("i = " + i + ", j = " + j);
        j = 39;
    }
    
    private static int x1 = printInit("static Insect.x1 initialized");
    
    static int printInit(String s) {
        System.out.println(s);
        return 47;
    }
    
}


public class Beetle extends Insect {
    private int k = printInit("Beetle.k initialized");
    
    public Beetle() {
        System.out.println("k = " + k);
        System.out.println("j = " + j);
    }
    
    private static int x2 = printInit("static Beetle.x2 initialized");
    
    public static void main(String[] args) {
        System.out.println("Beetle constructor");
        Beetle b = new Beetle();
    }
}

輸出是:

/**
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
Insect.h initialized and j=0
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
**/

好了現在也來一步一步分析下輸出,理解下涉及繼承的類加載和初始化順序:

首先,運行程序,是從main函數進去的,也就是先調用了Beetle類的static方法——main方法。第一次用static方法,編譯器找到Beetle類的編譯代碼也就是Beetle.class,然後加載這個類。但在加載這個類的過程中,發現了extends關鍵字,所以按邏輯關系,編譯器現在得先加載它的基類:Insect類。

加載Insect類的過程和加載普通類的過程一樣,找到.class文件,加載,並立刻初始化static對象。(如果Insect還有基類,那麽也要先加載基類)Insect有個private static int x1 = printlnt(static Insect.x1 initialized),這個static field的初始化用到了一個static的方法,於是有了第一句輸出——static Insect.x1 initialized

然後Insect的類加載過程就完成了,現在可以繼續進行Beetle類的加載。 這裏補一句,之所以要先加載基類,是為了保證加載子類的時候,static對象的初始化時如果需要用到基類的static對象,不會出現基類的static對象還沒有初始化或者invalid的。 所以現在就開始初始化Beetle類中的static對象。private static int x2 = printInit("static Beetle.x2 initialized"); 於是有了第二句輸出——static Beetle.x2 initialized然後Beetle的加載過程就完成了,因為這個類加載是由main方法引起的,所以沒有構造對象的對象。所以是運行main方法裏剩下的內容。所以第三句輸出——Beetle constructor。然後下一句是Beetle b = new Beetle()。但由於剛剛已經第一次用了Beetle類中是static方法(main),這裏已經不是第一次了,所以沒有類加載的過程,直接進入構造對象的過程。

構造對象的過程和普通的類似乎有點不一樣:首先是分配足夠的內存給Beetle對象,然後再清空,也就是把Beetle類中的類變量的值自動賦值為默認值,也就是k = 0。然後調用基類的構造器(我覺得不一樣就是在這裏,因為上面的普通類的過程是賦默認值後,運行了賦值語句才到構造器的)。這裏是自動調用,你也可以用super顯示調用。由於基類剛剛加載過了,所以這裏不用進行類的加載,而是直接進行基類對象的構造:分配足夠的內存,清空自動賦默認值,也就是Insect中的i = 0,j = 0,h = 0,x1 = 0。然後再執行相應的賦值語句——第四句輸出——Insect.h initialized and j=0。 這裏是執行了類變量h的賦值語句protected int h = printInt("Insect.h initialized and j = " + j); ,還有執行對i的賦值語句,private int i = 9。 然後再進入基類的構造器中,執行構造器中的代碼——第五句輸出——i = 9, j = 0。

基類對象的初始化完成了,構造完成了,就繼續子類對象的構造,剛剛做到所有類變量賦默認值,現在是執行類變量的賦值語句——第六句輸出——Beetle.k initialized。 然後才是Beetle的構造器中的代碼——第七句輸出——k =47 (回車)j = 39

也來總結一下,父類Animal,子類Dog,new Dog():

  1. 先是第一次用Dog的對象,也是static的構造方法的第一次使用,所以加載Dog.class,發現有基類,加載Animal.class
  2. 立刻初始化Animal的static field,完成Animal的類加載,然後繼續Dog的類加載
  3. 因為是new語句,所以開始構造Dog對象,先分配足夠內存,然後清0,相當於為所有的Dog中的類變量賦值為默認值,0啊null這些。
  4. 然後調用基類的構造器,進行基類對象的構造,一樣先為Animal類中的類變量賦值為默認值,然後執行賦值語句,為類變量賦值。然後進入Animal的構造器中,執行代碼。
  5. Animal對象的初始化與構造完成,繼續Dog的初始化與對象的構造,執行Dog中類變量的賦值語句,最後進入Dog的構造器中執行相應的代碼。

大概的類加載和類變量的初始化順序有點頭緒了,再具體的細節還有一些問題,待到學習到JVM的時候再深入去理解與學習。

類加載和初始化順序