詳解Java的構造方法及類的初始化

一. 利用構造方法給對象初始化

1. 構造方法的概念

構造方法(也稱為構造器)是一個特殊的成員方法,其名字必須與類名相同,在創建對象時,由編譯器自動調用,並且在整個對象的生命周期內隻調用一次。

構造方法的作用就是給對象中的成員進行初始化,並不負責給對象開辟空間。

public class Date {
    public int year;
    public int month;
    public int day;

    // 構造方法:
    // 名字與類名相同,沒有返回值類型,設置為void也不行
    // 一般情況下使用public修飾
    // 在創建對象時由編譯器自動調用,並且在對象的生命周期內隻調用一次
    public Date(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
        System.out.println("Date(int,int,int)方法被調用瞭");
    }

    public void printDate() {
        System.out.println(year + "-" + month + "-" + day);
    }

    public static void main(String[] args) {
        // 此處創建瞭一個Date類型的對象,並沒有顯式調用構造方法
        Date d = new Date(2021, 6, 9);
        // 輸出Date(int,int,int)方法被調用瞭
        d.printDate(); // 2021-6-9
    }
}

2. 構造方法的特性

1.名字必須與類名相同

2.沒有返回值類型,設置為void也不行

3.創建對象時由編譯器自動調用,並且在對象的生命周期內隻調用一次

4.絕大多數情況下使用public來修飾,特殊場景下會被private修飾

5.構造方法可以重載(用戶根據自己的需求提供不同參數的構造方法); 下面兩個構造方法:名字相同,參數列表不同,因此構成瞭方法重載

public class Date {
    public int year;
    public int month;
    public int day;
    
    // 無參構造方法
    public Date(){
        this.year = 1900;
        this.month = 1;
        this.day = 1;
    }
    
    // 帶有三個參數的構造方法
    public Date(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
    public void printDate(){
        System.out.println(year + "-" + month + "-" + day);
    }
    public static void main(String[] args) {
        Date d = new Date();
        d.printDate();
    }
}

6.如果用戶沒有顯式定義,編譯器會生成一份默認的構造方法,生成的默認構造方法一定是無參的; 一旦用戶定義,編譯器則不再生成;下面代碼中,沒有定義任何構造方法,編譯器會默認生成一個不帶參數的構造方法。

public class Date {
    public int year;
    public int month;
    public int day;
    public void printDate(){
        System.out.println(year + "-" + month + "-" + day);
    }
    public static void main(String[] args) {
        Date d = new Date();
        d.printDate();
    }
}

7.構造方法中,可以通過this調用其他構造方法來簡化代碼

【註意事項】

  • 構造方法中,通過this(…)去調用其他構造方法,這條語句必須是構造方法中第一條語句
  • 多個構造方法不可以互相調用(不能形成環), 會形成構造器的遞歸調用,但卻沒有調用的結束條件
public class Date {
    public int year;
    public int month;
    public int day;
// 無參構造方法--內部給各個成員賦值初始值,該部分功能與三個參數的構造方法重復
// 此處可以在無參構造方法中通過this調用帶有三個參數的構造方法
// 但是this(2022,8,16);必須是構造方法中第一條語句
    
    public Date(){
//System.out.println(year); 註釋取消掉,編譯會失敗
        this(2022, 8, 16);
//this.year = 1900;
//this.month = 1;
//this.day = 1;
    }
    
    // 帶有三個參數的構造方法
    public Date(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
}

3. 子類構造方法

在繼承基礎上,子類對象構造時,需要先調用基類構造方法,然後執行子類的構造方法。

在子類構造方法中,並沒有寫任何關於基類構造的代碼,但是在構造子類對象時,先執行基類的構造方法,然後執行子類的構造方法,

原因在於:子類對象中成員是有兩部分組成的,基類繼承下來的以及子類新增加的部分 。父類和子類, 肯定是先有父再有子,所以在構造子類對象時候 ,子類構造方法中先要調用基類的構造方法,將從基類繼承下來的成員構造完整 ,然後再完成子類自己的構造,將子類自己新增加的成員初始化完整 。

【註意事項】

1.若父類顯式定義無參或者默認的構造方法,在子類構造方法第一行默認有隱含的super()調用,即調用基類構造方法

public class Base {
    public Base(){
        System.out.println("Base()");
    }
}

public class Derived extends Base{
    public Derived(){
// super(); // 註意子類構造方法中默認會調用基類的無參構造方法:super(),
// 用戶沒有寫時,編譯器會自動添加,而且super()必須是子類構造方法中第一條語句,
// 並且隻能出現一次
        System.out.println("Derived()");
    }
}

public class Test {
    public static void main(String[] args) {
        Derived d = new Derived();
    }
}

2.如果父類構造方法是帶有參數的,此時需要用戶為子類顯式定義構造方法,並在子類構造方法中選擇合適的父類構造方法調用,否則編譯失敗。

public class Animal {
    public String name;
    public int  age;

   public Animal(String name, int age) {
        this.name = name;
        this.age = age;
       System.out.println("Animal(String , int )");
    }

}

public class Dog extends Animal{
    //傻狗  是狗的屬性
    public boolean silly;

   public Dog(String name,int age,boolean silly) {
        //1. 先幫助父類部分初始化 必須放到第一行
        super(name,age);
        this.silly = silly;
        System.out.println("Dog(String ,int ,boolean )");
   }
    public static void main(String[] args) {
        Animal animal2 = new Dog("金毛",6,false);
    }
}

3.在子類構造方法中,super(…)調用父類構造時,必須是子類構造方法中第一條語句。

4.super(…)隻能在子類構造方法中出現一次,由與this(…)調用時也要在第一條語句,所以super(…)不能和this(…)同時出現,也就是是說子類構造方法中不能使用this(…)

4. 避免在構造方法中調用重寫的方法

一段有坑的代碼. 我們創建兩個類, B 是父類, D 是子類. D 中重寫 func 方法. 並且在 B 的構造方法中調用 func

class B {
    public B() {
// do nothing
        func();
    }
    public void func() {
        System.out.println("B.func()");
    }
}
class D extends B {
    private int num = 1;
    @Override
    public void func() {
        System.out.println("D.func() " + num);
    }
}
public class Main {
    public static void main(String[] args) {
        D d = new D();
    }
}

執行結果:

  • 構造 D 對象的同時, 會調用 B 的構造方法.
  • B 的構造方法中調用瞭 func 方法, 此時會觸發動態綁定, 會調用到 D 中的 func
  • 此時 D 對象自身還沒有構造, num 處在未初始化的狀態, 值為 0;如果具備多態性,num的值應該是1.
  • 所以在構造函數內,盡量避免使用實例方法,除瞭final和private方法。

【結論】:

“用盡量簡單的方式使對象進入可工作狀態”, 盡量不要在構造器中調用方法(如果這個方法被子類重寫, 就會觸發動態綁定, 但是此時子類對象還沒構造完成), 可能會出現一些隱藏的但是又極難發現的問題.

二. 對象的默認初始化

在Java方法內部定義一個局部變量時,用戶必須要將其賦值或者初始化,否則會編譯失敗;

但對象中的字段(成員變量),用戶不需要將其初始化就可直接訪問使用,這裡其原因在於new對象時,jvm會給出字段的默認初始化。

下面是new對象是時,jvm層面執行的概述:

1.檢測對象對應的類是否加載瞭,如果沒有加載則加載

2.為對象分配內存空間

3.處理並發安全問題

比如:多個線程同時申請對象,JVM要保證給對象分配的空間不沖突

4.初始化所分配的空間

即:對象空間被申請好之後,對象中包含的成員已經設置好瞭初始值

數據類型 默認值
byte 0
short 0
int 0
long 0
float 0.0f
double 0.0
char /u0000
boolean false
reference (引用類型) null

5.設置對象頭信息(關於對象內存模型後面會介紹)

6.調用構造方法,給對象中各個成員賦值

三. 就地初始化對象

在聲明成員變量時,就直接給出瞭初始值。

代碼編譯完成後,編譯器會將所有給成員初始化的這些語句添加到各個構造方法中

public class Date {
    public int year = 1900;
    public int month = 1;
    public int day = 1;
    
    public Date(){
    }
    
    public Date(int year, int month, int day) {
    }
    
    public static void main(String[] args) {
        Date d1 = new Date(2022,8,16);
        Date d2 = new Date();
    }
}

四. 類的初始化順序

1. 普通類(沒有繼承關系)

靜態部分(靜態變量、常量,靜態代碼塊)

  • 在類加載階段執行,類中存在多個靜態部分時,會按順序執行
  • 靜態代碼塊隻會執行一次,且靜態的變量、常量等隻會創建一份

非靜態部分(實例變量、常量、實例代碼塊)

當有對象創建時才會執行,按順序執行

最後執行構造方法,當有對象創建時才會執行

代碼演示:

class Person {
    public String name;
    public int age;
    public Organ organ = new Organ();
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("構造方法執行");
    }
    {
        System.out.println("實例代碼塊執行");
    }
    static {
        System.out.println("靜態代碼塊執行");
    }
}
class Organ {
    //...
    public Organ() {
        System.out.println("實例變量::organ");
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Person person1 = new Person("xin",21);
        System.out.println("==============");
        Person person2 = new Person("xinxin",20);
    }
}

執行結果:

2. 派生類( 有繼承關系)

靜態部分(靜態變量、常量,靜態代碼塊)

  • 父類靜態代碼塊優先於子類靜態代碼塊執行,且是最早執行
  • 隻有第一次實例化子類對象時,父類和子類的靜態部分會執行; 之後再實例化子類對象時,父類和子類的靜態部分都不會再執行

父類非靜態部分(實例變量、常量、實例代碼塊)和父類構造方法

子類非靜態部分(實例變量、常量、實例代碼塊)和子類構造方法

class Person {
    public String name;
    public int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("Person:構造方法執行");
    }
    {
        System.out.println("Person:實例代碼塊執行");
    }
    static {
        System.out.println("Person:靜態代碼塊執行");
    }
}
class Student extends Person{
    public Student(String name,int age) {
        super(name,age);
        System.out.println("Student:構造方法執行");
    }
    {
        System.out.println("Student:實例代碼塊執行");
    }
    static {
        System.out.println("Student:靜態代碼塊執行");
    }
}
public class TestDemo4 {
    public static void main(String[] args) {
        Student student1 = new Student("張三",19);
        System.out.println("===========================");
        Student student2 = new Student("gaobo",20);

    }
    public static void main1(String[] args) {
        Person person1 = new Person("bit",10);
        System.out.println("============================");
        Person person2 = new Person("gaobo",20);
    }
}

執行結果:

到此這篇關於詳解Java的構造方法及類的初始化的文章就介紹到這瞭,更多相關Java初始化內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: