首頁 > 軟體

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

2022-08-16 18:04:22

一. 利用構造方法給物件初始化

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.初始化所分配的空間

即:物件空間被申請好之後,物件中包含的成員已經設定好了初始值

資料型別預設值
byte0
short0
int0
long0
float0.0f
double0.0
char/u0000
booleanfalse
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初始化內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com