咱們知道在建立對象的時候,通常會經過構造函數來進行初始化。在Java的繼承(深刻版)有介紹到類加載過程當中的驗證階段,會檢查這個類的父類數據,但爲何要怎麼作?構造函數在類初始化和實例化的過程當中發揮什麼做用?html
(若文章有不正之處,或難以理解的地方,請多多諒解,歡迎指正)java
構造函數,主要是用來在建立對象時初始化對象,通常會跟new運算符一塊兒使用,給對象成員變量賦初值。程序員
class Cat{ String sound; public Cat(){ sound = "meow"; } } public class Test{ public static void main(String[] args){ System.out.println(new Cat().sound); } }
運行結果爲:segmentfault
meow
等等,爲何無參構造函數和默認構造函數要分開說?它們有什麼不一樣嗎?是的。微信
咱們建立一個顯式聲明無參構造函數的類,以及一個沒有顯式聲明構造函數的類:函數
class Cat{ public Cat(){} } class CatAuto{}
而後咱們編譯一下,獲得它們的字節碼:
this
在《Java的多態(深刻版)》介紹了invokespecial指令是用於調用實例化<init>方法、私有方法和父類方法。咱們能夠看到,即便沒有顯式聲明構造函數,在建立CatAuto對象的時候invokespecial指令依然會調用<init>方法。那麼是誰建立的無參構造方法呢?是編譯器。spa
從前文咱們能夠得知,在類加載過程當中的驗證階段會調用檢查類的父類數據,也就是會先初始化父類。但畢竟驗證父類數據跟建立父類數據,從動做的目的上看兩者並不相同,因此類會在java文件編譯成class文件的過程當中,編譯器就將自動向無構造函數的類添加無參構造函數,即默認構造函數。.net
構造函數的目的就是爲了初始化,既然沒有顯式地聲明初始化的內容,則說明沒有能夠初始化的內容。爲了在JVM的類加載過程當中順利地加載父類數據,因此就有默認構造函數這個設定。那麼兩者的不一樣之處在哪兒?3d
兩者在建立主體上的不一樣。無參構造函數是由開發者建立的,而默認構造函數是由編譯器生成的。
兩者在建立方式上的不一樣。開發者在類中顯式聲明無參構造函數時,編譯器不會生成默認構造函數;而默認構造函數只能在類中沒有顯式聲明構造函數的狀況下,由編譯器生成。
兩者在建立目的上也不一樣。開發者在類中聲明無參構造函數,是爲了對類進行初始化操做;而編譯器生成默認構造函數,是爲了在JVM進行類加載時,可以順利驗證父類的數據信息。
噢...那我想分狀況來初始化對象,能夠怎麼作?實現構造函數的重載便可。
在《Java的多態(深刻版)》中介紹到了實現多態的途徑之一,重載。因此重載本質上也是
同一個行爲具備不一樣的表現形式或形態能力。
舉個栗子,咱們在領養貓的時候,通常這隻貓是沒有名字的,它只有一個名稱——貓。當咱們領養了以後,就會給貓起名字了:
class Cat{ protected String name; public Cat(){ name = "Cat"; } public Cat(String name){ this.name = name; } }
在這裏,Cat類有兩個構造函數,無參構造函數的功能就是給這隻貓附上一個統稱——貓,而有參構造函數的功能是定義主人給貓起的名字,但由於主人想法比較多,過幾天就換個名稱,因此貓的名字不能是常量。
當有多個構造函數存在時,須要注意,在建立子類對象、調用構造函數時,若是在構造函數中沒有特地聲明,調用哪一個父類的構造函數,則默認調用父類的無參構造函數(一般編譯器會自動在子類構造函數的第一行加上super()方法)。
若是父類沒有無參構造函數,或想調用父類的有參構造方法,則須要在子類構造函數的第一行用super()方法,聲明調用父類的哪一個構造函數。舉個栗子:
class Cat{ protected String name; public Cat(){ name = "Cat"; } public Cat(String name){ this.name = name; } } class MyCat extends Cat{ public MyCat(String name){ super(name); } } public class Test{ public static void main(String[] args){ MyCat son = new MyCat("Lucy"); System.out.println(son.name); } }
運行結果爲:
Lucy
總結一下,構造函數的做用是用於建立對象的初始化,因此構造函數的「方法名」與類名相同,且無須返回值,在定義的時候與普通函數稍有不一樣;且從建立主體、方式、目的三方面可看出,無參構造函數和默認構造函數不是同一個概念;除了Object類,全部類在加載過程當中都須要調用父類的構造函數,因此在子類的構造函數中,**須要使用super()方法隱式或顯式地調用父類的構造函數**。
在介紹構造函數的執行順序以前,咱們來作個題:
public class MyCat extends Cat{ public MyCat(){ System.out.println("MyCat is ready"); } public static void main(String[] args){ new MyCat(); } } class Cat{ public Cat(){ System.out.println("Cat is ready"); } }
運行結果爲:
Cat is ready MyCat is ready
這個簡單嘛,只要知道類加載過程當中會對類的父類數據進行驗證,並調用父類構造函數就能夠知道答案了。
那麼下面這個題呢?
public class MyCat{ MyCatPro myCatPro = new MyCatPro(); public MyCat(){ System.out.println("MyCat is ready"); } public static void main(String[] args){ new MyCat(); } } class MyCatPro{ public MyCatPro(){ System.out.println("MyCatPro is ready"); } }
運行結果爲:
MyCatPro is ready MyCat is ready
嘶......這裏就是在建立對象的時候會先實例化成員變量的初始化表達式,而後再調用本身的構造函數。
ok,結合上面的已知項來作作下面這道題:
public class MyCat extends Cat{ MyCatPro myCatPro = new MyCatPro(); public MyCat(){ System.out.println("MyCat is ready"); } public static void main(String[] args){ new MyCat(); } } class MyCatPro{ public MyCatPro(){ System.out.println("MyCatPro is ready"); } } class Cat{ CatPro cp = new CatPro(); public Cat(){ System.out.println("Cat is ready"); } } class CatPro{ public CatPro(){ System.out.println("CatPro is ready"); } }
3,2,1,運行結果以下:
CatPro is ready Cat is ready MyCatPro is ready MyCat is ready
經過這個例子咱們能看出,類在初始化時構造函數的調用順序是這樣的:
嘶......爲何會是這種順序呢?
咱們知道,一個對象在被使用以前必須被正確地初始化。本文采用最多見的建立對象方式:使用new關鍵字建立對象,來爲你們介紹Java對象初始化的順序。new關鍵字建立對象這種方法,在Java規範中被稱爲由執行類實例建立表達式而引發的對象建立。
當虛擬機遇到一條new指令時,首先會去檢查這個指令的參數是否能在常量池(JVM運行時數據區域之一)中定位到這個類的符號引用,而且檢查這個符號引用是否已被加載、解釋和初始化過。若是沒有,則必須執行相應的類加載過程(這個過程在Java的繼承(深刻版)有所介紹)。
類加載過程當中,準備階段中爲類變量分配內存並設置類變量初始值,而類初始化階段則是執行類構造器<clinit>方法的過程。而<clinit>方法是由編譯器自動收集類中的類變量賦值表達式和靜態代碼塊(static{})中的語句合併產生的,其收集順序是由語句在源文件中出現的順序所決定。
其實在類加載檢查經過後,對象所須要的內存大小已經能夠徹底肯定過了。因此接下來JVM將爲新生對象分配內存,以後虛擬機將分配到的內存空間都初始化爲零值。接下來虛擬機要對對象進行必要的設置,並這些信息放在對象頭。最後,再執行<init>方法,把對象按程序員的意願進行初始化。
以上就是Java對象的建立過程,那麼類構造器<clinit>方法與實例構造器<init>方法有何不一樣?
等等,構造函數呢?跑題了?莫急,在瞭解Java對象建立的過程以後,讓咱們把鏡頭聚焦到這裏「對象初始化」:
在對象初始化的過程當中,涉及到的三個結構,實例變量初始化、實例代碼塊初始化、構造函數。
咱們在定義(聲明)實例變量時,還能夠直接對實例變量進行賦值或使用實例代碼塊對其進行賦值,實例變量和實例代碼塊的運行順序取決於它們在源碼的順序。
在編譯器中,實例變量直接賦值和實例代碼塊賦值會被放到類的構造函數中,而且這些代碼會被放在父類構造函數的調用語句以後,在實例構造函數代碼以前。
舉個栗子:
class TestPro{ public TestPro(){ System.out.println("TestPro"); } } public class Test extends TestPro{ private int a = 1; private int b = a+1; public Test(int var){ System.out.println(a); System.out.println(b); this.a = var; System.out.println(a); System.out.println(b); } { b+=2; } public static void main(String[] args){ new Test(10); } }
運行結果爲:
TestPro 1 4 10 4
總結一下,Java對象建立時有兩種類型的構造函數:類構造函數<clinit>方法、實例構造函數<init>方法,而整個Java對象建立過程是這樣:
如今是快閱讀流行的時代,短小精悍的文章更受歡迎。但我的認爲回顧知識點最重要的是溫故知新,因此採用深刻版的寫法,不過每次寫完我都以爲我都不像是一個小甜甜了,卻是有點像下圖的那顆蔥頭......
若是以爲文章不錯,請點一個贊吧,這會是我最大的動力~
參考資料: