Java的構造函數與默認構造函數(深刻版)

前言

咱們知道在建立對象的時候,通常會經過構造函數來進行初始化。在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

構造函數的特色

  1. 構造函數的名稱必須與類名相同,並且還對大小寫敏感。
  2. 構造函數沒有返回值,也不能用void修飾。若是跟構造函數加上返回值,那這個構造函數就會變成普通方法。
  3. 一個類能夠有多個構造方法,若是在定義類的時候沒有定義構造方法,編譯器會自動插入一個無參且方法體爲空的默認構造函數
  4. 構造方法能夠重載

等等,爲何無參構造函數和默認構造函數要分開說?它們有什麼不一樣嗎?是的微信

默認構造函數

咱們建立一個顯式聲明無參構造函數的類,以及一個沒有顯式聲明構造函數的類:函數

class Cat{  
 public Cat(){}  
}  
class CatAuto{}

而後咱們編譯一下,獲得它們的字節碼:
默認構造函數.pngthis

《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

經過這個例子咱們能看出,類在初始化時構造函數的調用順序是這樣的:

  1. 按順序調用父類成員變量和實例成員變量的初始化表達式;
  2. 調用父類構造函數
  3. 按順序分別調用成員變量和實例成員變量的初始化表達式;
  4. 調用類構造函數

嘶......爲何會是這種順序呢

Java對象初始化中的構造函數

咱們知道,一個對象在被使用以前必須被正確地初始化。本文采用最多見的建立對象方式:使用new關鍵字建立對象,來爲你們介紹Java對象初始化的順序。new關鍵字建立對象這種方法,在Java規範中被稱爲由執行類實例建立表達式而引發的對象建立

Java對象的建立過程(詳見《深刻理解Java虛擬機》)

當虛擬機遇到一條new指令時,首先會去檢查這個指令的參數是否能在常量池(JVM運行時數據區域之一)中定位到這個類的符號引用,而且檢查這個符號引用是否已被加載、解釋和初始化過。若是沒有,則必須執行相應的類加載過程(這個過程在Java的繼承(深刻版)有所介紹)。

類加載過程當中,準備階段中爲類變量分配內存並設置類變量初始值,而類初始化階段則是執行類構造器<clinit>方法的過程。而<clinit>方法是由編譯器自動收集類中的類變量賦值表達式和靜態代碼塊(static{})中的語句合併產生的,其收集順序是由語句在源文件中出現的順序所決定。

其實在類加載檢查經過後,對象所須要的內存大小已經能夠徹底肯定過了。因此接下來JVM將爲新生對象分配內存,以後虛擬機將分配到的內存空間都初始化爲零值。接下來虛擬機要對對象進行必要的設置,並這些信息放在對象頭。最後,再執行<init>方法,把對象按程序員的意願進行初始化。
對象建立過程2.jpg
以上就是Java對象的建立過程,那麼類構造器<clinit>方法與實例構造器<init>方法有何不一樣?

  1. 類構造器<clinit>方法不須要程序員顯式調用,虛擬機會保證在子類構造器<clinit>方法執行以前,父類的類構造器<clinit>方法執行完畢。
  2. 在一個類的生命週期中,類構造器<clinit>方法最多會被虛擬機調用一次,而實例構造器<init>方法則會被虛擬機屢次調用,只要程序員還在建立對象。

等等,構造函數呢?跑題了?莫急,在瞭解Java對象建立的過程以後,讓咱們把鏡頭聚焦到這裏「對象初始化」:
對象初始化3.jpg

在對象初始化的過程當中,涉及到的三個結構,實例變量初始化實例代碼塊初始化構造函數

咱們在定義(聲明)實例變量時,還能夠直接對實例變量進行賦值或使用實例代碼塊對其進行賦值,實例變量和實例代碼塊的運行順序取決於它們在源碼的順序

編譯器中,實例變量直接賦值和實例代碼塊賦值會被放到類的構造函數中,而且這些代碼會被放在父類構造函數的調用語句以後,在實例構造函數代碼以前

舉個栗子:

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對象建立過程是這樣:
Java建立對象流程.jpg

結語

如今是快閱讀流行的時代,短小精悍的文章更受歡迎。但我的認爲回顧知識點最重要的是溫故知新,因此採用深刻版的寫法,不過每次寫完我都以爲我都不像是一個小甜甜了,卻是有點像下圖的那顆蔥頭......

若是以爲文章不錯,請點一個贊吧,這會是我最大的動力~
微信圖片_20200307103322.jpg

參考資料:

Java裏的構造函數(構造方法)

java無參構造函數(默認構造函數)

Java 構造函數的詳解

一個之前沒有注意的問題:java構造函數的執行順序

深刻理解Java對象的建立過程:類的初始化與實例化

相關文章
相關標籤/搜索