1. 程式人生 > 其它 >Head First Java學習:第九章-構造器和垃圾收集器

Head First Java學習:第九章-構造器和垃圾收集器

物件的前世今生

物件如何建立、存在何處以及如何讓儲存和拋棄更有效率。

會述及堆、棧、範圍、構造器、超級構造器、空引用等。

1、記憶體的兩個區域:堆和棧

堆(heap):物件的生存空間,又稱為可垃圾回收的堆

棧(stack):方法呼叫和區域性變數。

2、變數的生存空間

  • 例項變數:宣告在類中方法之外的地方,存在於所屬的物件中,因此儲存在堆中。
  • 區域性變數:區域性變數和方法的引數都宣告在方法中,是暫時的,生命週期僅限於方法被放在棧的這段時間(方法呼叫至執行完畢),儲存在棧中。

3、方法在棧中的存放順序

根據呼叫順序依次放在棧頂,最先釋放的在最上面。

4、物件的區域性變數

非primitive變數只是儲存物件的引用。

物件存在堆中,不論物件是否宣告或建立,如果區域性變數是個對該物件的引用,只有變數本身會放在棧上。

物件本身存在堆上。

5、物件的例項變數:存放於物件所屬的堆空間中

需要多大的存放空間:

  • 例項變數是 primitive主資料型別:

java會根據主資料型別的大小,在物件所屬的堆空間為該例項變數留下空間。如int需要32位,long需要64位。

變數所需要的空間在物件中。

  • 例項變數是 物件的引用

物件帶有物件引用的變數:此時真正的問題是,是否要保留物件帶有的所有物件的空間?--不帶

舉例1:

public class CellPhone{

    // 有宣告變數,未賦值;Antenna物件在堆上只會留下變數的空間

    private Antenna ant;

}

舉例2:

public class CellPhone{

    // 引用變數被賦值一個新的物件,新的Antenna物件在堆上佔有堆空間

    private Antenna ant = new Antenna();

}

結論:引用和物件都在堆中。

6、建構函式

物件建立三部曲:宣告引用變數,建立物件,連線物件和引用。

Duck myDuck = new Duck();

其中,建立物件是在呼叫 Duck的建構函式。

  • 什麼是建構函式:建構函式帶有你在初始化物件時,會執行的程式程式碼。新建一個物件時就會被執行。
  • 如果沒有寫建構函式,編譯器會幫你寫一個:public Duck(){}
  • 建構函式的特點:無返回型別;與類同名

7、構造Duck

程式碼:

public class Duck{
    public Duck(){
        System.out.println("Quck!");
    }
}
public class UseADuck {
    public static void main(String[] args) {
        Duck d = new Duck();
    }
}

輸出:Quck!

總結:建構函式讓你有機會介入new的過程中。

8、新建Duck狀態的初始化

程式碼:

public class Duck {
    int size;

    public Duck(){
        System.out.println("Quack!");
    }

    public void setSize(int newSize){
        size = newSize;
    }
}

程式碼:

public class UseADuck {
    public static void main(String[] args) {
        Duck d = new Duck();
        d.setSize(45);
    }
}

執行結果:Quack!

總結:

大部分人都是使用建構函式來初始化物件的狀態,即設定和給物件的例項變數賦值。在上面的程式碼中,可以使用setSize()來設定大小,但這會讓Duck暫時處於沒有大小數值的狀態(例項變數沒有預設值),且需要兩行搞定。

問題:先構造物件再設定大小會很危險,萬一忘記設定會出問題。

9、使用建構函式來初始化Duck的狀態

程式碼:

public class Duck02 {
    int size;

    public Duck02(int duckSize){
        System.out.println("Quack!");
        // 把初始化的程式程式碼放到建構函式中,然後把建構函式設定成需要引數的
        size = duckSize;
        System.out.println("size is "+size);
    }
}
public class UseADuck02 {
    public static void main(String[] args) {
        // 只需要一行就可以創建出新的Duck並且設定好大小
        Duck02 d = new Duck02(43);
    }
}

結果:

Quack!

size is 43

總結:給建構函式加引數,使用引數的值設定size的例項變數。只需要一行就可以創建出新的Duck並且設定好大小。

10、有參和無參構造方法

讓使用者建立物件的時候有選擇。

程式碼:

/**
 * 過載構造引數
 */
public class Duck03 {
    int size;

    // 無參構造方法
    public Duck03(){
        size = 27;
        System.out.println("size is "+ size);
    }

    // 有參構造方法
    public Duck03(int duckSize){
        size = duckSize;
        System.out.println("size is "+ size);
    }
}

程式碼:

public class useADuck03 {
    public static void main(String[] args) {
        // 呼叫無參構造方法
        Duck03 d1 = new Duck03();
        System.out.println("-------------------");
        // 呼叫有參構造方法
        Duck03 d2 = new Duck03(45);
    }
}

結果:

size is 27

-------------------

size is 45

11、編譯器一定會幫你寫出沒有引數的建構函式嗎?

  • 完全沒有設定建構函式:編譯器幫你呼叫一個無參建構函式
  • 寫了有參建構函式:自己要寫上無參建構函式,編譯器不會呼叫

12、過載建構函式

程式碼:

/**
 * 過載構造引數:
 * 代表你有一個以上的建構函式且引數都不相同
 * 不能有相同的引數型別和順序
 */
public class Mushroom {
    //要知道引數多大
    public Mushroom(int size){}
    // 不知道引數多大
    public Mushroom(){}
    // 知道是否有魔力,不知道多大
    public Mushroom(boolean isMagic){}
    // 知道是否有魔力,知道多大
    public Mushroom(boolean isMagic,int size){}
    // 和上面相同,但是引數順序不同所以過關
    public Mushroom(int size,boolean isMagic){}
}

例項變數的預設值:0/0.0/false;引用變數的預設值:null

13、父類、繼承和建構函式的關係

1)  例項變數

繼承下來的父類的例項變數也會儲存在物件中。

建立某個物件時(new一個物件),物件會取得所有例項變數所需要的空間,包括繼承下來的例項變數的空間。

2)  父類的建構函式

建立物件時,所有繼承下來的建構函式都會執行。

執行new的指令,會啟動建構函式連鎖反應。

3)   建構函式鏈:

Hippo物件IS-A Animal,Animal IS-A Object。如果你要創建出Hippo,也得創建出 Animal 與 Object的部分。

所以建構函式在執行的時候,第一件事情時去執行它的父類的建構函式,這會連鎖反應到Object這個類為止。

4)  呼叫過程舉例:

程式碼:

public class Animal {
    public Animal(){
        System.out.println("Making an Anilmal");
    }
}

public class Hippo extends Animal{
    public Hippo(){
        System.out.println("Making a Hippo");
    }
}

public class TestHippo {
    public static void main(String[] args) {
        System.out.println("starting...");
        Hippo h = new Hippo();
    }
}

結果:

starting...

Making an Anilmal

Making a Hippo

說明:先呼叫父類的建構函式,再呼叫自身的建構函式。

執行過程如下:

5) 如何呼叫父類的建構函式:super()

唯一方法:super()

含義:呼叫其父類的無參構造器。

程式碼舉例:

public class Animal {
    public Animal(){
        System.out.println("Making an Anilmal");
    }
}

public class Hippo02 extends Animal{
    int size;

    public Hippo02(int newSize){
        // 呼叫父類的建構函式
        super();
        size = newSize;
    }
}

6) 如果沒有呼叫super() 會發生什麼?

編譯器會幫我們加上super() 的呼叫

編譯器有兩種涉入建構函式的方法:

第一種:沒有編寫建構函式

編譯器會加super()及建構函式。

public ClassName(){
    super();
}

第二種:有建構函式但是沒有呼叫super()

編譯器會幫你對每個過載版本的建構函式,加上這種呼叫:super().

編譯器幫忙加的一定是沒有引數的版本,即使父類有多個過載版本,也只有無引數的版本會被呼叫到。

7)對super()的呼叫必須是建構函式的第一個語句。

8)有引數的父類建構函式:怎麼傳參?

例項變數name私有的,不能被繼承。Hippo有getName()方法但是沒有name例項變數,所以需要通過Animal維持name例項變數,然後從getName()來返回這個值。

程式碼:

public abstract class Animal02 {
    // 每個Animal02都有名字
    private String name;

    // Hippo03 會繼承這個getter
    public String getName(){
        return name;
    }

    // 有引數的建構函式,用來設定name
    public Animal02(String theName){
        name = theName;
    }
}

public class Hippo03 extends Animal02{
    public Hippo03(String name){
        // 傳給Animal的建構函式
        super(name);
    }
}

public class makeHippo {
    public static void main(String[] args) {
        Hippo03 h= new Hippo03("Buffy");
        System.out.println(h.getName());
    }
}

結果:

Buffy

總結:

通過super()來引用父類,所以要從這裡把name值都傳進去,讓Animal把它存到私有的name 例項變數中。

13、this() 從建構函式呼叫另一個過載版的另一個建構函式

  • 使用this() 來從某個建構函式呼叫同一個類的另外一個建構函式
  • this()只能用在建構函式中,且必須是第一行語句
  • super() 與 this() 不可兼得
  • this() 中的引數,根據需要呼叫的構造方法決定

舉例:

import java.awt.*;

public class Mini extends Car{
    Color color;
    public Mini(){// 無引數的建構函式以預設顏色呼叫真正的建構函式
        this(Color.RED);
    }

    public Mini(Color c){// 真正的建構函式
        super("Mini");
        color = c;
        // 初始化動作
    }

    public Mini(int size){// 有問題,不能同時呼叫super()和this()
        this(Color.RED);
        super(size);
    }
}

14、物件會存活多久?

  • 物件:生命週期看引用到它的“引用”。如果引用還活著,物件也會繼續活著;如果引用死了,物件也會跟著“陪葬”
  • 引用變數:

        例項變數:壽命和物件相同,物件活著,例項變數也活著。

        區域性變數:只存活在宣告該變數的方法中。

15、區域性變數的生命期和作用域

life:只要變數的堆疊塊還存在於堆疊中上,區域性變數就算活著,活到方法執行完畢。

Scope:區域性變數的範圍只限於宣告它的方法之內。

區域性變數在堆疊中,狀態儲存;

區域性變數所在方法在棧頂,才能被使用。

16、引用變數的生命期和作用域

1)變數的生命週期如何影響物件的生命週期

引用活著,物件活著。

當物件的最後一個引用消失,物件就會變成可回收的。

2)釋放物件引用的三種方法