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)釋放物件引用的三種方法