protected/super

June 6, 2022

就之前的 RPG 遊戲來說,如果建立了一個角色,想顯示角色的細節,必須如下撰寫:

var swordsMan = new SwordsMan();
...
System.out.printf("劍士 (%s, %d, %d)%n", swordsMan.getName(),
        swordsMan.getLevel(), swordsMan.getBlood());
var magician = new Magician();
...
System.out.printf("魔法師 (%s, %d, %d)%n", magician.getName(),
        magician.getLevel(), magician.getBlood());

protected 成員

這對使用 SwordsManMagician 的客戶端有點不方便,如果你可以在 SwordsManMagician 定義個 toString` 方法,傳回角色的字串描述:

public class SwordsMan extends Role {
    ...
    public String toString() {
        return String.format("劍士 (%s, %d, %d)", this.getName(),
                this.getLevel(), this.getBlood());
    }
}

public class Magician extends Role {
    ...
    public String toString() {
        return String.format("魔法師 (%s, %d, %d)", this.getName(),
                this.getLevel(), this.getBlood());
    }
}

客戶端就可以如下撰寫:

var swordsMan = new SwordsMan();
...
System.out.println(swordsMan.toString());
var magician = new Magician();
...
System.out.printf(magician.toString());

看來客戶端簡潔許多。不過你定義的 toString 在取得名稱、等級與血量時不是很方便,因為 Rolenamelevelblood 被定義為 private,無法直接於子類別中存取,只能透過 getNamegetLevelgetBlood 來取得。

Rolenamelevelblood 定義為 public?這又會完全開放 namelevelblood 存取權限,你並不想這麼作,只想讓子類別可以直接存取 namelevelblood 的話,可以定義它們為 protected

package cc.openhome;

public abstract class Role {
    protected String name;
    protected int level;
    protected int blood;
    ...
}

被宣告為 protected 的成員,相同套件中的類別可以直接存取,不同套件中的類別可以在繼承後的子類別直接存取。現在你的 SwordsMan 可以如下定義 toString

package cc.openhome;

public class SwordsMan extends Role {
    ...
    public String toString() {
        return String.format("劍士 (%s, %d, %d)", this.name, 
                this.level, this.blood);
    }
}

Magician 也可以如下撰寫:

package cc.openhome;

public class Magician extends Role {
    ...
    public String toString() {
        return String.format("魔法師 (%s, %d, %d)", this.name, 
                this.level, this.blood);
    }
}

如果方法中沒有同名參數,this 可以省略,不過基於程式可讀性,多打個 this 會比較清楚。

使用 super

有時候重新定義方法時,並非完全不滿意父類別中的方法,只是希望在執行父類別中方法的前、後作點加工。例如,也許 Role 類別中原本就定義了 toString 方法:

package cc.openhome;

public abstract class Role {
    ...
    public String toString() {
        return String.format("(%s, %d, %d)", this.name, 
                this.level, this.blood);
    }
}

如果在 SwordsMan 子類別中重新定義 toString 的內容時,可以執行 RoletoString 方法取得字串結果,再串接 "劍士" 字樣,不就是你想要的描述了嗎?可以於呼叫方法前,加上 super 鍵字。例如:

package cc.openhome;

public class SwordsMan extends Role {
    ...
    @Override
    public String toString() {
        return "劍士 " + super.toString();
    }
}

類似地,Magician 在重新定義 toString 時,也可以如法泡製:

package cc.openhome;

public class Magician extends Role {
    ...
    @Override
    public String toString() {
        return "魔法師 " + super.toString();
    }
}

可以使用 super 關鍵字呼叫的父類別方法,不能定義為 private(因為這就限定只能類別內使用)。

重新定義方法時,對於父類別中的方法權限,只能擴大但不能縮小。若原來成員 public,子類別中重新定義時不可為 privateprotected

static 方法屬於類別擁有,如果子類別中定義了相同簽署的 static 成員,該成員屬於子類別所有,而非重新定義,static 方法也沒有多型,因為物件不會個別擁有 static 成員。

如果類別有繼承關係,在建構子類別實例後,會先進行父類別定義的初始流程,再進行子類別中定義的初始流程,也就是建構子類別實例後,會先執行父類別建構式定義的流程,再執行子類別建構式定義的流程。

建構式可以重載,父類別中可重載多個建構式,如果子類別建構式中沒有指定執行父類別中哪個建構式,預設會呼叫父類別中無參數建構式。如果你如下撰寫程式:

class Some {
    Some() {
        System.out.println("呼叫Some()");
    }
}

class Other extends Some {
    Other() {
        System.out.println("呼叫Other()");
    }
}

如果嘗試 new Other(),看來好像是先執行 Some() 中的流程,再執行 Other() 的流程,也就是先顯示 "呼叫Some()",再顯示 "呼叫Other()"。很奇怪是吧!先繼續往下看,就知道為什麼了。如果想執行父類別中某建構式,可以使用 super 指定。例如:

class Some {
    Some() {
        System.out.println("呼叫Some()");
    }
    Some(int i) {
        System.out.println("呼叫Some(int i)");
    }
}

class Other extends Some {
    Other() {
        super(10);
        System.out.println("呼叫Other()");
    }
}

在這個例子中,new Other() 時,先呼叫了 Other() 版本的建構式,super(10) 表示呼叫父類別建構式時傳入數值 10,因此就是呼叫了父類別中 Some(int i) 版本的建構式,而後再繼續 Other()super(10) 之後的流程。

其實當你這麼撰寫時:

class Some {
    Some() {
        System.out.println("呼叫Some()");
    }
}

class Other extends Some {
    Other() {
        System.out.println("呼叫Other()");
    }
}

先前談過,如果子類別建構式中沒有指定執行父類別中哪個建構式,預設會呼叫父類別中無參數建構式,也就是等於你這麼撰寫:

class Some {
    Some() {
        System.out.println("呼叫Some()");
    }
}

class Other extends Some {
    Other() {
        super();
        System.out.println("呼叫Other()");
    }
}

也就是執行 new Other() 時,是先執行 Other() 的流程,而 Other() 中指定呼叫父類別無參數建構式,而後再執行 super() 之後的流程。

this()super() 只能擇一呼叫,而且一定要在建構式第一行執行。

那麼你知道以下為什麼會編譯錯誤嗎?

protected/super

編譯器會在你沒有撰寫任何建構式時,自動加入沒有參數的預設建構式(Default constructor),如果自行定義了建構式,就不會自動加入任何建構式了。在上圖中,Some 定義了有參數的建構式,所以編譯器不會再加入預設建構式,Other 的建構式中沒有指定呼叫父類別中哪個建構式,那就是預設呼叫父類別中無參數建構式,但父類別中現在哪來的無參數建構式呢?因此編譯失敗了!

分享到 LinkedIn 分享到 Facebook 分享到 Twitter