繼承

介紹繼承的概念及程式的撰寫方式。

為什麼需要繼承? Why Inheritance?

試想一種情況:

有一個Aminal類別,它的定義如下:

屬性:
重量、身長、年齡。

方法:
移動。

好,這個類別定義好,現在要再定義一個Dog的類別:

屬性:
重量、身長、年齡、毛色。

方法:
移動、吃、睡、吠叫。

有沒有發現到狗的定義中,許多東西都跟動物重複。

在現實中,狗是一種動物,應該擁有動物的屬性及方法,然後再加上狗專屬的屬性和方法。

以這個例子來說,我們可以把動物當成父類別(或稱超類別super class),狗『繼承』動物,狗是『子類別 subclass』。

子類別會擁有父類別的所有屬性、方法,再加上自己定義的屬性及方法,所以可以說子類別是父類別的延伸(extend)。 (這句話超重要!多個看幾十次,想一想)

類別圖:

這是UML (統一建模語言,Unified Modeling Language) 的類別圖(簡易版),常用來描述類別之間的關係。 實心箭頭表示繼承關係,由子類別指向父類別。

圖中讀作 Dog 繼承 Animal。 另外一種常見的說法是: Dog is a Animal.

繼承的概念用『is a』來表述。 反過來說 Animal is a Dog.是不成立的,利用『is a』可以幫助思考。

Java程式要如何表示繼承關係呢? 利用關鍵字 extends:

class 子類別 extends 父類別{
    // code
}

沒錯,就是延伸(extends),父類別定義的東西,子類別只要繼承就等於全部擁有了,然後以父類別擁有的成員為基本,再延伸出自己特有的東西。

以上面假設的 Animal 與 Dog 類別來看,程式會長這樣:

class Animal{
    int height;
    int weight;
    int age;
    void move(){
    } 
} // end of class Animal

class Dog extends Animal{
    Color hair;
    void eat(){
    }
    void sleep(){
    }
    void bark(){
    }
} // end of class Dog

有沒有稍微體會到繼承的方便性呢?

一般化、特殊化

我們可以將繼承想成是一般化(Generalization)與特殊化(Specialization)的關係,繼承樹上越頂端的父類別擁有越『一般』的特性,越底端的子類別越『特殊』。

子類別擁有父類別定義的所有成員,再多了自己特有的東西。

看起來就像這樣:

父類別的功能少,子類別的功能多,不要因為父類別的英文是(super)就覺得比較厲害。

萬物之父 Object

我們知道Java是純物件導向的程式語言,而每個類別,包括自訂的類別,都繼承Object。

特別要提的是Java只支援『單向繼承』,也就是說一個子類別只可以有一個父類別,不過一個父類別可以被多個子類別繼承。

定義類別的時候,如果沒有使用關鍵字extends,Java會自行extends Object

class Animal{
    // code
}
// 等價於下面敘述,Java會自動幫你extends Object
class Animal extends Object{
    // code
}

關鍵字 this 、 super

this跟super都是關鍵字,都是reference。 指到哪裡呢?

this

指到自己,也就是自己類別的成員。

class Human{
    String name;
    int age;
    Human(String str){
        this.name = str;
    }
    String getName(){
        return this.name;
    }
}

上述程式中, this.name意思是『自己這個類別的成員name』,當然在這個情況不寫也無所謂,但繼承關係越複雜的情況下這樣寫法可以大大增加程式的可讀性。

自己的建構字 this(.);

如果寫了很多建構子提供多元的建構物件方式,建構子之間彼此可以互相呼叫:

class Human{
    String name;
    int age;
    static int totalCount = 0;
    Human(){
        name = "untitled";
        age = -1;  // 使用-1來標記沒有被設定,否則會初始化為0,但人類有可能0歲
        totalCount++;
    }
    Human(String str){  
        this();                          
        this.name = str;
    }
    Human(String str,int a){
        this(str);  
        this.age = a;
    }
    void printInfo(){
        System.out.println(name+" 年齡:"+age+" 目前總人數:"+totalCount);
    }
}

上述程式中,this()表示呼叫自己不帶參數的建構子,this(String)表示呼叫自己帶有一個字串參數的建構子,以此類推。

這樣寫的好處是,各建構子之間有功能擴充的效果,已經寫好的程式可以被充分的再利用,要修改某個環節也比較不會出錯。

特別要注意的是:

this(.) 建構子只能放在第一行!!!

『Constructor call must be the first statement in a constructor.』

Human(String str){  // 如果這樣寫會編譯錯誤,底下兩行位置需要互換
    this.name = str;
    this();        // 因為要讓建構子跑完,初始化好東西,才能做後續的設定
}

好,那用定義好的3個建構子來測試一下程式:

class Test{
    public static void main(String[] args){
        Human h1 = new Human();
        h1.printInfo();
        Human h2 = new Human("小木");
        h2.printInfo();
        Human h3 = new Human("小婷",18);
        h3.printInfo();
    }// end of main(String[])
}

執行結果:

untitled 年齡:-1 目前總人數:1
小木 年齡:-1 目前總人數:2
小婷 年齡:18 目前總人數:3

super

指到父類別,使用方法跟this類似。

class Animal {
    int height;
    int weight;
    static int totalCount = 0;

    Animal() {
        this(-1, -1);
    }
    Animal(int h) {
        this(h, -1);
    }
    Animal(int h, int w) {
        this.height = h;
        this.weight = w;
        totalCount++;
    }
    String getInfo() {
        return "身長:" + height + " 重量:" + weight;
    }
} // end of class Animal

class Dog extends Animal {
    String color;
    static int totalCount = 0;

    Dog() {
        this(-1, -1, "noset");
    }
    Dog(int h, int w) {
        this(h, w, "noset");
    }
    Dog(String c) {
        this(-1, -1, c);
    }
    Dog(int h, int w, String c) {
        super(h, w);
        this.color = c;
        totalCount++;
    }
    String getInfo() {
        return super.getInfo() + " 毛色:" + this.color;
    }
} // end of class Dog

程式有點長,慢慢看沒關係。

Animal中帶有兩個參數的建構子 Animal(int h,int w) 視為主要負責初始化功能的運算子,其他參數比較少的建構子就負責呼叫這個建構子。

Dog的建構子中,主要運做的是 Dog(int h,int w,String c),其他參數比較少的運算子只需要設計應該帶什麼參數給他。 這種被稱為方法的包裝(wrapped) 是常見且比較容易設計的做法。

父類別的建構子 super(.)

利用super(.)可以呼叫父類別中定義好相應參數的建構子,那為什麼還要特地呼叫父類別的建構子呢? 很多時候父類別已經定義好的東西,子類別直接用就好,設計上比較好維護,設計邏輯比較有階層性。

覆寫 Override

再來看getInfo()方法,Animal裡面已經定義了一個,依據繼承的理論,Dog繼承Animal應該不用自己寫也會有一個getInfo()才對。 沒錯,Dog如果不自己定義會有一個跟Animal『一模一樣』的getInfo()方法,但很明顯,父類別太過於一般化,沒辦法滿足子類別需要的功能(以此為例就是資訊量不夠),所以子類別『覆寫(override)』了父類別的方法,創造了特殊且適合自己的getInfo()。

super.方法()、super.欄位,就是呼叫父類別那邊的方法、欄位,當然前提是存取修飾子允許你看到。

測試一下執行結果:

class Test {
    public static void main(String[] args) {
        Dog d1 = new Dog();
        System.out.println(d1.getInfo());

        Dog d2 = new Dog(30, 10);
        System.out.println(d2.getInfo());

        Dog d3 = new Dog("white");
        System.out.println(d3.getInfo());

        Dog d4 = new Dog(30, 10, "white");
        System.out.println(d4.getInfo());

        System.out.println("動物數量:" + Animal.totalCount);
        System.out.println("狗狗數量:" + Dog.totalCount);
    }// end of main(String[])
}// end of class Test

執行結果:

身長:-1 重量:-1 毛色:noset
身長:30 重量:10 毛色:noset
身長:-1 重量:-1 毛色:white
身長:30 重量:10 毛色:white
動物數量:4
狗狗數量:4

層層初始化

我們知道子類別擁有父類別的所有程式碼,那初始化順序呢?

初始化子類別前,還要先初始化父類別,畢竟父類別建構出來才有足夠的基礎資料去建構『延伸』的部份,所以每個類別要建構的時候,都會往上追朔,追朔到 Object 開始一層一層建構下來,也是因為這樣,子類別才能擁有父類別的所有成員。

