****# 物件導向

tags: java

目錄

前言

物件導向是一種對程式開發上的思考方式,與一般程式語言的的開發方式有所不同,Java是一種支援物件導向的語言,並不代表寫Java程式就是在使用物件導向,這是不一樣的兩件事情。

物件導向是一種思考方式,與程式語言無關,要完全理解並不容易,光物件導向這門技術,可能就可以寫上整本書來說明。Java作為一門支援物件導向的,幾乎完整的支援物件導向開發發方式的所有需求。

類別-封裝

定義類別

在Java物件導向中,物件並不會憑空出現,必須先使用class關鍵字定義類別,類別就相當於物件的規格書或是設計藍圖,在使用該類別來產生一個個的物件,再透過該物件提供的方法來操作物件。

其語法為:

public class 類別名稱 {
    類別成員
}

例如:

public class Car {
    ...
}

class School {
    ...
}

class Bank {
   ...
}

Car是類別的名稱,由於這個類別前面使用了「public」關鍵字來修飾,檔案的主檔名必須與類別名稱相同,也就是檔案要取名為「Car.java」,這是Java的規定,在一個檔案中可以定義多個類別,但只能有一個類別被設定為 「public」,且檔案名稱主檔名必須與這個 public 的類別相同名稱。

類別成員

物件裡主要會有兩個種成員:

  1. 資料(Field)。
  2. 方法(Method)。

語法為:

public class Car {
    存取修飾 資料型態 資料成員名稱;
    ...
    
    存取修飾 回傳型態 方法成員名稱(參數列) {
        陳述句;
        ...
        
        return 回傳值;
    }
}

資料成員

在定義資料成員時可以指定初值,如果沒有指定初值,則會有預設值,資料成員如果是基本型態,則預設值與陣列介紹時的預設直一樣,如果是物件型態,則預設值為 null,也就是不參考任何的物件。

存取權限有「public」、「protected」和「private」三種關鍵字:

  1. 「public」關鍵字表示所定義的成員可以使用宣告的物件名稱加上點「.」運算子來直接呼叫,稱為「公用成員」或「公開成員」,所有外部的物件都可以存取該成員。
  2. 「private」這個關鍵字用來定義一個「私用成員」,私用成員不可以透過參考名稱加上點「.」直接呼叫,稱為「私有成員」,私有成員只有在類別內部才能存取。

例如:

public class Car {
    public int price;
    protected String modelName;
    private float gas;
}

一個類別中的資料成員,若宣告為 "private",則其可視範圍(Scope)為整個類別內部,由於外界無法直接存取私用成員,所以您要使用兩個公開方法 getAccountNumber() 與 getBalance() 分別傳回其這兩個成員的值。

方法成員

一個方法被宣告為「public」,表示該方法可以藉由物件的參考名稱加上「.」被直接呼叫,一個方法成員為一小個程式片段,方法可重複被呼叫使用,並可傳入參數或回傳執行結果。

參數列用來傳入方法成員執行時所需的資料,如果傳入的參數是基本資料型態(Primitive data type),則該資料為原本資料的複製,如果傳入的是物件,則該參數存放的原物件的參考

方法區塊內宣告的變數(Variable)在方法區塊執行結束後就會被自動清除,如果方法中宣告的變數名稱與類別資料成員的名稱同名,則方法中的變數名稱會暫時覆蓋資料成員的作用範圍;參數列上的參數名稱也會覆蓋資料成員的作用範圍,如果要在方法區塊中操作資料成員,可以透過「this」關鍵字來指定。

補充

  1. 在定義類別時,有一個基本原則是:將資料最小化公開,並盡量以方法來操作資料,也就是說不開放外面直接存取物件內部的資料成員(也就是 Field 成員),這個原則是基於安全性的考量,避免程式設計人員隨意操作內部資料成員而造成臭蟲。
  2. 如果在宣告成員時不使用存取修飾詞,則預設將以「套件」(package)為存取範圍,也就是在package外就無法存取。
public class Car {
    public int price;
    protected String modelName;
    private float gas;
    
    public void setModelName(String modelName) {
        this.modelName = modelName;
    }
    
    public String getModelName() {
        return modelName;
    }
    
    private void setPrice(int price) {
        this.price = price;
    }
}


方法的傳回值可以將計算的結果或其它想要的數值、物件傳回,傳回值與傳回值型態的宣告必須一致,在方法中如果執行到 "return" 陳述,則會立即終止區塊的執行;如果方法執行結束後不需要傳回值,則可以撰寫 "void",且無需使用 "return" 關鍵字。

在物件導向程式設計的過程中,有一個基本的原則,如果資料成員能不公開就不公開,在 Java 中若不想公開成員的資訊,方式就是宣告成員為 "private",這是「資訊的最小化」,此時在程式中要存取 "private" 成員,就要經由 setXXX() 與 getXXX() 等公開方法來進行設定或存取,而不是直接存取資料成員。

透過公開方法存取私用成員的好處之一是,如果存取私用成員的流程有所更動,只要在公開方法中修改就可以了,對於呼叫方法的應用程式不受影響,例如您的 Car 類別中,gas() 如果為 50已經滿的時後,再呼叫addGas()就會提醒油已經滿了,無法繼續加油:

final static int MAX_GAS = 50;

public bool addGas(int gas) {
    if(this.gas >= MAX_GAS) {
        return false;
    } else if(this.gas + gas >= MAX_GAS)
        this.gas = GAS_MAX;
    } else {
        this.gas += gas;
    }
    
    return true;
}

這麼一來,setGas() 對gas做了些檢查,但對於addGas()呼叫者來說,則不用做修改,也不用理會細節。

提示

  1. 習慣上,方法名稱的命名慣例為第一個字母小寫,名稱以了解該方法的作用為原則,之後每個單字的第一個字母為大寫,例如showMeTheMoney() 。

  2. 為資料成員設定 setXXX() 或 getXXX() 存取方法時,XXX 名稱最好與資料成員名稱相對應,例如命名modelName這個資料成員對應的方法時,可以命名為 setModelName() 與 getModelName(),這樣可以在閱讀程式碼上更清楚其作用。

補充

參數與引數的差別:

在定義方法時,可以定義「參數列」:

public void setXXX(int something) { // something為參數
    // ...
}

呼叫方法時傳遞的數值或物件稱為「引數」:

someObject.setXXX(99); // 99是引數

物件

定義好類別之後,就可根據這個類別來建立物件,也就是產生類別的實體,建立物件為透過使用「new」關鍵字來達成。

Car car1 = new Car(); 
Car car2 = new Car();

在上面的程式中宣告了car1與car2兩個Car型態的參考名稱,並讓它們分別參考至物件。

要透過公開成員來操作物件或取得物件資訊的話,可以在物件名稱後加上「.」運算子來進行,例如:

car1.price; 
car2.setModelName("BMW M3");

建構方法

與類別名稱同名的方法稱之為「建構方法」(Constructor),也有人稱之為「建構子」,它沒有傳回值,建構方法的作用是在建構物件的同時,可以初始做一些必要的初始化,建構方法可以有多個,也就是可以被「重載」(Overload),用來滿足物件建立時的各種不同初始化需求,例如:

class Car {
    public int price;
    protected String modelName;
    private float gas;
    
    public Car() {
        price = -1;
        modelName = "未知的型號";
        gas = 0;
    }
    
    public void setModelName(String modelName) {
        this.modelName = modelName;
    }
    
    public String getModelName() {
        return modelName;
    }
    
    private void setPrice(int price) {
        this.price = price;
    }
}

解說

在這裡有兩個建構方法,一個是:

public Car() {
    price = -1;
    modelName = "未知的型號";
    gas = 0;
}

用來將物件成員price初始化為-1,modelName初始化為「未知的型號」,gas初始化為0,而這個方法不需要呼叫,也無法直接呼叫,它會在new出該物件的同時自動被呼叫,也是物件第一個被執行的方法。

this

this關鍵字用來明確指定物件本身的成員,在每一個方法成員內都會隱含該名稱;例如有時方法成員內可能會出現跟資料成員同名的變數或參數,這時如果要操作資料成員,就必須使用this來明確指定,例如:

class Car {
    public int price;
    protected String modelName;
    private float gas;
    
    public Car(int price, String modelName, int gas) {
        this.price = price;
        this.modelName = modelName;
        this.gas = gas;
    }
    
    public void setModelName(String modelName) {
        this.modelName = modelName;
    }
    
    public String getModelName() {
        return modelName;
    }
    
    private void setPrice(int price) {
        this.price = price;
    }
}

解說

在建構方法Car內有三個參數:price、modelName和gas都剛好跟資料成員名稱重複,所以當要使用時,必須明確指定才能正確操作,例如:this.price = price代表將參數price的值指派的資料成員price。

而在方法成員getModelName()內因為沒有同名的參數或變數,所以即使沒有使用this關鍵字,也會代表資料成員的modelName。

補充

當變數名稱重複時,程式執行會有內向外找有沒有符合該變數名稱的宣告,例如:區域變數 -> 資料成員,如果找完一輪都沒有符合名稱的變數或成員,就會出現編譯錯誤。

靜態 - static

同一個類別每次透過new所產生的物件,其資料成員都是各自獨立且不會互相影響的,在某些時候,會需要所有物件可以擁有共享的資料成員,這時就可以透過static關鍵字來宣告一個資料成員,A物件如果修改了該static資料成員,B物件的該static成員內容也會跟著改變,因其實他們所擁有的參考都是指向同一筆資料:

被宣告為static的資料成員,稱為「靜態資料成員」,靜態成員是屬於類別所擁有,而不是個別的物件。要宣告靜態資料成員,只要在宣告資料成員時加上static關鍵字即可,例如:

class Car { 
    public static String brand = "BMW"; // 宣告static資料成員
    ...
}

靜態成員屬於類別所擁有,可以不用建立物件直接使用類別名稱加上點「.」運算子就可以存取靜態資料成員,不過靜態資料成員仍然會受到「public」、「protected」與「private」關鍵字的存取權限來規範,例如:

class Car { 
    public static String brand = "BMW"; // 宣告static資料成員
    private static String engine = "V12"; // 私有靜態資料成員
}

public class App {
    public static void main(String[] args) {
        System.out.println(Car.brand);
        System.out.println(Car.engine); // 錯誤,無法存取靜態資料成員
    }
}

補充

雖然也可以在宣告物件之後,透過物件名稱加上點「.」運算子來存取靜態資料成員,但該方式較不建議,通常建議使用類別名稱加上點「.」運算子來存取,可以在閱讀程式碼時比較清楚知道該成員是靜態或非靜態成員,例如下面存取靜態成員方式不建議使用:

Car myCar = new Car();
System.out.println(myCar.brand);

除了靜態資料成員方法成員也可以宣告為靜態,一樣使用static關鍵字,該方法成員稱為「靜態方法」,被宣告為靜態的方法通常用來作為「工具方法」,不需要再建立物件的情況下就可以直接透過類別名稱來呼叫:

class Car { 
    private static String brand = "BMW";
    
    public static void showBrand() {
         System.out.println(brand);
    }
}

public class App {
    public static void main(String[] args) {
        Car.showBrand();
    }
}

與靜態資料成員一樣,可以直接透過類別名稱使用點「.」運算子來呼叫static方法,相同的也有public、private和protected的權限問題,例如:

class Car { 
    private static String brand = "BMW";
    private static String color = "白色";
    
    public static void showBrand() {
         System.out.println(brand);
    }

    private static void showColor() {
        System.out.println(color);
    }
}

public class App {
    public static void main(String[] args) {
        Car.showBrand();
        Car.showColor(); // 錯誤:The method showColor() from the type Car is not visible
    }
}

補充

靜態資料與靜態方法的作用通常是為了提供共享的資料或工具方法,例如將數學常用的Math 類別:Math.random(),或是Integer.parseInt()其實就是靜態方法。

靜態方法無法存取物件成員,沒有this參考名稱可以用

由於靜態成員是屬於類別而不是物件,所以在呼叫靜態方法時,並不會傳入物件的參考,所以靜態方法中不會有 this 參考名稱,由於沒有 this 名稱,所以無法存取到物件的資料成員和方法成員。如果在靜態方法中使用非靜態資料成員,在編譯時就會出現下面錯誤訊息:

non-static variable test cannot be referenced from a static context

或者是在靜態方法中呼叫非靜態方法,編譯時會出現下面錯誤訊息:

non-static method showHello() cannot be referenced from a static context

也就是說規則為:

  1. 靜態方法只能呼叫靜態方法。
  2. 靜態方法只能操作靜態資料。
  3. 物件方法可以呼叫物件方法和靜態方法。
  4. 物件方法可以操作物件資料和靜態資料。
static區塊

在類別內可以定義一種叫static的區塊,區塊內可以有陳述句,用來做一些類別相關的初始化,語法為:

public class 類別名稱 { 
    static { 
        // 一些初始化程式碼 
    } 
    .... 
}

在類別被載入時,預設會先執行靜態區塊中的程式碼,而且整個程式的生命週期只會執行一次,例如:

class Car { 
    public Car() {
        System.out.println("Car()");
    }
}

public class App {
    static {
        System.out.println("static block");
    }
    
    public App() {
        System.out.println("app()");
    }

    public static void main(String[] args) {
        new App();
        new App();
    }
}

執行結果為:

static block
app()
app()

會發現即使建立了多個App實例,static區塊只有被執行一次,且會比建構方法還要早被執行

靜態區塊、非靜態區塊、資料成員初始化和建構方法呼叫順序

看下面例子:

class Car { 
    public Car() {
        System.out.println("Car()建構方法");
    }
}

public class App {
    static {
        System.out.println("static block");
    }
    {
        System.out.println("non-static block");
    }
    public App() {
        System.out.println("app()建構方法");
    }

    Car car = new Car();

    public static void main(String[] args) {
        new App();
        new App();
    }
}

執行結果為:

static block
non-static block
Car()建構方法
app()建構方法
non-static block
Car()建構方法
app()建構方法

當靜態區塊、非靜態區塊、資料成員初始化和建構方法同時存在時,其執行順序為:

  1. 靜態區塊
  2. 非靜態區塊
  3. 資料成員初始化
  4. 建構方法

方法多載 - Overload

方法「重載」(Overload),也可稱為「超載」或「過載」,其主要是讓類別提供多個相同名稱,但是有不同參數的方法,可以提供在不同情況下使用同一種方法的機制,例如String類別的 valueOf()方法提供了多個版本:

static String valueOf(boolean b)
static String valueOf(char c)
static String valueOf(char[] data)
static String valueOf(char[] data, int offset, int count)
static String valueOf(double d)
static String valueOf(float f)
static String valueOf(int i)
static String valueOf(long l)
static String valueOf(Object obj)

雖然呼叫的方法名稱都是 valueOf(),但是根據所傳遞的引數資料型態不同,就會呼叫對應版本的方法來進行對應的動作,例如:

  1. String.valueOf(99)會呼叫valueOf(int i)的版本。
  2. String.valueOf(1.2),因為1.2是double型態,所以會呼叫的是valueOf(double d)的版本。

除了型態的不同之外,,參數列的參數個數也可以用來作為方法重載,例如:

public class MyClass {
    public void method() {
        // ...
    }
    public void method(int i) {
        // ...
    }
    public void method(int i, float f) {
        // ...
    }
    public void method(int i, int j) {
        // ...
    }
}

注意:回傳值型態即使不相同,也無法作為方法重載的根據,例如:

public class MyClass {
    public int method(int i) {
        // ...
        return 0;
    }
    public float method(int i) {
        // ...
        return 0.0F;
    }
}

上面範例會出現編譯錯誤,因為對編譯器來說,這兩個是一樣的方法,錯誤訊息為:

Duplicate method method(int) in type App

boxing問題

public class App {
    public static void main(String[] args) {
        someMethod(1);
    }
 
    public static void method(int i) {
        System.out.println("int版本被呼叫");
    }
 
    public static void method(Integer integer) {
        System.out.println("Integer版本被呼叫");
    }
}

結果為:

int 版本被呼叫

在這個例子下,裝箱(boxing)的動作並不會發生,如果想要呼叫Integer版本的方法,需要明確指定,例如:

method(new Integer(1));

不定長度參數

在呼叫某個方法時,如果需要的引數數量無法固定,例如 System.out.printf()方法中並沒有辦法事先知道引數長度,例如:

System.out.printf("%d", 10);
System.out.printf("%d, %d, %d", 10, 20, 30);
System.out.printf("%d, %d, %d, %d, %d", 10, 20, 30, 40, 50);

要使用不定長度引數,在宣告參數列時要於型態後面加上「」三個點,然後該參數在使用時,實際上會是一個陣列,可以以陣列的方式來操作它,例如實際上編譯器會將參數列的 (String args) 解釋為 (String[] args),這樣就可以只訂一一個方法來接收各種不同的長度的引數給方法使用,例如:

class MyTool {
    public static int sum(int... nums) { // 使用...宣告參數
        int sum = 0;
        for(int num : nums) {
            sum += num;
        }
        return sum;
    }
}

public class App {
    public static void main(String[] args) {
        int total = 0;
 
        total = MyTool.sum(1, 2);
        System.out.println("1 + 2 = " + total);
 
        total = MyTool.sum(1, 2, 3);
        System.out.println("1 + 2 + 3 = " + total);
 
        total = MyTool.sum(1, 2, 3, 4, 5);
        System.out.println("1 + 2 + 3 + 4 + 5 = " + total);
    }
}

執行結果:

1 + 2 = 3
1 + 2 + 3 = 6
1 + 2 + 3 + 4 + 5 = 15

補充

編譯器會將不定長度引數轉成陣列,又稱為:「編譯糖」(Compiler Sugar)。

當使用不定長度參數時,如果還有其它固定參數,則不定長度參數必須放在最後一個,例如:

public void method(int arg1, int arg2, int... args) {
     // ....
}

下面為不合法的定義方式:

public void method(int... varargs, int arg1, int arg2) {
     // ....
}

不定長度參數只能有一個,例如下面方式不合法:

public void ,ethod(int... args1, int... args2) {
     // ....
}

補充

編譯器在處理重載方法、裝箱和不定長度引數,會依下面的順序來尋找符合的方法:

  • 未裝箱、符合引數個數與型態的方法。
  • 裝箱、符合引數個數與型態的方法。
  • 嘗試「不定長度引數」可以符合的方法。
  • 找不到合適的方法,編譯錯誤。

垃圾回收

使用new配置的物件,基本上當必須清除以回收物件所佔據的記憶體空間,Java語言有提供垃圾收集機制,在「適當」的時候,執行環境會自動檢查物件,如果有未被參考的物件,就會被清除並回收物件所佔據的記憶體空間。

Java中垃圾收集的時機何時開始無法知道,可能在記憶體資源不足的時候、可能在程式執行的空閒時候,也可以主動透過程式建議執行環境進行垃圾收集,但也只是建議,垃圾回收機制並不一定會馬上進行。

在物件中有個finalize() 這個方法,它被宣告為 "protected",finalize() 會在物件被回收時執行,但不可以當成一般物件結束時打算執行某些程式碼的解構方法來使用,因為資源回收的時間是不可控制的,所以並不是物件一不使用就會被呼叫,但仍然可以使用 finalize() 來進行不需要及時的一些相關資源的清除動作。

如果確定不再使用某個物件,可以直接將該物件參考設定為「null」,例如:car = null,表示這個名稱不再參考至任何物件,不被任何名稱參考的物件就會被回收資源,透過呼叫System.gc()方法就可以建議垃圾回收進行,如果建議被採納,則回收機制就會被觸發,回收前會呼叫物件的finalize() 方法,例如:

class GCTest { 
    private String name; 
 
    public GCTest(String name) { 
        this.name = name; 
        System.out.println(name + "建立"); 
    } 
 
    // 物件回收前執行
    protected void finalize() { 
        System.out.println(name + "被回收"); 
    } 
}

public class App { 
    public static void main(String[] args) { 
        GCTest obj1 = new GCTest("object1"); 
        GCTest obj2 = new GCTest("object2"); 
        GCTest obj3 = new GCTest("object3"); 
 
        // 名稱不參考至物件 
        obj1 = null; 
        obj2 = null; 
        obj3 = null; 
 
        // 建議回收物件 
        System.gc(); 
    } 
}

執行結果:

object1建立
object2建立
object3建立
object1被回收
object3被回收
object2被回收

可以看到finalize()方法被垃圾回收機制自動呼叫了。

類別-繼承

類別可以基於某個類別(該類別可以稱之為父類別)對來加以擴充,擴充後誕生的新的類別稱為子類別,子類別可以繼承父類別原來的某些功能,並增加原來的父類別所沒有的功能,或者是將父類別原有的功能重新定義。事實上,在 Java 中,所有的類別是繼承自java.lang.Object類別。

擴充 - extends

有時候,想要擴充原始類別已經定義好的功能,但不想去修改原始類別的程式碼,或是根本沒有原始類別的原始碼,可以使用extends關鍵字來保留原始類別以機定義好的功能,然後再加上新功能來成為一個新的類別,此時原始類別為新類別的父類別,相對來說,新的類別則是原始類別的子類別。

而extends類別這件事,在物件導向裡就是所謂的「繼承」(Inherit)。

以車子來說,不同的車子可能會有其他類型車子沒有的功能,但他們都可以前進,例如:

class Car {
    private String brand;
    
    public Car() {
    }

    public void move(int miles) {
        System.out.println("Move " + miles + " miles.");
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }
    
    private void show() {
        System.out.println("This is a private method.")
    }
}

class SportCar extends Car {
    private boolean isTurboMode = false;
    
    public void turbo() {
        this.isTurboMode = !this.isTurboMode;
        System.out.println("Turbo mode: " + this.isTurboMode);
    }
}

public class App {
    public static void main(String[] args) {
        SportCar sportCar = new SportCar();
        
        sportCar.move(10);
        sportCar.turbo();
        sportCar.turbo();
    }
}

解說

SportCar繼承自Car,所以雖然沒有定義move()方法,但其實在類別中有定義了,所以也可以呼叫該方法,當擴充某個類別時,該類別的所有public成員都可以在衍生類別中被呼叫使用,而private成員則不可以直接在衍生類別中被呼叫使用(例如show()方法)。

super() - 指定建構式

class Car {
    private String brand;
    
    public Car() {
        System.out.println("建構方法一號");
        this.brand = "無品牌";
    }

    public Car(String brand) {
        System.out.println("建構方法2號");
        this.brand = brand;
    }

    public void move(int miles) {
        System.out.println("Move " + miles + " miles.");
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }
}

class SportCar extends Car {
    private boolean isTurboMode = false;

    public SportCar() {
        super("無品牌跑車");
    }
    
    public void turbo() {
        this.isTurboMode = !this.isTurboMode;
        System.out.println("Turbo mode: " + this.isTurboMode);
    }
}

public class App {
    public static void main(String[] args) {
        SportCar sportCar = new SportCar();
        
        sportCar.move(10);
        sportCar.turbo();
        sportCar.turbo();
    }
}

執行結果:

建構方法2號
Move 10 miles.
Turbo mode: true
Turbo mode: false

在擴充某個類別之後,子類別建構方法被呼叫前會先呼叫類別的建構方法,預設會呼叫無參數的父類別建構方法,但透過super()方法,透過參數,可以指定要呼叫建構方法版本,且super() 必須在建構方法一開始就呼叫

父類別的public成員可以直接在衍生類別中使用,而private成員則不行,private成員只限於定義它的類別之內來存取,如果真的想存取父類別的private成員,只能透過父類別中繼承下來的public方法成員內去間接呼叫,例如夠過setBrand()方法去存取private的brand成員。

受保護的 - protected

資料成員如果設為private成員,也就是私用成員,就只能在物件內部使用,不能直接透過參考名稱加上點「.」運算子來使用,即使是擴充了該類別的衍生類別也無法直接使用父類別的私有成員,只能透過父類別提供的「public」方法成員來呼叫或設定私用成員。

但有些時候,會希望子類別也可以取用父類別的的成員,但又不想要公開給其它套件使用,這時就可以使用protected(受保護的)關鍵字來修飾成員,保護的意思表示存取該成員是有條件限制的,當類別的成員宣告為受保護的成員之後,繼承的類別就可以直接使用這些成員,但這些成員受到保護,所以如果是不同套件的(package)的類別就無法使用。
demo/Shape.java

