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


前言

我們知道在創建對象的時候,一般會通過構造函數來進行初始化。在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);
    }
}

運行結果為:

meow

構造函數的特點

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

等等,為什么無參構造函數和默認構造函數要分開說?它們有什么不同嗎?是的

默認構造函數

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

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

然后我們編譯一下,得到它們的字節碼:
在這里插入圖片描述
《Java的多態(深入版)》介紹了invokespecial指令是用於調用實例化方法、私有方法和父類方法。我們可以看到,即使沒有顯式聲明構造函數,在創建CatAuto對象的時候invokespecial指令依然會調用方法。那么是誰創建的無參構造方法呢?是編譯器

前文我們可以得知,在類加載過程中的驗證階段會調用檢查類的父類數據,也就是會先初始化父類。但畢竟驗證父類數據跟創建父類數據,從動作的目的上看二者並不相同,所以類會在java文件編譯成class文件的過程中,編譯器就將自動向無構造函數的類添加無參構造函數,即默認構造函數

為什么可以編譯器要向沒有定義構造函數的類,添加默認構造函數?

構造函數的目的就是為了初始化,既然沒有顯式地聲明初始化的內容,則說明沒有可以初始化的內容。為了在JVM的類加載過程中順利地加載父類數據,所以就有默認構造函數這個設定。那么二者的不同之處在哪兒?

二者在創建主體上的不同。無參構造函數是由開發者創建的,而默認構造函數是由編譯器生成的。

二者在創建方式上的不同。開發者在類中顯式聲明無參構造函數時,編譯器不會生成默認構造函數;而默認構造函數只能在類中沒有顯式聲明構造函數的情況下,由編譯器生成。

二者在創建目的上也不同。開發者在類中聲明無參構造函數,是為了對類進行初始化操作;而編譯器生成默認構造函數,是為了在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的繼承(深入版)有所介紹)。

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

其實在類加載檢查通過后,對象所需要的內存大小已經可以完全確定過了。所以接下來JVM將為新生對象分配內存,之后虛擬機將分配到的內存空間都初始化為零值。接下來虛擬機要對對象進行必要的設置,並這些信息放在對象頭。最后,再執行方法,把對象按程序員的意願進行初始化。

在這里插入圖片描述
以上就是Java對象的創建過程,那么類構造器方法與實例構造器方法有何不同?

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

等等,構造函數呢?跑題了?莫急,在了解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對象創建時有兩種類型的構造函數:類構造函數方法、實例構造函數方法,而整個Java對象創建過程是這樣:
在這里插入圖片描述

結語

現在是快閱讀流行的時代,短小精悍的文章更受歡迎。但個人認為回顧知識點最重要的是溫故知新,所以采用深入版的寫法,不過每次寫完我都覺得我都不像是一個小甜甜…

如果覺得文章不錯,請點一個贊吧,這會是我最大的動力~

參考資料:

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

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

Java 構造函數的詳解

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM