Home
Explore Uedu
Student Console
Register as Member/Login
(2) In future presentations of the research findings, in addition to the course project website and public presentations, your real name and personal information will not appear in this research report. If you are interested in the research results, we can provide you with an executive summary after the study is completed.
問卷中心
Teacher Console
Course Setup
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

0

Local AI

--

CISOSE26 本地 AI UG26
政治大學 AQI 18 29°C

AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

類別與物件

Java 類別與物件:從一張會員卡看懂 OOP-first 的世界觀

用一個健身房會員系統,親手把類別、物件、建構子、this、封裝與 package 串起來,並從參考語意與 equals/hashCode 契約理解 Java 與其他語言的根本差異。

從一張會員卡開始:為什麼 Java 一定要先有「類別」

想像你要替一間健身房寫一個會員管理程式。每位會員都有姓名、會員編號、剩餘點數,還能做「儲值」「扣點」這些動作。如果你寫過 Python,你可能會直覺地先開一個函式、開幾個變數,邊寫邊長出結構。但在 Java,你連印出一個「Hello」都得先把它放進一個 class 裡——程式碼沒有類別就無處安身。

這不是 Java 在為難你,而是它的世界觀:萬物皆在類別之中(everything lives in a class)。Java 是一門 OOP-first 的語言,物件導向不是「可選風格」,而是語言的地基。相較於 Python 允許你寫純函式腳本、把物件導向當作進階選項,Java 從第一行就要你用「類別」來組織想法。這篇文章帶你親手把那張會員卡,從一個概念變成一個能在 JVM 上跑起來的物件。

Java 類別與物件概念示意圖

類別是藍圖,物件是蓋出來的房子

先釐清兩個最常被混淆的詞。類別(class)是一份藍圖、一個型別(type)的定義;物件(object)是依藍圖造出來、實際存在於記憶體裡的個體,也常被稱為「實例(instance)」。

一份藍圖可以蓋出無數棟房子。Member 這個類別只有一份,但你可以 new 出成千上萬個會員物件,每個物件各自帶著自己的姓名與點數。

public class Member {
    String name;   // 欄位(field):每個物件各自擁有的狀態
    int points;
}

把這份藍圖實例化(instantiate)成物件,要用 new 關鍵字:

Member alice = new Member();   // 在堆積上配置一個 Member 物件
alice.name = "Alice";
alice.points = 100;

這裡有個 Java 與 Python 的關鍵差異:Member alice 這段宣告了變數 alice型別就是 Member。Java 是靜態強型別(statically & strongly typed)語言——每個變數的型別在編譯期就固定,編譯器會在你執行前就抓出「把字串塞進 int」這類錯誤。Python 則是動態型別,型別要到執行時才確定。靜態型別讓 Java 在大型專案、多人協作、企業系統裡格外吃香,因為很多錯誤不必等到上線才爆炸。

欄位與方法:物件的狀態與行為

一個物件由兩部分組成:欄位(field)存放狀態(state),方法(method)定義行為(behavior)。我們替 Member 加上儲值與扣點的能力:

public class Member {
    String name;
    int points;

    // 方法:對「這個物件自己的」欄位進行操作
    void topUp(int amount) {
        points += amount;
    }

    boolean spend(int cost) {
        if (cost > points) {
            return false;        // 點數不足,交易失敗
        }
        points -= cost;
        return true;
    }
}

方法寫在類別大括號內,預設就能直接存取同一個物件的欄位。注意 void topUp(...) 前面的 voidboolean spend(...) 前面的 boolean——這是回傳型別(return type)。Java 要求你明確寫出方法回傳什麼型別;void 代表「不回傳值」。這又是靜態型別的體現:相較於 Python 的 def 不必宣告回傳型別,Java 把契約寫得更死,但也更清楚。

建構子:物件誕生那一刻

前面的寫法有個尷尬:先 new 出一個空物件,再一行一行手動填欄位。萬一忘了填 name,就會得到一個半成品物件。建構子(constructor)就是為了解決這個問題——它是物件被建立時自動執行的特殊方法,負責把物件初始化到「一出生就可用」的狀態。

public class Member {
    String name;
    int points;

    // 建構子:方法名稱必須與類別同名,且「沒有」回傳型別
    Member(String name, int points) {
        this.name = name;
        this.points = points;
    }
}

現在建立物件變得乾淨又安全:

Member bob = new Member("Bob", 50);   // 一次到位,不會有半成品

建構子有兩個鐵則:名稱與類別完全相同不寫任何回傳型別(連 void 都不寫)。如果你不寫建構子,Java 會偷偷給你一個無參數的「預設建構子」;但只要你自己寫了任何一個建構子,那個免費的預設建構子就消失了。這是初學者常踩的雷,後面會再提。

this:到底是「哪一個」物件?

在上面的建構子裡,你看到了 this.name = name;this 是一個指向「目前這個物件自己」的參考(reference)。

為什麼需要它?因為建構子的參數叫 name,欄位也叫 name,發生了名稱遮蔽(shadowing)。此時光寫 name = name 會變成「參數指派給參數自己」,欄位永遠不會被更新。用 this.name 明確表示「我要的是這個物件的欄位」,把參數值正確寫進去。

Member(String name, int points) {
    this.name = name;       // this.name 是欄位;右邊的 name 是參數
    this.points = points;
}

當方法被呼叫時,例如 bob.topUp(30),在 topUp 內部的 this 就指向 bob;換成 alice.topUp(30)this 就指向 alice。同一份方法程式碼,靠 this 區分「現在操作的是哪一個物件」。這跟 Python 把第一個參數 self 顯式寫進方法簽章不同:Java 的 this 是隱含存在的,你不必在參數列宣告它,需要時直接用即可。

封裝:把欄位藏起來,只開一扇門

到目前為止,Member 的欄位是「裸露」的——任何人都能寫 bob.points = -9999; 把點數改成負數,物件的狀態瞬間失去意義。這違反了物件導向的核心原則之一:封裝(encapsulation)

封裝的做法是:把欄位設為 private(外部不可直接存取),只透過受控的方法對外開放讀寫,這些方法慣例上叫 gettersetter

public class Member {
    private String name;     // private:外部碰不到
    private int points;

    public Member(String name, int points) {
        this.name = name;
        this.points = points >= 0 ? points : 0;   // 在入口就守住規則
    }

    // getter:唯讀對外開放
    public String getName() {
        return name;
    }

    public int getPoints() {
        return points;
    }

    // setter:寫入時可以加驗證,擋掉非法值
    public void setPoints(int points) {
        if (points < 0) {
            throw new IllegalArgumentException("點數不可為負");
        }
        this.points = points;
    }
}

加上 private 後,外部程式再也不能亂改點數,只能走 setPoints 這扇門,而門口有警衛(驗證邏輯)。publicprivate 這些叫存取修飾子(access modifier),它們決定誰能看見、誰能碰。封裝的價值在於:內部實作可以隨時改,只要對外那扇門的形狀不變,外部程式完全不受影響

這裡又能對比 Python:Python 沒有真正的 private,靠的是 _name 這種「命名約定」表達「請別亂碰」,但語言層級攔不住你。Java 的 private 是編譯器強制執行的硬規則,這也是企業級程式碼偏好 Java 的理由之一——規則由語言守,不靠君子協定。

動手寫一段:完整可執行的會員系統

把上面所有概念組裝成一支能跑的程式。注意 Java 的進入點固定是 public static void main(String[] args)

public class MemberDemo {
    public static void main(String[] args) {
        Member alice = new Member("Alice", 100);
        Member bob = new Member("Bob", 50);

        alice.topUp(30);                 // Alice: 100 + 30 = 130
        boolean ok = bob.spend(60);      // Bob 想花 60,但只有 50

        System.out.println(alice.getName() + " 的點數:" + alice.getPoints());
        System.out.println(bob.getName() + " 扣點是否成功:" + ok);
        System.out.println(bob.getName() + " 的點數:" + bob.getPoints());
    }
}

class Member {
    private String name;
    private int points;

    public Member(String name, int points) {
        this.name = name;
        this.points = points >= 0 ? points : 0;
    }

    public void topUp(int amount) {
        points += amount;
    }

    public boolean spend(int cost) {
        if (cost > points) {
            return false;
        }
        points -= cost;
        return true;
    }

    public String getName() {
        return name;
    }

    public int getPoints() {
        return points;
    }
}

// 輸出:
// Alice 的點數:130
// Bob 扣點是否成功:false
// Bob 的點數:50

把這段存成 MemberDemo.java,用 javac MemberDemo.java 編譯(產生 .class 位元組碼),再用 java MemberDemo 執行。你會看到 alicebob 各自記得自己的點數,互不干擾——這就是「一份藍圖、多個獨立物件」的實感。

package:替類別找一個家

當專案長大,幾百個類別擠在同一個資料夾會亂成一團,而且兩個人都想叫某個類別 Member 時就會撞名。package(套件)是 Java 的命名空間與檔案組織機制,用來把相關類別歸類,並避免名稱衝突。

package com.uedu.gym;   // 必須是檔案的第一行程式碼

public class Member {
    // ...
}

package 名稱對應資料夾結構:com.uedu.gym.Member 這個類別,原始檔就放在 com/uedu/gym/Member.java。業界慣例是用反轉的網域名稱開頭(如 com.uedu.*)來確保全球唯一。要在別的 package 用到它,就 import

import com.uedu.gym.Member;

相較於 Python 用「模組即檔案、套件即資料夾」的較鬆散方式,Java 的 package 與目錄結構是強制對應的,編譯器會檢查。這份嚴謹是 Java 龐大企業生態(Spring、Maven 等)能穩定運作的基礎。

常見錯誤

初學 Java 物件導向時,這幾個雷幾乎人人踩過,先打預防針:

  1. 以為宣告變數就等於建立了物件。 Member m; 只是宣告了一個型別為 Member 的參考變數,它此刻是 null,並沒有任何物件。一定要 m = new Member(...) 才真的造出物件,否則對 null 呼叫方法會丟 NullPointerException

  2. 建構子忘了 this,導致欄位永遠是預設值。 寫成 name = name; 時,因為名稱遮蔽,你只是把參數指派給自己,欄位 name 始終是 null。記得用 this.name = name;

  3. 自訂建構子後,忘了預設建構子已經消失。 一旦你寫了 Member(String, int),那個免費的無參數 Member() 就不存在了。若你還需要它,得自己再補一個 Member() {}

  4. 誤把建構子寫上回傳型別。 寫成 void Member(...) {} 的瞬間,它就不再是建構子,而退化成一個剛好同名的普通方法,new Member(...) 會找不到對應建構子而編譯失敗。

  5. 把欄位全開成 public 圖方便。 這等於拆掉封裝的門,任何人都能把物件改成非法狀態。慣例是欄位 private、透過方法控管存取。

重點回顧

  • 類別是藍圖(型別定義),物件是 new 出來、活在記憶體中的實例。
  • 欄位存狀態、方法定行為;this 指向「目前這個物件自己」。
  • 建構子與類別同名、不寫回傳型別,負責讓物件一誕生就處於合法狀態。
  • 封裝=欄位 private + 受控的 getter/setter;Java 的 private 是語言強制,不是約定。
  • package 提供命名空間並與目錄結構強制對應。

深入探討(研究所視角)

到這裡你已經會用 Java 寫物件了。但若要真正理解這門語言,得往下挖一層,看物件在記憶體裡長什麼樣、變數到底裝了什麼。

物件配置在堆積,變數握的是參考

當你寫 Member bob = new Member("Bob", 50);,背後其實發生了兩件事,分別動到兩塊記憶體區域:

  • 堆積(heap)new 在堆積上配置一塊空間,放置真正的 Member 物件(它的 namepoints 等欄位資料就住在這裡)。
  • 堆疊(stack):區域變數 bob 本身放在當前方法的呼叫堆疊框(stack frame)裡,它不是物件本體,而是一個參考(reference)——你可以把它想成一個指向堆積位址的「把手」。

這就是 Java 的參考語意(reference semantics)。Java 中的變數,凡是物件型別(非 intdouble 等基本型別 primitive),裝的都是參考而非物件本身。理解這點,下面這個經典陷阱就不再神祕:

Member a = new Member("Amy", 100);
Member b = a;          // 複製的是「參考」,不是物件!a 與 b 指向同一個物件
b.topUp(50);
System.out.println(a.getPoints());   // 輸出:150(a 也變了,因為根本是同一個物件)

b = a 只複製了把手,沒有複製房子。ab 握著同一塊堆積記憶體的兩個把手,透過任一個修改都會反映在另一個上。相較之下,基本型別(int x = 5; int y = x;)複製的是值本身,y 改了不影響 x

物件不再被任何參考指向時(例如把 ab 都設成 null 或它們離開作用域),這塊堆積空間就成為垃圾。Java 的自動記憶體回收(garbage collection, GC)會在適當時機回收它——這是 Java 相對於 C/C++ 的一大解放:你不必手動 free,也因此避開了一整類「忘記釋放」「重複釋放」「懸空指標」的記憶體錯誤。代價是 GC 會在背景花費運算資源,且回收時機不完全由你掌控。

== 比的是把手,equals 才比內容

既然變數握的是參考,== 運算子對物件比較的就是「兩個把手是否指向同一塊堆積位址」,也就是同一性(identity),而不是「內容是否相等」。

Member x = new Member("Amy", 100);
Member y = new Member("Amy", 100);
System.out.println(x == y);        // 輸出:false(兩個不同物件,位址不同)

兩個會員資料一模一樣,但它們是堆積上兩棟不同的房子,== 自然是 false。要比較「邏輯上是否相等」,必須覆寫(override)equals 方法。而 Java 規定:只要你覆寫 equals,就必須一起覆寫 hashCode,這就是著名的 equals/hashCode 契約(contract)

契約的核心規則是:a.equals(b)true,則 a.hashCode() 必須等於 b.hashCode()(反之不必然,兩個不相等的物件允許湊巧同雜湊值,稱為碰撞 collision)。

為什麼這條契約如此重要?因為 HashMapHashSet 這些以雜湊為基礎的集合,存取流程是先用 hashCode 定位到某個桶(bucket),再用 equals 在桶內逐一比對。它們依賴 hashCode 把相等的物件導向同一個桶。若你只改 equals 而不改 hashCode,兩個「相等」的物件可能算出不同雜湊值、被丟進不同的桶,HashSet 就會誤判它們不重複,集合的去重與查找邏輯整個壞掉。

import java.util.Objects;

public class Member {
    private String name;
    private int points;

    // ... 建構子與 getter 省略 ...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                    // 同一物件,快速通過
        if (o == null || getClass() != o.getClass()) return false;
        Member other = (Member) o;
        return points == other.points
                && Objects.equals(name, other.name);   // 逐欄位比對
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, points);             // 用相同欄位算雜湊,保證契約
    }
}

Objects.equalsObjects.hash 是慣例做法——它們會妥善處理 null,並確保 equalshashCode 基於「相同的欄位集合」計算,這正是維持契約的關鍵。正確的雜湊讓 HashMap 的平均查找維持在 $O(1)$;若雜湊設計糟糕、大量碰撞退化成一條鏈,最壞情況會掉到 $O(n)$(現代 JDK 在單一桶過長時會把鏈轉成紅黑樹,最壞情況改善為 $O(\log n)$)。

把這條線索串起來:Java 是 OOP-first、靜態強型別、以參考語意操作物件、靠 JVM 上的 GC 自動管理堆積記憶體。你今天寫的每一個 new、每一次 equals 覆寫,背後都連著這套機制。理解了它,你就不只是「會用 Java」,而是真的「懂 Java」。

AI 共讀助教正在陪你讀:Java 類別與物件:從一張會員卡看懂 OOP-first 的世界觀
嗨!我是這篇文章的共讀助教,只根據〈Java 類別與物件:從一張會員卡看懂 OOP-first 的世界觀〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。