public class Shape { 
    // 受保護的成員
    protected int x;
    protected int y;
    protected int width;
    protected int height; 

    public Shape() { 
    } 

    public Shape(int x, int y, int width, int height) { 
        this.x = x;  
        this.y = y; 
        this.width = width;   
        this.height = height; 
    } 

    public void setX(int x) { this.x = x; } 
    public void setY(int y) { this.y = y; } 
    public void setWidth(int width) { this.width = width; } 
    public void setHeight(int height) { this.height = height; } 

    public int getX() { return x; } 
    public int getY() { return y; } 
    public int getWidth() { return width; } 
    public int getHeight() { return height; } 
}

class Rectangle extends Shape { 
    public Rectangle() {
        super();
    } 

    public Rectangle(int x, int y, int z, int width, int height) { 
        super(x, y, width, height); 
    } 

    public int getPositionX() { return x; }
    public int getPositionY() { return y; }  
}

App.java

public class App {
    public static main(String[] args) {
        Shape shape = new Shape();
        shape.x = 0; // 錯誤,無法存取pretected成員
        shape.y = 0; // 錯誤,無法存取pretected成員
    }
}

Shape類別的x、y、width、height成員因為被宣告成protected,所以在子類別Rectangle類別內可以直接使用,但是在不同套件的App類別內操作x和y成員將會出現編譯錯誤:

The field Shape.x is not visible

同理,方法成員也可以宣告為受保護的成員。父類別中想要讓子類別擁有的資料成員會宣告為protected,父類別中想要子類別也可以使用的方法成員也會宣告為protected,這些方法對不同套件(package)的類別來說,可能是呼叫它並沒有意義或是有某些風險,所以就會宣告成protected。

重載 - Override

類別是物件的定義,如果父類別中的定義不符合需求,需要做調整個話,可以在擴充類別的同時重新定義,可以重新定義:

  • 方法的實作內容
  • 成員的存取權限
  • 成員的返回值型態

例如:

public class Car {
    protected int gas;

    public Car() {
    }
    
    public void setGas(int gas) {
        this.gas = gas;
    }
    ....
} 

Car類別有一個可以用來設定汽油量的方法setGas(),但setGas()方法不夠安全,因為汽油量不可能是負的,所以做了一個汽油量的檢查,如果汽油量小於0,就不會進行設定:

public class SportCar extends Car {
    public SportCar() {
    }
    // 重新定義setGas()
    public void setGas(int gas) {
        if(gas < 0)
            return;
            
        this.gas = gas;
    }
    ....
}

這麼一來,以 SportCar 類別的定義所產生的物件,就可以使用新的定義方法,就 SportCar 類別來說,由於操作介面與 Car是一致的,所以可以這麼使用:

Car car = new SportCar();
car.setGas();

SportCar 與 Car 擁有一致的操作介面,因為 SportCar 是 Car 型態的子類,擁有從父類中繼承下來的 setGas() 操作介面,雖然使用了Car型態的介面來操作 SportCar實例,但由於實際運作的物件是SafeArray的實例,所以被呼叫執行的會是 SafeArray中重新定義過的 setGas() 方法,這個就叫「多型」(Polymorphism)。

如果想在衍生類別中呼叫基底類別的建構方法,可以使用 super() 方法;如果要在衍生類別中呼叫父類別方法,則可以使用super.methodName()的方式來達成,但使用 super() 呼叫父類別建構方法或使用 super.methodName() 呼叫父類別中方法一樣需要遵從權限設定,也就是該方法不能是private(私有成員)。

注意

重新定義方法可以加大父類別中的方法權限,但不可以縮小父類別的方法權限,例如:原來成員是public的話,不可以在父類別中重新定義它為private或protected,所以在擴充 Car 時,就不可以這樣:

public class SportCar extends Car {
    public SportCar() {
    }
    // 重新定義setGas()
    protected void setGas(int gas) {
        if(gas < 0)
            return;
            
        this.gas = gas;
    }
    ....
}

嘗試將 setGas() 方法從public權限縮小成private會在編譯時得到下面錯誤訊息:

setGas(int) in SportCar cannot override setGas(int) in Car; attempting to assign weaker accessprivileges; was public
private void setGas(int i) {
^
1 error

也可以重新定義返回值的型態,其條件就是,原本的回傳值必須是原本回傳值的子類別,例如有個父類別為:

public class Car {
    protected int gas;

    public Car() {
    }
    
    public void setGas(int gas) {
        this.gas = gas;
    }
    
    public Car getClone() {
        return new Car();
    }
    ....
} 

getClone() 方法原本回傳的會是Car物件,現在打算Car類別衍生了一個SportCar類別,然後重新定了getClone()方法,並且回傳值改為SportCar物件,由於回傳值是原本型態的子類別,有繼承關係,所以可以這樣重新定義回傳值:

public class SportCar extends Car {
    public SportCar() {
    }
    // 重新定義setGas()
    protected void setGas(int gas) {
        if(gas < 0)
            return;
            
        this.gas = gas;
    }
    
    public SportCar getClone() {
        return new SportCar();
    }
    ....
}

注意

static 方法無法被重新定義,一個方法要被重新定義,必須是非 static 的,如果在子類別中定義一個相同名稱和參數的 static成員,那其實不是重新定義,而是定義一個屬於該子類別的static成員,兩者互不相干。

Object類別

在 Java 中只要使用class關鍵字定義類別,就已經開始使用繼承的機制了,因為在 Java 中所有的物件都是擴充自 java.lang.Object 類別,Object 類別是Java程式中所有類別的父類別,每個類別都直接或間接繼承自Object 類別,例如當定義一個類別時:

public class Foo { 
    // 實作 
} 

在Java中定義類別時如果沒有指定要繼承的類別,會自動繼承自Object類別,上面的類別定義其實是下面的樣子:

public class Foo extends Object { 
    // 實作 
} 

由於Object 類別是 Java中所有類別的父類別,所以使用Object宣告的名稱,可以參考至任何的物件而不會發生任何錯誤,因為每一個物件都是Object的子物件,例如在使用容器時,必須設定容器可以存放的類別物件:

import java.util.ArrayList;

class Car {
    public Car() { }
}

public class App {
    public static void main(String[] args) {
        ArrayList<String> stringArray = new ArrayList<>();

        stringArray.add(new String("Hello"));
        stringArray.add(new Car());
    }
}

在這個範例中,因為stringArray被設定成只能存放String物件的容器,所以當stringArray.add(new Car())要放入一個Car物件時,因為型態的問題,將會出現下面錯誤:

The method add(String) in the type ArrayList<String> is not applicable for the arguments (Car)

但由於Java中所有的類別都會直接或間接繼承自Object類別,所以只要修改ArrayList可以接受的型態如下:

import java.util.ArrayList;

class Car {
    public Car() { }
}

public class App {
    public static void main(String[] args) {
        ArrayList<Object> stringArray = new ArrayList<>();

        stringArray.add(new String("Hello"));
        stringArray.add(new Car());
    }
}

這樣就可以正常編譯並執行了。

但如果從ArrayList中取出後要使用,就必須做一個轉型,恢復成原本的型態才可以正確呼叫到方法成員和操作資料成員,例如:

import java.util.ArrayList;

class Car {
    public Car() { }
    public void show() {
        System.out.println("Show Car");
    }
}

public class App {
    public static void main(String[] args) {
        ArrayList<Object> stringArray = new ArrayList<>();

        stringArray.add(new String("Hello"));
        stringArray.add(new Car());
        
        Car car = (Car)stringArray.get(1);
        car.show();
    }
}

在程式中,ArrayList 物件可以加入任何型態的物件至其中,因為所有的物件都是 Object 的子物件,從stringArray指定索引取回物件時,要將物件的類型從 Object 轉換為原來的類型,如此才能操作物件上的方法。

補充

Object 類別定義了幾個方法,包括:

  • protected" 權限的 clone()、finalize()。
  • public" 權限的 equals()、toString()、hashCode()、notify()、notifyAll()、wait()、getClass() 等方法。

除了getClass()、notify()、notifyAll()、wait()等方法(因為被宣告為final,所以無法重新定義)之外,其它方法都可以在繼承之後加以重新定義。

toString()方法

Object 的 toString() 方法是對物件的文字描述,它會返回 String 實例,在定義物件之後可以重新定義toString() 方法,來為自己的物件提供特定的文字描述;預設toString()會傳回類別名稱及 16 進位制的編碼,也就是傳回以下的字串:

getClass().getName() + '@' + Integer.toHexString(hashCode())

getClass()方法會傳回物件於執行時期的 Class 實例, 再使用Class實例的 getName() 方法可以取得類別名稱;hashCode() 傳回該物件的「雜湊碼」(Hash code);重新定義自己的toString()方法,可以讓物件用更符合的字串來表示自己的物件,例如:

class Car {
    public Car() {}
    public String toString() {
        return "This is a Car";
    }
} 

public class App {
    public static void main(String[] args) {
        Car car = new Car();
        
        System.out.println(car);
    }
}

執行結果:

This is a Car

在這例子中,雖然沒有明確呼叫toString()方法來取得代表該物件的字串,但是預設上如果直接把整個物件丟給標準輸出,會自動呼叫toString()方法。

final關鍵字

final關鍵字可以使用在變數宣告時,表示該變數一旦設定之後,就不可以再改變該變數的值,例如下面的PI變數被設定成了final,所以不能再有指定值給 PI 的動作:

final double PI = 3.14159;

如果在定義方法成員時使用final,表示該方法成員在無法被子類別重新定義(Override),例如:

public class Car { 
    protected int gas = 0;
    
    public final void setGas(int gas) { 
        this.gas = gas;
    } 
    
    .... 
} 

在繼承Car類別後,由於setGas() 方法被宣告為final,所以在子類別中setGas()方法不能再被重新定義。

如果在宣告類別時加上final關鍵字,則表示該類別無法再被擴充,也就是這個類別不可以被其它類別繼承,例如:

public final class Car {
    // .... 
}

類別-多型

多型操作指的是使用同一個操作介面,來操作不同的物件實例;多型操作在物件導向上是為了降低對操作介面的依賴程度,進而增加程式架構的彈性與可維護性。

多型操作是物件導向上一個重要的特性,而代表多行的兩個技術則為:

  • 「抽象類別」(Abstract)
  • 「介面」(Interface)

在類別上定義的公開方法也稱為操作介面,透過這些介面,可以對物件加以操作,如果使用不正確的類別型態來轉換物件的操作介面,會發生 java.lang.ClassCastException 例外,例如:

Class1 c1 = new Class1();
Class2 c2 = (Class2) c1; // 會發生ClassCastException例外

如果Class1上定義了method1() 方法,而 Class2 上也定義了method1() 方法,然後定義了兩個run() 方法來分別操作 Class1 與 Class2 的method1()方法:

public void run(Class1 c1) {
    c1.method1();
}
public void run(Class2 c2) {
    c2.method1();
}

但如果透過繼承的方式,定義一個父類別 ParentClass 類別,當中定義有method1(),並讓 Class1 與 Class2 都繼承 ParentClass 類別並重新定義自己的method1() 方法,這樣一來就可以將程式中的run() 改成:

public void run(ParentClass c) {
    c.method1();
}

因為Class1 與 Class2 是 ParentClass 的子類別,所以可以透過 ParentClass 來操作這兩個類別。

抽象-abstract

在定義類別時,可以僅宣告方法名稱而不實作當中的邏輯,也就是該方法並沒有程式碼區塊,這樣的方法稱之為「抽象方法」(Abstract method),如果一個方法中包括了抽象方法,則該類別稱之為「抽象類別」(Abstract class),抽象類別是擁有未實作方法的類別,所以無法生成物件,只能被繼承擴充,並於繼承後實作未完成的抽象方法,要宣告抽象方法與抽象類別,要使用abstract關鍵字,例如:

public class SportCar {
    private int gas = 0;
    public void setGas(int gas) { this.gas = gas; }
    public void showMe() {
        System.out.printf("我是一輛跑車");
    }
}

public class Minivan {
    private int gas = 0;
    public void setGas(int gas) { this.gas = gas; }
    public void showMe() {
        System.out.printf("我是一輛小貨車");
    }
}

在上面這兩個不同類型車子的類別除了showMe()方法實作不一樣之外,其它的定義是一樣的,而且這兩個類別所定義的顯然都是「汽車」的一種類型,可以定義一個抽象的 AbstractCar 類別,將SportCar與Minivan中相同的行為與定義提取(Pull up)至抽象類別中,例如:

public abstract class AbstractCar {
    protected int gas = 0;
    public void setGas(int gas) { this.gas = gas; }
    
    public abstract void showMe();
}

注意到在類別宣告上使用了abstract" 關鍵字,所以 AbstractCar是個抽象類別,它只能被繼承,而 showMe() 方法也使用了abstract來修飾,表示是ㄧ個抽象方法,目前還不用實作這個方法,繼承了 AbstractCar的類別必須實作showMe() 方法,例如:

public class SportCar extends AbstractCar {
    public void showMe() {
        System.out.printf("我是一輛跑車");
    }
}

public class Minivan extends AbstractCar {
    public void showMe() {
        System.out.printf("我是一輛小貨車");
    }
}

相同的成員定義因為被提取到AbstractCar類別中了,擴充時便會一併繼承下來,所以在SportCar與Minivan中不v需要再定義,只要個別定義自己的showMe()方法即可,由於SportCar和Minivan都是AbstractCar的子類別,所以可以直接使用AbstractCircle上定義好的操作介面來操作子類別物件的方法,例如:

public class App {
    public static void main(String[] args) {
        AbstractCar car1 = new SportCar();
        AbstractCar car2 = new Minivan();
        
        car1.showMe();
        car2.showMe();
    }
}

執行結果:

我是一輛跑車
我是一輛小貨車

由於 AbstractCar上有定義showMe() 方法,所以可用來操作子類別物件上的方法,由於實際物件的不同,也會呼叫到對應子類別定義的showMe()方法,這就是繼承上多型操作的一個實例應用。

提示

將抽象類別的名稱加上 Abstract 作為開頭,可以表示這是個一個抽象類別,用意在提醒開發人員不要使用這個類別來產生物件(事實上也無法產生物件)。

抽象類別應用

寫一程式,可以用來輸出各種形狀到畫面上:

import java.util.ArrayList;

abstract class Shape {
    public abstract void draw();
} 

class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("*****");
        System.out.println("*****");
        System.out.println("*****");
    }
}

class Triangle extends Shape {
    @Override
    public void draw() {
        System.out.println("  *  ");
        System.out.println(" *** ");
        System.out.println("*****");
    }
}

class RTriangle extends Shape {
    @Override
    public void draw() {
        System.out.println("*****");
        System.out.println(" *** ");
        System.out.println("  *  ");
    }
}

public class App {
    public static void main(String[] args) {
        ArrayList<Shape> shapes = new ArrayList<>();

        shapes.add(new Triangle());
        shapes.add(new Rectangle());
        shapes.add(new RTriangle());

        for(Shape s : shapes) {
            s.draw();
        }
    }
}

執行結果:

  *  
 *** 
*****
*****
*****
*****
*****
 *** 
  *  

首先定義出了一個代表形狀的Shape抽象類別,有一個draw()抽象方法,代表繼承的類別都需要實作把自己畫出來的draw()方法,接下來的Rectange、Triangle和RTriangle擴充自Shape抽象類別並各自實現了自己的draw()方法,接下來,定醫了一個可以存放Shape物件的ArrayList容器,因為Shape為Rectange、Triangle和RTriangle的父類別,所以都可以被存放到shapes這個ArrayList內,最後透過Shape的draw()介面來操作每個物件的draw()方法,因而會輸出每個實例實際輸出的形狀。

另一個應用

import java.util.ArrayList;

abstract class Car {
    protected String brand = "";
    protected int gas = 0;

    public void setBrand(String brand) { this.brand = brand; }
    public String getBrand() { return this.brand; }

    public void setGas(int gas) {
        if(gas < 0) this.gas = 0;
        else this.gas = gas;
    }

    public int getGas() { return this.gas; }

    public void status() {
        System.out.println("廠牌:" + brand + ", 汽油:" + gas);
    }

    public abstract void start();
}

class SportCar extends Car {
    private boolean isTurboMode = false;

    public void turboMode(boolean enabled) {
        this.isTurboMode = enabled;
        System.out.println("Turbo模式: " + (this.isTurboMode ? "已打開" : "已關閉"));
    }

    public void status() {
        super.status();
        System.out.println("Turbo模式: " + (this.isTurboMode ? "已打開" : "已關閉"));
    }

    public void start() {
        System.out.println("指紋啟動車子");
    }
}

class SUV extends Car {
    private boolean isBackDoorOpened = false;

    public void openBackDoor() {
        this.isBackDoorOpened = true;
        System.out.println("打開後門");
    }

    public void closeBackDoor() {
        this.isBackDoorOpened = false;
        System.out.println("關閉後門");
    }

    public void status() {
        super.status();
        System.out.println("後門: " + (this.isBackDoorOpened ? "已打開" : "已關閉"));
    }

    public void start() {
        System.out.println("鑰匙啟動車子");
    }
}

public class App {
    public static void main(String[] args) {
        Car sportCar = new SportCar();
        Car suv = new SUV();

        sportCar.status();
        ((SportCar)sportCar).turboMode(true);
        sportCar.start();

        suv.status();
        ((SUV)suv).openBackDoor();
        suv.start();

        // ArrayList<Car> cars = new ArrayList<>();
        // cars.add(sportCar);
        // cars.add(suv);

        // for(Car c : cars) {
        //     c.status();
        //     c.start();
        //     c.tur
        // }
        
    }
}

由於SportCar類別和SUV類別都是繼承自Car,且都有實現Car類別的抽象方法status(),所以都可以透過Car這個「介面」來操作兩種物件的status()方法,且會正確呼叫實際對應物件的statuas()方法。

介面-interface

介面看起來有點像是完全沒有任何方法被實作的抽象類別,但實際上兩者在語義與應用上是有差別的。繼承某抽象類別的類別必定是該抽象類別的一個子類別,由於同屬一個類型,只要父類別中有定義同名方法,您就可以透過父類別型態來操作子類實例中被重新定義的方法,也就是透過父類別型態進行多型操作,但實作某介面的類別並不屬於該介面的類別,而且一個類別可以實作多個介面,但繼承只能有一個父類別。

