相信我們在面試Java的時候總會有一些公司要做筆試題目的,而Java類的加載和對象創建流程的知識點也是常見的題目之一。接下來通過實例詳細的分析一下。
實例問題
實例代碼
Parent類
1 package mytest.javaBase; 2 3 public class Parent { 4 int a = 10; 5 static int b = 11; 6 // 靜態代碼塊 7 static { 8 System.out.println("Parent靜態代碼塊:b=" + b); 9 b++; 10 } 11 // 代碼塊 12 { 13 System.out.println("Parent代碼塊: a=" + a); 14 System.out.println("Parent代碼塊: b=" + b); 15 b++; 16 a++; 17 } 18 19 // 無參構造函數 20 Parent() { 21 System.out.println("Parent無參構造函數: a=" + a); 22 System.out.println("Parent無參構造函數: b=" + b); 23 } 24 25 // 有參構造函數 26 Parent(int a) { 27 System.out.println("Parent有參構造函數: a=" + a); 28 System.out.println("Parent有參構造函數: b=" + b); 29 } 30 31 // 方法 32 void function() { 33 System.out.println("Parent function run ……"); 34 } 35 36 }
Child類
1 package mytest.javaBase; 2 3 public class Child extends Parent { 4 int x = 10; 5 static int y = 11; 6 // 靜態代碼塊 7 static { 8 System.out.println("Child靜態代碼塊:y=" + y); 9 y++; 10 } 11 // 代碼塊 12 { 13 System.out.println("Child代碼塊: x=" + x); 14 System.out.println("Child代碼塊: y=" + y); 15 y++; 16 x++; 17 } 18 19 // 構造函數 20 Child() { 21 System.out.println("Child構造函數: x=" + x); 22 System.out.println("Child構造函數: y=" + y); 23 } 24 25 // 方法 26 void function() { 27 System.out.println("Child function run ……"); 28 } 29 30 }
Test測試類
1 package mytest.javaBase; 2 3 public class Test { 4 public static void main(String[] args) { 5 Child demo = new Child(); 6 demo.function(); 7 System.out.println("…………………………………………………………………………………………………………………………"); 8 Child child = new Child(); 9 child.function(); 10 } 11 }
我們可以先不看運行結果,自己思考下,運行結果會是什么,之后再比較下和自己思考的結果是否一樣。
運行結果
Parent靜態代碼塊:b=11
Child靜態代碼塊:y=11
Parent代碼塊: a=10
Parent代碼塊: b=12
Parent無參構造函數: a=11
Parent無參構造函數: b=13
Child代碼塊: x=10
Child代碼塊: y=12
Child構造函數: x=11
Child構造函數: y=13
Child function run ……
…………………………………………………………………………………………………………………………
Parent代碼塊: a=10
Parent代碼塊: b=13
Parent無參構造函數: a=11
Parent無參構造函數: b=14
Child代碼塊: x=10
Child代碼塊: y=13
Child構造函數: x=11
Child構造函數: y=14
Child function run ……
結果詳細分析
我們運行Test類的main方法
1、 啟動JVM,開始分配內存空間;
2、 開始加載Test.class文件,加載到方法區中,在加載的過程中靜態的內容要進入靜態區中;
3、 在開始運行main方法,這時JVM就會把main調用到棧中運行,開始從方法的第一行往下運行;
4、 在main方法中new Child();這時JVM就會在方法區中查找有沒有Child文件,如果沒有就加載Child.class文件,並且Child繼承Parent類,所以也要查找有沒有Parent類,如果沒有也要加載Parent.class文件。
5、 Child.class和Parent.class中的所有的非靜態內容會加載到非靜態的區域中,而靜態的內容會加載到靜態區中。靜態內容(靜態變量,靜態代碼塊,靜態方法)按照書寫順序加載。
說明:類的加載只會執行一次。下次再創建對象時,可以直接在方法區中獲取class信息。
6、 開始給靜態區中的所有靜態的成員變量開始默認初始化。默認初始化完成之后,開始給所有的靜態成員變量顯示初始化。
7、 所有靜態成員變量顯示初始化完成之后,開始執行靜態的代碼塊。先執行父類的靜態代碼塊,再執行子類的靜態代碼塊。
//這時輸出 Parent靜態代碼塊:b=11 Child靜態代碼塊:y=11
說明:>>靜態代碼塊是在類加載的時候執行的,類的加載只會執行一次所以靜態代碼塊也只會執行一次;
>>非靜態代碼塊和構造函數中的代碼是在對象創建的時候執行的,因此對象創建(new)一次,它們就會執行一次。
8、 這時Parent.class文件 和 Child.class文件加載完成。
9、 開始在堆中創建Child對象。給Child對象分配內存空間,其實就是分配內存地址。
10、開始對類中的的非靜態的成員變量開始默認初始化。
11、開始加載對應的構造方法,執行隱式三步
①有個隱式的super();
②顯示初始化(給所有的非靜態的成員變量)
③執行構造代碼塊
之后才開始執行本類的構造方法中的代碼
super()是調用父類的構造函數,此處即為Parent的構造函數,在Parent的構造函數中也有個隱式三步:首先super(),再執行Parent的顯示初始化,然后執行Parent的非靜態構造代碼塊,最后執行Parent的構造函數中的代碼。
//這時輸出 Parent代碼塊: a=10 Parent代碼塊: b=12 Parent無參構造函數: a=11 Parent無參構造函數: b=13
說明:雖然Parent沒有明寫extends,但是我們知道在Java中有個超類Object,它是所有類的父類,因此此處Parent類的super()是調用Object的構造函數
Parent的執行完之后,回來繼續執行Child自己的隱式三步中的第二步:顯示初始化,然后執行Child的非靜態代碼塊的,最后執行Child的構造函數中的代碼
//這時輸出 Child代碼塊: x=10 Child代碼塊: y=12 Child構造函數: x=11 Child構造函數: y=13
12、對象創建完成,把內存的地址賦值給demo使用。
13、執行demo.function()方法。
//這時輸出 Child function run ……
14、由於后面又創建(new)了一個新的Child對象,因此重復一下【9】之后的步驟,很容易明白它的輸出結果為
Parent代碼塊: a=10 Parent代碼塊: b=13 Parent無參構造函數: a=11 Parent無參構造函數: b=14 Child代碼塊: x=10 Child代碼塊: y=13 Child構造函數: x=11 Child構造函數: y=14 Child function run ……
簡單的畫個內存運行示例圖
總結
我們知道,我們在創建(new)一個對象的時候,先要去JVM的方法區里獲取該對象所對應的類的信息,如果方法區里沒有該類的信息,則需要去將它加載進來,加載進來之后,有了該類的信息,我們才能創建一個對象。
一般,Java類被編譯后,會生成一個class文件,在運行的時候會將class文件加載到Java虛擬機JVM中,class文件由類裝載器裝載,在JVM中(准確的來說應該是在JVM的方法區里)將形成一份描述Class結構的元信息對象,通過該元信息對象可以獲知Class的結構信息:如構造函數,屬性和方法等。
一、類的加載過程
首先,Jvm在執行時,遇到一個新的類時,會到內存中的方法區去找class的信息,如果找到就直接拿來用,如果沒有找到,就會去將類文件加載到方法區。在類加載時,靜態成員變量加載到方法區的靜態區域,非靜態成員變量加載到方法區的非靜態區域。
靜態代碼塊是在類加載時自動執行的代碼,非靜態代碼塊是在創建對象時自動執行的代碼,不創建對象不執行該類的非靜態代碼塊。
加載過程:
1、JVM會先去方法區中找有沒有相應類的.class存在。如果有,就直接使用;如果沒有,則把相關類的.clss加載到方法區。
2、在.class加載到方法區時,先加載父類再加載子類;先加載靜態內容,再加載非靜態內容
3、加載靜態內容:
- 把.class中的所有靜態內容加載到方法區下的靜態區域內
- 靜態內容加載完成之后,對所有的靜態變量進行默認初始化
- 所有的靜態變量默認初始化完成之后,再進行顯式初始化
- 當靜態區域下的所有靜態變量顯式初始化完后,執行靜態代碼塊
4、加載非靜態內容:把.class中的所有非靜態變量及非靜態代碼塊加載到方法區下的非靜態區域內。
5、執行完之后,整個類的加載就完成了。
對於靜態方法和非靜態方法都是被動調用,即系統不會自動調用執行,所以用戶沒有調用時都不執行,主要區別在於靜態方法可以直接用類名直接調用(實例化對象也可以),而非靜態方法只能先實例化對象后才能調用。
二、對象的創建過程
1、new一個對象時,在堆內存中開辟一塊空間。
2、給開辟的空間分配一個地址。
3、把對象的所有非靜態成員加載到所開辟的空間下。
4、所有的非靜態成員加載完成之后,對所有非靜態成員變量進行默認初始化。
5、所有非靜態成員變量默認初始化完成之后,調用構造函數。
6、在構造函數入棧執行時,分為兩部分:先執行構造函數中的隱式三步,再執行構造函數中書寫的代碼。
隱式三步:
①執行super()語句 ②顯示初始化(對開辟空間下的所有非靜態成員變量進行)
③執行構造代碼塊
7、在整個構造函數執行完並彈棧后,把空間分配的地址賦給引用對象。
三、其他
super語句,可能出現以下三種情況:
1)構造方法體的第一行是this()語句,則不會執行隱式三步,而是調用this()語句所對應的的構造方法,最終肯定會有第一行不是this語句的構造方法。
package mytest.javaBase; public class Student { private String name; private String age; Student() { }; Student(String name) { this.name = name; }; Student(String name, String age) { // 不會執行隱式三步 this(name); this.age = age; }; }
2)構造方法體的第一行是super()語句,則調用相應的父類的構造方法,
3)構造方法體的第一行既不是this()語句也不是super()語句,則隱式調用super(),即其父類的默認構造方法,這也是為什么一個父類通常要提供默認構造方法的原因;