CodeSprout程式萌芽

CHAPTER 03 · JAVA 11 FOUNDATIONS

Java 物件導向

由類別建立物件,以建構子整理初始狀態,串起封裝、繼承、多型、覆寫與超載。

這一章學會
  • 建立有清楚狀態與操作的類別
  • 分辨實例、static、this 與 super
  • 追蹤覆寫、超載、轉型與多型
先預測結果,再看輸出。

每個 Java 範例都是獨立的 Main.java,可用 JDK 11 編譯執行;請一次複製一個範例。題庫使用 public class Solution 中的 public static solve(...) 方法,請保留題目指定的型別與參數,使用 return 交回答案,不必另外讀取輸入或撰寫 main。

01|類別是藍圖,物件是實例

類別把相關資料與操作放在一起:欄位記錄狀態,方法提供行為。new 建立新實例並執行建構子;變數保存該實例的參考。兩次 new 得到不同物件,實例欄位各自保存資料。

JAVA 11 · Main.java程式碼
public class Main {
    static class Counter {
        int value;
        void add(int amount) {
            value += amount;
        }
    }

    public static void main(String[] args) {
        Counter first = new Counter();
        Counter second = new Counter();
        first.add(3);
        System.out.println(first.value);
        System.out.println(second.value);
    }
}
預期輸出
3
0

教材對照:PDF 第 2–8 頁

02|建構子與 this(...)

建構子名稱與類別相同,不寫回傳型別,連 void 也不能加。只在完全沒有自行宣告建構子時,編譯器才會提供預設無參數建構子。建構子可以超載;this(...) 呼叫同類別另一個建構子,在 Java 11 必須放在第一個敘述。

JAVA 11 · Main.java程式碼
public class Main {
    static class Student {
        String name;
        Student() {
            this("新同學");
        }
        Student(String name) {
            this.name = name;
        }
    }

    public static void main(String[] args) {
        Student first = new Student();
        Student second = new Student("小芽");
        System.out.println(first.name);
        System.out.println(second.name);
    }
}
預期輸出
新同學
小芽

教材對照:PDF 第 9–12、33–37 頁

03|static 共用,實例各自保存

static 欄位由類別共用,適合保存全體共同資訊;實例欄位保存每個物件自己的狀態。static 方法沒有目前物件的 this,若要使用實例成員,需先取得明確的物件參考。呼叫靜態成員時以類別名稱表示最清楚。

JAVA 11 · Main.java程式碼
public class Main {
    static class Ticket {
        static int created;
        final int number;
        Ticket() {
            created++;
            number = created;
        }
    }

    public static void main(String[] args) {
        Ticket first = new Ticket();
        Ticket second = new Ticket();
        System.out.println(first.number);
        System.out.println(second.number);
        System.out.println(Ticket.created);
    }
}
預期輸出
1
2
2

教材對照:PDF 第 3–4、13–14 頁

04|封裝:讓方法維護合法狀態

封裝是把資料與操作整合並限制直接修改。欄位設為 private,再提供能維護規則的方法。例如帳戶只接受正數存款,外部就不能任意把餘額設成負數。getter/setter 不是必備配對,應依操作需求設計。

JAVA 11 · Main.java程式碼
public class Main {
    static class Account {
        private int balance;
        boolean deposit(int amount) {
            if (amount <= 0) {
                return false;
            }
            balance += amount;
            return true;
        }
        int getBalance() {
            return balance;
        }
    }

    public static void main(String[] args) {
        Account account = new Account();
        System.out.println(account.deposit(100));
        System.out.println(account.deposit(-20));
        System.out.println(account.getBalance());
    }
}
預期輸出
true
false
100

教材對照:PDF 第 26 頁

05|繼承、super 與建構順序

extends 表示子類別是父類別的一種;一個類別只能直接繼承一個類別,但可實作多個介面。子類別不能直接讀取父類別 private 欄位。建構子不會被繼承;子類別透過 super(...) 呼叫父類別建構子,再完成自身初始化。

JAVA 11 · Main.java程式碼
public class Main {
    static class Person {
        final String name;
        Person(String name) {
            this.name = name;
            System.out.println("Person 初始化");
        }
    }
    static class Student extends Person {
        Student(String name) {
            super(name);
            System.out.println("Student 初始化");
        }
    }

    public static void main(String[] args) {
        Student student = new Student("小芽");
        System.out.println(student.name);
    }
}
預期輸出
Person 初始化
Student 初始化
小芽

教材對照:PDF 第 27–37 頁

06|覆寫與動態多型

子類別以相同方法名稱與參數列提供新實作,稱為覆寫(override)。父類別型別的變數可以參考子類別實例;可呼叫哪些方法先由宣告型別決定,實例方法的實際實作再由執行時物件決定。加上 @Override,編譯器會幫你檢查是否真的覆寫。

  • 覆寫不能縮小存取權限;參考型別回傳值可改成更具體的子型別,稱為共變回傳。
  • 覆寫不能新增或擴大父方法未允許的 checked exception;可以少宣告或改成其子型別。
  • final 方法不能被覆寫,private 方法不會被子類別繼承;static 方法屬於隱藏,不是實例方法覆寫。
JAVA 11 · Main.java程式碼
public class Main {
    static class Animal {
        String move() { return "移動"; }
    }
    static class Cat extends Animal {
        @Override
        String move() { return "跑跳"; }
    }
    static class Bird extends Animal {
        @Override
        String move() { return "飛行"; }
    }
    static void showMove(Animal animal) {
        System.out.println(animal.move());
    }

    public static void main(String[] args) {
        Animal[] animals = {new Animal(), new Cat(), new Bird()};
        for (Animal animal : animals) {
            showMove(animal);
        }
    }
}
預期輸出
移動
跑跳
飛行

教材對照:PDF 第 15–20、38–45、50–53 頁

07|欄位遮蔽與 instanceof 轉型

欄位不採用實例方法的動態派送:同名欄位取哪一份,依存取運算式的宣告型別決定。轉型只改變看待同一物件的型別,不會把父類別實例變成子類別。向下轉型前先用 instanceof 判斷;null instanceof 某型別 為 false。

JAVA 11 · Main.java程式碼
public class Main {
    static class Animal {
        String label = "動物";
        String sound() { return "聲音"; }
    }
    static class Cat extends Animal {
        String label = "貓";
        @Override
        String sound() { return "喵"; }
    }

    public static void main(String[] args) {
        Animal animal = new Cat();
        System.out.println(animal.label);
        System.out.println(animal.sound());
        if (animal instanceof Cat) {
            Cat cat = (Cat) animal;
            System.out.println(cat.label);
        }
    }
}
預期輸出
動物
喵
貓

教材對照:PDF 第 21、44–49 頁

08|超載與 varargs

超載(overload)是相同方法名搭配不同參數型別、數量或順序;只改回傳型別不成立。呼叫哪個超載在編譯時根據參數型別選擇。varargs 用 int... values 接收不定數量參數,方法內當作陣列處理;每個方法最多一個,且要放最後。

JAVA 11 · Main.java程式碼
public class Main {
    static String describe(int value) {
        return "數字=" + value;
    }
    static String describe(String value) {
        return "文字=" + value;
    }
    static int sum(int... values) {
        int total = 0;
        for (int value : values) {
            total += value;
        }
        return total;
    }

    public static void main(String[] args) {
        System.out.println(describe(11));
        System.out.println(describe("Java"));
        System.out.println(sum(2, 4, 6));
        System.out.println(sum());
    }
}
預期輸出
數字=11
文字=Java
12
0

教材對照:PDF 第 21–25 頁

09|is-a、has-a 與組合

is-a 表示『是一種』,例如 Computer 是 Machine;has-a 表示『擁有』,例如 Computer 有 Cpu。只有確實能當成父型別使用時才設計繼承;如果只是使用另一個物件的能力,將它保存為欄位通常較清楚。

JAVA 11 · Main.java程式碼
public class Main {
    static class Machine { }
    static class Cpu {
        int cores = 4;
    }
    static class Computer extends Machine {
        final Cpu cpu = new Cpu();
    }

    public static void main(String[] args) {
        Computer computer = new Computer();
        System.out.println(computer instanceof Machine);
        System.out.println(computer.cpu.cores);
    }
}
預期輸出
true
4

教材對照:PDF 第 54–55 頁

10|import static 簡化靜態成員名稱

一般 import 引入型別名稱;import static 引入該型別可存取的靜態成員名稱,例如 Math.max 與 System.out。它不會新增功能,也不會改變存取權限。名稱衝突時保留類別名稱,讓程式容易辨識。

JAVA 11 · Main.java程式碼
import static java.lang.Math.max;
import static java.lang.System.out;

public class Main {
    public static void main(String[] args) {
        out.println(max(11, 7));
    }
}
預期輸出
11

教材對照:PDF 第 56–58 頁

11|抽象類別與介面

抽象類別用 abstract 宣告,可以保存欄位、提供已實作的方法,也可以留下沒有方法本體的抽象方法;不能直接 new 抽象類別。介面用 interface 定義可提供的能力,類別用 implements 實作。以下把共用的 amount 留在抽象父類別,再由 AddRule 完成 Rule 要求的 apply 方法。

  • 介面中沒有方法本體的 apply 隱含 public abstract,實作方法也必須是 public。
  • BaseRule 尚未實作 apply,因此必須維持 abstract;AddRule 完成實作後才能建立實例。
  • Java 11 支援介面 default 方法,能提供預設實作。類別只能 extends 一個父類別,但可以 implements 多個介面。
JAVA 11 · Main.java程式碼
public class Main {
    interface Rule {
        int apply(int value);
        default String label() { return "規則"; }
    }
    static abstract class BaseRule implements Rule {
        protected final int amount;
        BaseRule(int amount) { this.amount = amount; }
    }
    static class AddRule extends BaseRule {
        AddRule(int amount) { super(amount); }
        @Override
        public int apply(int value) { return value + amount; }
    }

    public static void main(String[] args) {
        Rule rule = new AddRule(3);
        System.out.println(rule.label());
        System.out.println(rule.apply(4));
    }
}
預期輸出
規則
7

教材對照:PDF 第 33(Java 11 介面與抽象類別延伸) 頁

WHEN SOMETHING GOES WRONG

遇到錯誤,先檢查這裡

建構子前面誤寫 void

加上 void 後會成為一般方法,new 時不會把它當成建構子執行。檢查名稱與類別完全相同,且無回傳型別。

在 static 方法直接使用 this

static 沒有目前實例。先取得物件再呼叫實例方法,或重新思考操作是否應該是實例方法。

以為轉型後一定呼叫父類別方法

轉型不會改變實際物件;被覆寫的實例方法仍由物件決定。子類別實作內要明確呼叫父方法時用 super.method()。

PAUSE AND TRY

先不要急著看別人的寫法

  1. 建立兩個帳戶,只向第一個存款,第二個餘額應該是多少?
  2. 把多型範例的 Cat 加上新方法,Animal 變數為何不能直接呼叫?
  3. 比較 void run(int n) 與 void run(String text) 屬於超載還是覆寫。

先用紙筆預測,再改動範例中的數字或文字。結果與預期不同時,找出最早開始不同的那一步。

把這幾件事帶走

  • 建構子建立合法起點,方法維護合法狀態。
  • 宣告型別決定可呼叫內容,物件決定被覆寫實例方法的實作。
  • 分清楚 is-a 繼承與 has-a 組合,避免讓關係過度複雜。

FROM READING TO PRACTICE

用三道免費題,練習本章觀念

先做簡單題確認語法,再把幾個步驟組合起來。三題都來自本章主題的 Java 免費題庫,難度表示需要組合的基本概念。

教材來源與 Java 11 對照

本章依據《第二部分 03 Java 物件導向.pdf》PDF 第 2–58 頁整理,將原有觀念重新編寫為可閱讀與執行的網站教學。頁碼為 PDF 檔案頁次;舊版語法說明與已知誤植依 Java 11 修正,補充內容另有標示。

需要查閱更完整的規則時,可參考以下官方文件。