舉個例子:

class A{
    A(){
        System.out.println("這裡是A的建構子");
    }
}
class B extends A{
    B(){
        System.out.println("這裡是B的建構子");
    }
}
class C extends B{
    C(){
        System.out.println("這裡是C的建構子");
    }
}

建構一個C物件試試:

class Test {
    public static void main(String[] args) {
        C c = new C();
    }// end of main(String[])
}// end of class Test

執行結果:

這裡是A的建構子
這裡是B的建構子
這裡是C的建構子

哇塞,真是太神奇了!

看一下類別示意圖:

我們要建構的是C,而C是B的延伸,所以要先有B,而B是A的延伸,所以要先有A,而A是Object的延伸,所以要先有Object。 於是就從最頂端的父類別一直建構下來。

好,現在我知道需要從父類別初始化下來,但建構子呢? 一個類別可以定義無數個建構子,他怎麼知道我要用哪個建構子來建構我的物件? 到底是以什麼機制來建構父類別的?

嗯,回想一下,當初在定義類別的時候,如果沒有定義任何建構子,Java會幫你定義一個不帶參數不做任何事的建構子,現在同樣的老招又來一次!

只要你的建構子中『沒有呼叫任何建構子』,就會在『第一行』偷偷幫你家上去一個 super(); 有多偷偷呢? 你連看都看不到!! 但他就是存在於最後的程式碼中。

以上的程式來說,就像這樣:

class A{
    A(){
        super();  // 這行不寫的話,Java會幫你加上,但你看不到
        System.out.println("這裡是A的建構子");
    }
}
class B extends A{
    B(){
        super();  // 這行不寫的話,Java會幫你加上,但你看不到
        System.out.println("這裡是B的建構子");
    }
}
class C extends B{
    C(){
        super();  // 這行不寫的話,Java會幫你加上,但你看不到
        System.out.println("這裡是C的建構子");
    }
}

好的,現在知道他會自動幫我呼叫super();來建構父類別,但是如果我不想用這個不帶參數的建構子呢? 我辛苦設計那麼多建構子,他只會幫我呼叫不帶參數的,太慘了吧!

嗯嗯,沒錯就是這麼慘,所以如果要呼叫有帶參數的super(.);你就要自己寫!

觀察底下程式,想想執行結果:

class A{
    A(){
        System.out.println("這裡是A的建構子");
    }
}
class B extends A{
    B(){
        System.out.println("這裡是B的建構子");
    }
    B(String str){
        this();
        System.out.println("嗨這裡是B:"+str);
    }
}
class C extends B{
    C(){
        this("hello tina");
        System.out.println("這裡是C的建構子");
    }
    C(String str){
        super(str);
        System.out.println("嗨這裡是C:"+str);
    }
}

主程式:

class Test {
    public static void main(String[] args) {
        C c = new C();
    }// end of main(String[])
}// end of class Test

執行結果:

這裡是A的建構子
這裡是B的建構子
嗨這裡是B:hello tina
嗨這裡是C:hello tina
這裡是C的建構子

如果跟你想的不一樣,在重新看一下上面的描述再想想,哪理卡卡的可以問我。 這裡是重要的繼承觀念。

存取修飾子 protected

存取修飾子的章節提過,現在剛好提到繼承再拿出來討論。

protected是個關鍵字,開放的最大權限為『不同套件的子類別』可以存取。

假設 Animal 與 Dog 位在不同package,先看Animal的程式碼:

 package A;
 public class Animal {
     public String name;  // 4個屬性剛好4種權限範圍都做測試
     protected int height;
     int weight;
     private int age;
     // ↓這個修飾子一定要public或protected,不然不同類別的Dog不能用他來建構物件
     public Animal(String str,int h,int w,int a){
         this.name = str;
         this.height = h;
         this.weight = w;
         this.age = a;
     }
 }