介面的目的在定義一組可操作的方法,實作某介面的類別必須實作該介面上所定義的所有方法,只要物件有實作某個介面,就可以透過該介面來操作物件上對應的方法,而且也只能操作介面上有定義的方法,物件上期方法對於介面來說是不可見的。

介面的宣告是使用 "interface" 關鍵字,語法如下:

[public] interface 介面名稱 {
    權限設定 回傳型態 方法(參數列); 
    ...
}

在宣告介面時方法上的權限設定可以省略,省略的話,預設會是public,例如:

public interface IAction {
     public void run();
}

補充

如果有加上public,則該介面需要獨立為一個原始碼檔案。

在定義類別時,透過implements關鍵字來指定要實作的介面,介面中所有定義的方法都要實作,例如要實現上面的IAction介面:

class OKAction implements IAction {
    private String name;
 
    public OKAction(String name) {
        this.name = name;
    }
 
    public void run() {
        System.out.println("開始動作:" + name);
    }
}

class StopAction implements IAction {
    private int time;
 
    public StopAction(int time) {
        this.time = time;
    }
 
    public void run() {
        System.out.println("停止直到:" + time);
    }
}

雖然OKAction與StopAction是兩種不同的類別,但因為都實現了IAction介面,所以只要知道IAction定義了什麼方法,就可以操作OKAction和StopAction物件,而不需要知道物件到底是什麼類別的實例,例如:

import java.util.ArrayList;

public class App {
    public static void main(String[] args) {
        ArrayList<IAction> actions = new ArrayList<>();
        
        OKAction ok = new OKAction("Run");
        StopAction stop = new StopAction(99);
        
        actions.add(ok);
        actions.add(stop);
        
        for(IAction a : actions) {
            a.run();
        }
    }
}

執行結果:

開始動作:Run
停止直到:99

介面可以一次被實作多個,語法如下:

public class 類別名稱 implements 介面1, 介面2, 介面3 { 
    // 介面實作
}

當實作多個介面時,每一個介面中所定義的方法都必須實作,由於實作了多個介面,所以要操作物件時,會需要透過各自的介面來操作各自的方法,透過各自的介面操作如此程式才知道如何正確的操作物件,例如:world 實作了 ICountry 與 IPlace 兩個介面,就需要像下面一樣透過「介面的轉換」來作不同的操作:

ICountry c = (ICountry)world;
c.fight();

IPlace p = (IPlace)world;
p.fire();

每多實作一個介面,就要多遵守一個實作協議。

介面本身也可以進行繼承的動作,同樣也是使用extends關鍵字來繼承父介面,例如:

public interface 名稱 extends 介面1, 介面2 { 
    // ... 
}

與類別不同的是,類別一次只能繼承一個父類別,但一個介面可以同時繼承多個父介面,實作子介面的類別必須將所有在父介面和子介面中定義的方法實作出來。

提示

在定義介面名稱時,可以使用「I'(大寫i)」作為開頭,例如 IAction這樣的名稱,用來表示它是一個介面(Interface)。

2021-09-04

上課範例

import java.util.ArrayList;

interface IShape {
    void show();
    void draw();
}

interface IShape2 {
    void recycle();
}

// 抽象類別不能實體化
abstract class Shape {
    private int count = 0; // 資料成員

    public Shape() { // 建構式
        System.out.println("建構式");
    }

    // 方法成員
    public void show() {
        System.out.println("This is a shape: " + count);
    }

    public abstract void draw(); // 抽象方法
}

// 抽象類別不能實體化
abstract class Shape2 {
    private int count = 0; // 資料成員

    public Shape2() { // 建構式
        System.out.println("建構式");
    }

    public abstract void recycle(); // 抽象方法
}

class Rectangle extends Shape {
    public Rectangle() {
        System.out.println("Rectangle建構式");
    }

    // 複寫override父類別的方法
    @Override
    public void show() {
        super.show(); // 呼叫父類別的show()方法
        System.out.println("Rectangle.show()");
    }

    @Override
    public void draw() {
       System.out.println("Rectangle.draw()");
    }
    
}

class Triangle extends Shape {
    public Triangle() {
        System.out.println("Triangle建構式");
    }

    public void show() {
        System.out.println("Triangle.show()");
    }

    @Override
    public void draw() {
        System.out.println("Triangle.draw()");
    }
}

class RTriangle extends Shape {
    public RTriangle() {
        System.out.println("RTriangle");
    }

    public void show() {
        System.out.println("RTriangle.show()");
    }
    
    @Override
    public void draw() {
        System.out.println("RTriangle.draw()");
    }
}

class RectangeV2 implements IShape, IShape2 {

    @Override
    public void show() {
        System.out.println("RectangleV2.show()");
    }

    @Override
    public void draw() {
        System.out.println("RectangleV2.draw()");
    }

    @Override
    public void recycle() {
        System.out.println("RectangleV2.recycle()");  
    }
}

class TriangleV2 implements IShape, IShape2 {
    @Override
    public void show() {
        System.out.println("TriangleV2.show()");
    }

    @Override
    public void draw() {
        System.out.println("TriangleV2.draw()");
    }

    @Override
    public void recycle() {
        System.out.println("TriangleV2.recycle()");
    }
}

public class App {
    public static void main(String[] args) throws Exception {
        ArrayList<Shape> shapes = new ArrayList<>();
        shapes.add(new Rectangle());
        shapes.add(new Triangle());
        shapes.add(new RTriangle());
        shapes.add(new Triangle());
        shapes.add(new RTriangle());
        shapes.add(new Triangle());
        shapes.add(new RTriangle());

        for(Shape s : shapes) {
            s.show();
        }

        IShape ishape1 = new RectangeV2();
        IShape ishape2 = new TriangleV2();
        ishape1.draw();
        ishape2.draw();
        
        IShape2 iShape3 = (IShape2)ishape1;
        IShape2 iShape4 = (IShape2)ishape2;
        iShape3.recycle();
        iShape4.recycle();
    }
}