Java – 物件導向 – 繼承觀念

基本觀念


繼承的觀念一樣源自於現實社會中對於繼承二字的直覺觀念, 也就是父類別擁有的屬性及方法, 在子類別中, 只要存取權限允許下, 就自動也擁有父類別的屬性及方法. 這樣的開發觀念, 可以充分的運用了功能性的擴展, 以及多樣化的子類別實現.

舉例來說, 武器應該是在許多遊戲中的必備物件, 但是武器又有很多不同種類的武器, 可能還可以區分為防禦型武器, 攻擊型武器, 越想越頭大…

此時, 何不先來單純的思考一件事, 武器應該具有哪些特徵:

  • 武器名稱
  • 特定的擁有人
  • 目前狀態(損耗程度)

再來往下發展, 十字弓, 步槍, 機關槍, 也具有以上三項特徵. 那就不需要重複開發, 只需要直接繼承武器類別, 就可以擁有以上三項特徵, 而各自再去開發撰寫不一樣的部分即可.

前面的單元, 曾經設計過 Bike 類別. 想一想摩托車(速克達Scooter), 也是具有速度的屬性, 可以加速/減速, 傳回目前的速度值等等. 所以就從Bike來進行相關練習.

 

父類別 Super class


從 Bike.java 出發…

// 腳踏車的設計圖
public class Bike extends Object {
  private double speed;	// 速度
  // 定義建構式
  Bike(){
    System.out.println("Bike()");
  }
  Bike(double speed){
    this.speed = speed;
    System.out.println("Bike(double)");
  }
  
  // 加速功能
  void upSpeed() {
    if (speed < 1) {
      speed = 1;
    }else {
      speed *= 1.2;
    }
  }
  
  // 煞車功能
  void downSpeed() {
    if (speed < 1) {
      speed = 0;
    }else {
      speed *= 0.7;
    }
  }
  
  // 加裝速度表
  double getSpeed() {
    return speed;
  }
}

建構式在繼承行為上的特性


已知目前 Bike 擁有兩個建構式:

  • Bike()
  • Bike(double speed)

開始撰寫 Scooter 類別, 在宣告定義類別時, 透過

extends 父類別名稱

來指定繼承的父類別

public class Scooter extends Bike{

}

因為沒有Scooter自行定義的建構式, 所以將會以Bike中無傳參數的建構式, 為其唯一的建構式.

public class PlayScooterV1 {

  public static void main(String[] args) {
    Scooter scooter1 = new Scooter();
  }

}

執行結果, 將會是:

Bike()

這是在建構式單元中已經建立的觀念. 現在, 使Scooter類別有自己的建構式…

public class Scooter extends Bike{
  public Scooter() {
    System.out.println("Scooter()");
  }
}

回到 PlayScooterV1 執行後, 發現其結果如下:

Bike()

Scooter()

奇怪了, 不是說自己類別有定義建構式, 就不再使用父類別無傳參數的建構式嗎? 重點來囉…

在繼承的行爲中, extends 父類別 只是宣告而已, 而當任何一個物件實體被建立出來, 如何能夠呼叫使用父類別物件實體的屬性及方法呢? 就是讓記憶體中也同時存在父類別的物件實體先, 有了父類別的物件實體之後, 再來產生需要的子類別物件實體. 但是, 對於開發者而言, 直覺上就是需要子類別物件實體而已, 一旦還要追朔到祖宗八代, 反而增加了開發的困擾及負擔,

因此, 就讓這件事, 自動發生在呼叫建構式的初始化的第一道敘述句, 開發者毋須自行處理這件事, 相當於在建構式第一道敘述句之前, 就已經執行了 super(), 來呼叫出父類別物件實體的建構式, 也已經確保父類別物件實體已經存在了.

public class Scooter extends Bike{
  public Scooter() {
    // super() ==> 自動加上, 無需特別去呼叫
    System.out.println("Scooter()");
  }
}

而上例中的 super() 將呼叫 Bike() 建構式, 所以輸出結果, 將會是先印出 Bike(), 再來印出 Scooter().

這裡所闡述的觀念, 並不與之前建構式中提及的, 如果沒有定義建構式, 將會使用父類別無傳參數的建構式, 為其唯一建構式. 因為, 之前的觀念源頭是, 任何類別都一定有自己的建構式的前提下, 如果沒有, 編譯器會幫忙處理的部分.

而此處的源頭觀念是, 任何一個子類別物件實體之所以存在, 其祖宗八代的物件實體, 都已經自動(強制)的存在了.

接下來開始有所變化.

如果父類別有其他不是無傳參數的建構式, 在子類別的建構式中想要呼叫使用, 則該如何處理呢? 很簡單的, 此時必須在建構式的第一道敘述句, 直接講明要呼叫哪一個父類別的建構式, 如下:

public class Scooter extends Bike{
  public Scooter() {
    super(1.2); 	// 必須放在第一道敘述句, 呼叫 Bike(double speed)
    System.out.println("Scooter()");
  }
}

則執行結果如下:

Bike(double)

Scooter()

也可以直接呼叫自己類別中的其他建構式, 此時就要使用 this 的關鍵字. this 表示本類別所產生的物件實體, 而 this() 則是呼叫本類別中的其他建構式. 調整一下Scooter的建構式如下:

public class Scooter extends Bike{
  public Scooter() {
    this(4);
    System.out.println("Scooter()");
  }
  public Scooter(int n) {
    super(n); 	// 必須放在第一道敘述句, 呼叫 Bike(double speed)
    System.out.println("Scooter(int)");
  }
}