再看Dog的程式碼:

 package B;
 import A.Aminal;
 public class Dog extends Animal{
     String color;
     public Dog(String str,int h,int w,int a,String c){
         super(str,h,w,a);
         this.color = c;
     }
     public void printInfo(){
         System.out.println(name);    // OK, public 不同套件也可以存取
         System.out.println(height);  // OK, protected 允許不同套件子類別存取
         System.out.println(weight);  // 編譯錯誤,預設只有同 package 可以存取
         System.out.println(age);     // 編譯錯誤, private 只有自身類別能存取
         System.out.println(color);   // OK, 自己類別定義的成員當然OK
     }
 }

覆寫的存取修飾限制

上面的範例程式有稍為提到過覆寫(override),這邊再詳細討論一下,以及一些限制。

覆寫 Override,字面上的意思就是『覆蓋重寫』。

在繼承中關係,父類別定義了一些方法,子類別覺得不適用的話可以『覆蓋』掉父類別的方法,然後『重寫』屬於自己的方法。

舉個例子:

class A{
    void printInfo(){
        System.out.println("hello, I am A.");
    }
}
class B extends A{
    void printInfo(){
        System.out.println("hello, I am B.");
    }
}
class C extends A{
}

測試程式:

class Test {
    public static void main(String[] args) {
        B b = new B();
        b.printInfo();
        C c = new C();
        c.printInfo();
    }// end of main(String[])
}// end of class Test

執行結果:

hello, I am B.
hello, I am A.

上述程式中,B與C都是繼承A,表示擁有了A所有的成員,但B覆寫了printInfo()方法,而C沒有。 所以在呼叫的時候,物件b會使用B類別覆寫的方法,而物件c因為C類別沒有自己定義,所以會使用到父類別A所定義的printInfo()。

好,那來談談覆寫的限制。

要覆寫父類別方法必須滿足幾個條件:

  1. 父類別方法不能用 final 修飾。

  2. 子類別覆寫的方法名稱、回傳型態、參數個數順序需相同。

  3. 子類別覆寫的方法,其開放權限不可以小於要覆寫的父類別方法。

第一點,用final修飾的方法無法被覆寫。

這是關鍵字final修飾方法的特性,詳細內容於後面討論。

第二點,方法名稱、回傳型態、參數個數必須相同。

嗯,如果不一樣的話,就是自己再定義一個新方法了阿!!跟覆寫有什麼關係 XD

第三點,子類別方法開放權限不得小於父類別方法。

簡單來說,如果父類別說這個方法是對全世界公開(public)的方法,你要覆寫就不能占為己有(private)。

※存取修飾子的開放權限從大到小:public -> protected -> (no modifier) -> private。

如果父類別說此方法是protected,那子類別覆寫時的修飾子必須是public或protected。

如果父類別說此方法是private,那子類別覆寫時的修飾子必須是public或protected或(no modifier)或private。

關鍵是權限的開放範圍不得小於覆寫對象。

以下針對三種限制用程式來說明~

第一點,程式範例:

class A{
    //       (↓關鍵字 final)
    public final void printInfo(){
        System.out.println("hello, this is A.");
    }
}
class B extends A{
    // 編譯錯誤!  ↓ 利用final修飾的方法不能被覆寫。
    public void printInfo(){
        System.out.println("hello, this is B;");
    }
}

在類別A的printInfo()方法利用關鍵字 final 修飾,所以任何繼承他的子類別都不能覆寫這個方法。 否則會產生編譯錯誤:『Cannot override the final method from A』。

第二點,程式範例:

class A{
    public void printInfo(){
        System.out.println("hello, this is A.");
    }
}
class B extends A{
    public void printInfo2Tina(){
        System.out.println("hello Tina, nice to meet you <3");
    }
}

恩,就是多定義一個方法,沒什麼好說的,這根本不是覆寫。

第三點,程式範例:

class A{
    // 注意存取修飾子是(no modifier)
    void printInfo(){
        System.out.println("hello, this is A.");
    }
}
class B extends A{
    // ↓ 編譯錯誤,覆寫的方法存取權限小於覆寫對象
    private void printInfo(){
        System.out.println("hello, this is B.");
    }
}

在A類別中的printInfo()方法修飾子是(no modifier),依據覆寫的開放權限規則,B類別繼承了A類別想覆寫printInfo(),覆寫的開放權限必須為public或protected或(no modifier),重點就是不能小於覆寫對象,否則會發生編譯錯誤:『Cannot reduce the visibility of the inherited method from A』。

Last updated