前言
我們知道在創建對象的時候,一般會通過構造函數來進行初始化。在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
構造函數的特點
- 構造函數的名稱必須與類名相同,而且還對大小寫敏感。
- 構造函數沒有返回值,也不能用void修飾。如果跟構造函數加上返回值,那這個構造函數就會變成普通方法。
- 一個類可以有多個構造方法,如果在定義類的時候沒有定義構造方法,編譯器會自動插入一個無參且方法體為空的默認構造函數。
- 構造方法可以重載。
等等,為什么無參構造函數和默認構造函數要分開說?它們有什么不同嗎?是的。
默認構造函數
我們創建一個顯式聲明無參構造函數的類,以及一個沒有顯式聲明構造函數的類:
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
通過這個例子我們能看出,類在初始化時構造函數的調用順序是這樣的:
- 按順序調用父類成員變量和實例成員變量的初始化表達式;
- 調用父類構造函數;
- 按順序分別調用類成員變量和實例成員變量的初始化表達式;
- 調用類構造函數。
嘶…為什么會是這種順序呢?
Java對象初始化中的構造函數
我們知道,一個對象在被使用之前必須被正確地初始化。本文采用最常見的創建對象方式:使用new關鍵字創建對象,來為大家介紹Java對象初始化的順序。new關鍵字創建對象這種方法,在Java規范中被稱為由執行類實例創建表達式而引起的對象創建。
Java對象的創建過程(詳見《深入理解Java虛擬機》)
當虛擬機遇到一條new指令時,首先會去檢查這個指令的參數是否能在常量池(JVM運行時數據區域之一)中定位到這個類的符號引用,並且檢查這個符號引用是否已被加載、解釋和初始化過。如果沒有,則必須執行相應的類加載過程(這個過程在Java的繼承(深入版)有所介紹)。
類加載過程中,准備階段中為類變量分配內存並設置類變量初始值,而類初始化階段則是執行類構造器方法的過程。而**方法是由編譯器自動收集類中的類變量賦值表達式和靜態代碼塊**(static{})中的語句合並產生的,其收集順序是由語句在源文件中出現的順序所決定。
其實在類加載檢查通過后,對象所需要的內存大小已經可以完全確定過了。所以接下來JVM將為新生對象分配內存,之后虛擬機將分配到的內存空間都初始化為零值。接下來虛擬機要對對象進行必要的設置,並這些信息放在對象頭。最后,再執行方法,把對象按程序員的意願進行初始化。
以上就是Java對象的創建過程,那么類構造器方法與實例構造器方法有何不同?
- 類構造器方法不需要程序員顯式調用,虛擬機會保證在子類構造器方法執行之前,父類的類構造器方法執行完畢。
- 在一個類的生命周期中,類構造器方法最多會被虛擬機調用一次,而實例構造器方法則會被虛擬機多次調用,只要程序員還在創建對象。
等等,構造函數呢?跑題了?莫急,在了解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對象創建過程是這樣:
結語
現在是快閱讀流行的時代,短小精悍的文章更受歡迎。但個人認為回顧知識點最重要的是溫故知新,所以采用深入版的寫法,不過每次寫完我都覺得我都不像是一個小甜甜…
如果覺得文章不錯,請點一個贊吧,這會是我最大的動力~
參考資料: