Java基礎系列5:Java程式碼的執行順序

  • 2019 年 11 月 13 日
  • 筆記

該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接著了解每個Java知識點背後的實現原理,更完整地了解整個Java技術體系,形成自己的知識框架。

 

一、構造方法

構造方法(或構造函數)是類的一種特殊方法,用來初始化類的一個新的對象。Java 中的每個類都有一個默認的構造方法,它必須具有和類名相同的名稱,而且沒有返回類型。構造方法的默認返回類型就是對象類型本身,並且構造方法不能被 static、final、synchronized、abstract 和 native 修飾。

提示:構造方法用於初始化一個新對象,所以用 static 修飾沒有意義;構造方法不能被子類繼承,所以用 final 和 abstract 修飾沒有意義;多個執行緒不會同時創建記憶體地址相同的同一個對象,所以用 synchronized 修飾沒有必要。

構造方法的語法格式如下:

public class Person {    	/**  	 * 1.構造方法沒有返回值 默認返回類型就是對象類型本身  	 * 2.構造方法的方法名和類名相同  	 */    	//無參構造方法  	public Person() {  		System.out.println("我是無參構造方法");  	}    	//有參構造方法  	public Person(String username,Integer age) {  		System.out.println("我是有參構造"+"姓名:"+username+"  密碼:"+age);  	}    	public static void main(String[] args) {  		Person p1=new Person();//調用無參構造    		Person p2=new Person("小王",12);//調用有參構造  	}    }  

  

關於構造方法,需要注意:

  • 如何調用:
    • 構造方法在實例化的時候調用,如上述程式碼中的Person p1=new Person(),這裡便調用了Person類的無參構造,構造方法由系統自動調用
  • 構造函數重載
    • 我們知道方法可以重載(方法名相同,參數列表不同),那麼構造方法也是方法的一種,當然也可以繼承,如上述程式碼中的兩個構造方法,一個無參構造方法,一個帶兩個參數的構造方法。
    • 當有多個構造方法時,程式會在你創建類時根據你傳入的參數決定調用哪個構造方法
  • 默認構造方法
    • 細心的讀者可能會有疑問,之前創建類的時候我並沒有聲明構造函數,但是也可以創建類,是不是可以說類不需要構造函數也可以創建。不是滴,當你沒有顯示聲明構造函數時,程式會自動生成一個默認的無參構造函數
    • 並且該構造函數的許可權是隨著類的改變而改變的(類為public,構造函數也為public;類改為private,構造函數也改為private);而當該類一旦聲明了構造函數以後,java 是不會再給該類分配默認的構造函數。就是說,一旦你聲明了構造函數,並且該構造函數有形參,那麼你就不能pen ipen=new pen();像這樣聲明一個對象了。
  • 構造方法作用:
    • 構造函數是用於對象初始化
    • 一個對象建立,構造函數只運行一次,而一般方法可以被該對象調用多次。

 

二、程式碼塊

1、普通程式碼塊:

普通程式碼塊是我們用得最多的也是最普遍的,它就是在方法名後面用{}括起來的程式碼段。普通程式碼塊是不能夠單獨存在的,它必須要緊跟在方法名後面。同時也必須要使用方法名調用它。

public class Test {      public void test(){          System.out.println("普通程式碼塊");      }  }  

  

2、構造程式碼塊:

在類中直接定義沒有任何修飾符、前綴、後綴的程式碼塊即為構造程式碼塊。我們明白一個類必須至少有一個構造函數,構造函數在生成對象時被調用。構造程式碼塊和構造函數一樣同樣是在生成一個對象時被調用

public class Test{    {        System.out.println("我是構造程式碼塊");    }  }    

 

注意:

  • 構造程式碼塊的作用是給對象初始化。
  • 對象一建立就調用構造程式碼塊了,而且優於構造函數執行。這裡強調一下,有對象創建,才會執行構造程式碼塊,類不能調用構造程式碼塊的,而且構造程式碼塊與構造函數的執行順序是前者先於後者執行。
  • 構造程式碼塊與構造函數的區別是:構造程式碼塊是給所有對象進行統一初始化,而構造函數是給對應的對象初始化,因為構造函數是可以多個的,運行哪個構造函數就會建立什麼樣的對象,但無論建立哪個對象,都會先執行相同的構造程式碼塊。也就是說,構造程式碼塊中定義的是不同對象共性的初始化內容。

  

 

3、靜態程式碼塊:

想到靜態我們就會想到static,靜態程式碼塊就是用static修飾的用{}括起來的程式碼段,它的主要目的就是對靜態屬性進行初始化。

public class Test {      static{          System.out.println("靜態程式碼塊");      }  }  

  

注意:

  • 靜態程式碼塊隨著類的載入而執行,而且只會執行一次,並優於主函數。具體說靜態程式碼塊由類調用,類調用時先執行靜態程式碼塊,然後才執行主函數。
  • 靜態程式碼塊是給類初始化的,而構造程式碼塊是給對象初始化的。
  • 靜態程式碼塊中的變數是局部變數,和普通方法中的局部變數沒有區別。
  • 一個類中可以有多個靜態程式碼塊。

 

三、Java類的初始化順序

1、一個類的情況:

A:

public class Test {    	public Test(){  		System.out.println("Test構造函數");  	}    	{  		System.out.println("Test構造程式碼塊");  	}    	static {  		System.out.println("靜態程式碼塊");  	}      	public static void main(String[] args) {    	}    }  

  

結果:

靜態程式碼塊  

  

B:

public class Test {    	public Test(){  		System.out.println("Test構造函數");  	}    	{  		System.out.println("Test構造程式碼塊");  	}    	static {  		System.out.println("靜態程式碼塊");  	}      	public static void main(String[] args) {  		Test t=new Test();//創建了一個對象    	}    }  

  

這段程式碼相比於上述程式碼多了一個創建對象的程式碼

結果:

靜態程式碼塊  Test構造程式碼塊  Test構造函數  

  

C:

public class Test {    	public Test(){  		System.out.println("Test構造函數");  	}    	{  		System.out.println("Test構造程式碼塊");  	}    	static {  		System.out.println("靜態程式碼塊");  	}      	public static void main(String[] args) {  		Test t1=new Test();//創建了一個對象    		Test t2=new Test();    	}    }  

  

結果:

靜態程式碼塊  Test構造程式碼塊  Test構造函數  Test構造程式碼塊  Test構造函數  

  

由此結果可以看出:靜態程式碼塊只會在類載入的時候執行一次,而構造函數和構造程式碼塊則會在每次創建對象的都會執行一次

 

對於一個類而言,按照如下順序執行:

  1. 執行靜態程式碼塊
  2. 執行構造程式碼塊
  3. 執行構造函數

對於靜態變數、靜態初始化塊、變數、初始化塊、構造器,它們的初始化順序依次是(靜態變數、靜態初始化塊)>(變數、初始化塊)>構造器。

 

D:

public class Test {    	//靜態變數  	public static String staticField="靜態變數";    	//變數  	public String field="變數";    	//靜態初始化塊  	static {  		System.out.println(staticField);  		System.out.println("靜態初始化塊");  	}    	{  		System.out.println(field);  		System.out.println("初始化塊");  	}    	//構造函數  	public Test() {  		System.out.println("構造函數");  	}    	public static void main(String[] args) {  		Test t=new Test();  	}    }  

  

結果:

靜態變數  靜態初始化塊  變數  初始化塊  構造函數  

  

2、繼承情況下的程式碼執行順序:

class TestA{  	public TestA() {  		System.out.println("A的構造函數");  	}    	{  		System.out.println("A的構造程式碼塊");  	}    	static {  		System.out.println("A的靜態程式碼塊");  	}  }    public class TestB extends TestA {    	public TestB() {  		System.out.println("B的構造函數");  	}    	{  		System.out.println("B的構造程式碼塊");  	}    	static {  		System.out.println("B的靜態程式碼塊");  	}    	public static void main(String[] args) {  		TestB t=new TestB();  	}    }  

  

這裡有兩個類,屬於繼承的關係,讀者先不要看答案,自己思考一下結果是啥?

1 A的靜態程式碼塊  2 B的靜態程式碼塊  3 A的構造程式碼塊  4 A的構造函數  5 B的構造程式碼塊  6 B的構造函數

結果

 

 

當設計到繼承時,程式碼的執行順序如下:

1、執行父類的靜態程式碼塊,並初始化父類的靜態成員

2、執行子類的靜態程式碼塊,並初始化子類的靜態成員

3、執行父類的構造程式碼塊,執行父類的構造函數,並初始化父類的普通成員變數

4、執行子類的構造程式碼塊,執行子類的構造函數,並初始化子類的普通成員變數

 

Java初始化流程圖:

 

 

 

class Parent {  	/* 靜態變數 */  	public static String p_StaticField = "父類--靜態變數";  	/* 變數 */  	public String p_Field = "父類--變數";  	protected int i = 9;  	protected int j = 0;  	/* 靜態初始化塊 */  	static {  		System.out.println(p_StaticField);  		System.out.println("父類--靜態初始化塊");  	}  	/* 初始化塊 */  	{  		System.out.println(p_Field);  		System.out.println("父類--初始化塊");  	}    	/* 構造器 */  	public Parent() {  		System.out.println("父類--構造器");  		System.out.println("i=" + i + ", j=" + j);  		j = 20;  	}  }    public class SubClass extends Parent {  	/* 靜態變數 */  	public static String s_StaticField = "子類--靜態變數";  	/* 變數 */  	public String s_Field = "子類--變數";  	/* 靜態初始化塊 */  	static {  		System.out.println(s_StaticField);  		System.out.println("子類--靜態初始化塊");  	}  	/* 初始化塊 */  	{  		System.out.println(s_Field);  		System.out.println("子類--初始化塊");  	}    	/* 構造器 */  	public SubClass() {  		System.out.println("子類--構造器");  		System.out.println("i=" + i + ",j=" + j);  	}    	/* 程式入口 */  	public static void main(String[] args) {  		System.out.println("子類main方法");  		new SubClass();  	}  }  

  

結果:

父類--靜態變數  父類--靜態初始化塊  子類--靜態變數  子類--靜態初始化塊  子類main方法  父類--變數  父類--初始化塊  父類--構造器  i=9, j=0  子類--變數  子類--初始化塊  子類--構造器  i=9,j=20  

  

(1)訪問SubClass.main(),(這是一個static方法),於是裝載器就會為你尋找已經編譯的SubClass類的程式碼(也就是SubClass.class文件)。在裝載的過程中,裝載器注意到它有一個基類(也就是extends所要表示的意思),於是它再裝載基類。不管你創不創建基類對象,這個過程總會發生。如果基類還有基類,那麼第二個基類也會被裝載,依此類推。

(2)執行根基類的static初始化,然後是下一個派生類的static初始化,依此類推。這個順序非常重要,因為派生類的“static初始化”有可能要依賴基類成員的正確初始化。

(3)當所有必要的類都已經裝載結束,開始執行main()方法體,並用new SubClass()創建對象。

(4)類SubClass存在父類,則調用父類的構造函數,你可以使用super來指定調用哪個構造函數。基類的構造過程以及構造順序,同派生類的相同。首先基類中各個變數按照字面順序進行初始化,然後執行基類的構造函數的其餘部分。

(5)對子類成員數據按照它們聲明的順序初始化,執行子類構造函數的其餘部分。