以 PlayScooterV1來實現

public class PlayScooterV1 {

  public static void main(String[] args) {
    Scooter scooter1 = new Scooter();
    System.out.println("---我是分隔線---");
    Scooter scooter2 = new Scooter(2);
  }

}

輸出結果如下:

Bike(double)

Scooter(int)

Scooter()

—我是分隔線—

Bike(double)

Scooter(int)

即使是呼叫  this() 建構式, 還是將會實現這個觀念, 任何子類別物件實體存在, 其各自祖宗八代的物件實體也都會存在先.

注意: this 或是 super 來呼叫建構式的敘述句, 無論是自動(隱含)或是指定, 都只能有一道敘述句.(只有一個父類別物件實體)

 

屬性及方法的繼承特性


屬性及方法都是在繼承的廣義範圍內, 但是, 在父類別中, 也可能會針對特定的屬性及方法, 設定存取修飾字, 來決定繼承之子類別是否可以進行存取.

成員(屬性及方法)的修飾字有以下四種:

  1. public : 全部(全世界)都可以
  2. protected : 繼承的子類別及相同Package
  3. 沒有存取修飾字 : 相同Package
  4. private : 僅相同類別中

而就繼承這件事來看, 如果是相同 Package 下, 宣告為 public, protected 及沒有存取修飾字的情況下, 都可以繼承存取; 如果是在不同Package 下, 則只有 public 與 protected 可以繼承存取.

提醒: 以上的存取修飾字的範圍, 也適用於建構式, 只是建構式並未有繼承擁有的觀念.

以 Bike 的類別來看, speed 的屬性被宣告為 private, 從此之後, 最多也只能透過 Bike 類別中的方法才能對  speed 的屬性進行存取使用, 對於要發展成 Scooter 有點不方便, 因此, 將其 private 修改為 protected, 如下:

// 腳踏車的設計圖
public class Bike extends Object {
  protected double speed;	// 速度 => 存取修飾字改為 protected
  // 定義建構式
  Bike(){
    System.out.println("Bike()");
  }
  Bike(double speed){
    this.speed = speed;
    System.out.println("Bike(double)");
  }
  
  // 加速功能
  void upSpeed() {
    if (speed < 1) {
      speed = 1;
    }else {
      speed *= 1.2;
    }
  }
  
  // 煞車功能
  void downSpeed() {
    if (speed < 1) {
      speed = 0;
    }else {
      speed *= 0.7;
    }
  }
  
  // 加裝速度表
  double getSpeed() {
    return speed;
  }
}

來到 Scooter 類別中, 增加一個屬性 gear, 稱之為檔位. 就是模擬摩托車的檔車, 可以設定目前的排檔.

public class Scooter extends Bike{
  protected int gear;	// 檔位
  
  public Scooter() {
    this(1);
  }
  public Scooter(int gear) {
    if (gear > 0 && gear <= 4) {
      this.gear = gear;
    }else {
      this.gear = 1;
    }
  }
  public boolean setGear(int gear) {
    if (gear >0 && gear <=4) {
      this.gear = gear;
      return true;
    }else {
      return false;
    }
  }
  
}

因為宣告了 gear 的屬性為 int, 對於物件而言, 其預設值為 0. 而在建構式中, 就是想盡辦法使  gear 的範圍落在 1 到 4 之間的整數值. 雖然也提供了設定檔位 setGear(int) 方法, 也使其範圍相同, 如果傳遞參數超出範圍, 則沒有變更, 並傳回 false; 如果設定新檔位成功, 將傳回 true.

此時看到了繼承的一個特性, 將父類別的功能擴增了 setGear().

 

方法覆寫(Method Override)


將原本父類別所定義的方法, 進行改寫:

  • 方法名稱一樣
  • 傳遞參數的型別及個數一樣

例如以下:

public class Scooter extends Bike{
  protected int gear;	// 檔位
  
  public Scooter() {
    this(1);
  }
  public Scooter(int gear) {
    if (gear > 0 && gear <= 4) {
      this.gear = gear;
    }else {
      this.gear = 1;
    }
  }
  public boolean setGear(int gear) {
    if (gear >0 && gear <=4) {
      this.gear = gear;
      return true;
    }else {
      return false;
    }
  }
  
  @Override
  void upSpeed() {
    super.upSpeed(); // 先呼叫執行父類別的方法
    // 再處理改寫的內容
    speed *= (1 + gear*0.1);
  }
  
}

對於 Scooter 的物件實體而言, 當呼叫使用了 upSpeed() 方法, 與原先父類別所定義的 upSpeed() 方法的結果是不一樣的.

通常, 對於 Override 的方法而言, 是在進行改良, 改進的工程, 會事先呼叫父類別原來的方法之後, 再進行改良或是改進. 而呼叫父類別原來的方法, 或是其他方法, 則是 super.方法(參數). 但是, 並非一定要這麼做, 而且也不一定要在第一列, 也沒限制只能呼叫一次.

Override 是繼承觀念下的特徵, 而繼承的目的就是在於發揚光大發揚光大發揚光大. 所以,

  • 存取修飾字的範圍要大於或是等於父類別的原方法
  • 傳回值型別
    • 基本型別及void 都要一模一樣
    • 物件型別, 則必須一樣或是其子類別. 因為子類別就是發揚光大, 所以連傳回值型別也一樣要發揚光大

 

 

本站資源一切隨緣,
不用註冊, 不看廣告
如果對您有所助益,
歡迎功德隨喜, 金額隨意,
請點擊以下...(感謝您)

功德箱/打賞箱

%d bloggers like this: