1. 封裝的定義
封裝 是物件導向程式設計(OOP)的基本原則之一。白話來說,封裝就是把物件的內部細節藏起來,只透過特定的「門」——公開的方法——對外提供存取。
想像一台現代咖啡機。使用者只看到按鈕與顯示器——他不需要知道鍋爐、幫浦與管路是怎麼運作的。他按下「卡布奇諾」——就得到結果。裡面的一切都被隱藏。這就是封裝!
在 Java(以及其他 OOP 語言)中,封裝仰賴以下方式達成:
- 資料隱藏——將類別的欄位宣告為 private(或至少不要 public)。
- 公開介面——只「對外」提供物件使用者真正需要的方法。
示意圖:封裝如何呈現
+-------------------------------+
| Class Student |
|-------------------------------|
| - name: String | // private 欄位
| - age: int | // private 欄位
|-------------------------------|
| + getName(): String | // public 方法
| + setName(String): void | // public 方法
| + getAge(): int | // public 方法
| + setAge(int): void | // public 方法
+-------------------------------+
這裡的 - 代表 private(隱藏),而 + 代表 public(對外可用)。
什麼是 getter 與 setter?
在探討封裝的用途之前,先快速認識一下 getter 與 setter——它們是與類別私有欄位「互動」的特殊方法。
Getter——用來取得私有欄位的值。通常命名為 getFieldName()。
Setter——用來設定私有欄位的值。通常命名為 setFieldName(value)。
簡單範例:
public class Student {
private String name; // 私有欄位——外部不可見
// Getter——「給我學生的名字」
public String getName() {
return name;
}
// Setter——「設定學生的名字」
public void setName(String name) {
this.name = name;
}
}
如何使用:
Student student = new Student();
student.setName("Vasya"); // 透過 setter 設定名字
String name = student.getName(); // 透過 getter 取得名字
把 getter 與 setter 想像成對物件的「有禮貌請求」:與其直接伸手到口袋裡拿(student.name = "Vasya"),不如有禮貌地說:「請幫我設定名字」(student.setName("Vasya"))。
先打預防針: 再過幾講,我們會更深入探討 getter 與 setter,了解它們的眉角並學會熟練運用。現在先掌握核心概念即可!
2. 為什麼需要封裝?
保護資料不被不當使用
如果類別的所有欄位都是公開的(public),任何外部程式碼都能隨意直接修改它們:
Student s = new Student();
s.age = -1000; // 糟了,變成吸血鬼學生!
這很危險!你的程式可能開始出現不可預期的行為,且錯誤會在最意想不到的地方冒出來。
在不影響外部程式碼的前提下更改內部實作
封裝讓你可以調整類別的內部結構而不破壞使用它的程式碼。比方說,你可以改變資料的儲存方式,或在方法中加入驗證;類別的使用者完全不需要知道——他們仍然呼叫相同的方法。
提升程式碼的可讀性與可維護性
當內部細節被隱藏後,對外的介面會更乾淨、清楚。使用你類別的工程師無須了解內部如何運作——只要知道有哪些可用的方法、它們做什麼就好。
生活化的例子
想想你如何使用智慧型手機。你不會去思考觸控事件如何處理、電池如何設計或通訊模組如何工作。你只透過清楚的介面(圖示、按鈕)使用需要的功能。如果製造商更換了內部實作,你甚至不會察覺。
程式中的實際例子
假設我們有一個 BankAccount 類別。在舊版程式中,餘額以帶有點號分隔的字串儲存,例如 "1.000.50"。後來工程師決定用數值 double 來儲存。如果這個欄位是公開的,所有直接存取 account.balance 的舊程式碼都會壞掉。
但如果我們使用封裝、將欄位隱藏,只提供 deposit() 與 getBalance() 這些方法,外部程式碼甚至不會知道內部改變了:
public class BankAccount {
private double balance; // 欄位被隱藏
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public double getBalance() {
return balance;
}
}
現在如果我們明天想把餘額改成以分為單位來儲存(例如用 long),只需要修改類別的內部實作;而所有呼叫 deposit() 與 getBalance() 的外部程式碼會和以前一樣正常運作。
3. 壞與好的封裝範例
壞範例:公開欄位
public class Student {
public String name;
public int age;
}
此作法的問題:
- 任何程式碼都能替欄位賦予任意值,即便是不合理的值。
- 無法加入資料驗證。
- 若你決定更改欄位的型別或結構,所有使用到它的程式碼都得跟著改。
好範例:私有欄位與公開方法
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
// 可以加入驗證!
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("姓名不可為空");
}
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
// 確保年齡不是負數
if (age < 0) {
throw new IllegalArgumentException("年齡不能為負數");
}
this.age = age;
}
}
優點:
- 外部程式碼無法直接修改欄位——只能透過方法。
- 可以加入驗證、記錄、或自動行為(例如更新統計數據)。
- 即使內部表達方式改變(例如年齡改以其他格式儲存),對外的介面仍保持不變。
使用時的樣子
Student s = new Student();
s.setName("Alisa");
s.setAge(20);
System.out.println(s.getName() + ",年齡:" + s.getAge());
試著賦予負的年齡——你會在執行階段就收到錯誤!你的程式已被保護,避免愚蠢的輸入。
4. 與其他 OOP 原則的關係
封裝是其他 OOP 原則的「母親」。沒有它,就不會有繼承、多型或抽象。我們稍後會深入學習,但先簡短提一下:
- 繼承(extends)允許基於既有類別建立新類別,並擴充或改變其行為。若類別的內部細節是開放的,子類別可能一不小心就破壞關鍵部分。
- 多型(不同類別的物件對相同訊息做出不同回應)需要清楚區分內部實作與外部介面,否則不可行。
- 抽象——只保留重要特性並隱藏細節。封裝有助於在實務上實現抽象。
類比
想像一台汽車。駕駛只能操作方向盤、踏板與控制桿——這就是介面。其他一切(引擎、變速箱、電子系統)都藏在引擎蓋下。若駕駛能直接操控引擎的每顆螺絲,意外恐怕會頻繁得多!
5. 實務範例:在我們的應用中運用封裝
讓我們繼續開發教學用的小應用——例如「地址簿」。假設我們有個 Contact 類別,用來儲存姓名與電話。
沒有封裝(反例):
public class Contact {
public String name;
public String phone;
}
使用方式:
Contact contact = new Contact();
contact.name = ""; // 哎呀!姓名是空的
contact.phone = null; // 電話未設定
有封裝(正確做法):
public class Contact {
private String name;
private String phone;
public String getName() {
return name;
}
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("聯絡人姓名不可為空");
}
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
if (phone == null || phone.isBlank()) {
throw new IllegalArgumentException("電話不可為空");
}
this.phone = phone;
}
}
現在外部程式碼無法留下空白姓名或電話:
Contact contact = new Contact();
contact.setName("Ivan");
contact.setPhone("+1-999-123-45-67");
若嘗試設定空白姓名——程式會拋出錯誤。
6. 實用細節
封裝與長期維護
在小型教學專案中,你可能覺得「靠默契就好」:誰會把年齡設為負數,或把姓名設為空呢?但一旦專案變大、加入其他開發者,甚至過幾個月後連你自己也忘了實作細節——這時封裝就能拯救你免於混亂。
- 更容易調整類別內部——如果需要把電話改成以 PhoneNumber 物件而非字串儲存,你只要改內部實作,外部程式碼不必動。
- 更容易測試——當所有變更都透過方法發生,就能清楚追蹤資料何時、如何被修改。
- 更少錯誤——避免不合理的值與不小心的修改。
問題:是否一定要為每個欄位都做 getter 與 setter?
新手常想:「既然封裝就是私有欄位加公開的 getter/setter,那我就幫每個欄位都做一組!」其實不盡然。
- 有時欄位應該只能讀取(例如物件的唯一識別碼)。那就只提供 getter。
- 有時根本不需要「對外」提供欄位——那就不要做 getter 或 setter。
- 如果欄位只能在類別內部被修改,可以把 setter 設為私有。
黃金法則:只公開外部程式碼真正需要的資料與方法。
視覺化:做法比較
| 做法 | 欄位存取範例 | 可控性 | 安全性 |
|---|---|---|---|
| public 欄位 | |
否 | 低 |
| private 欄位 + 方法 | |
是 | 高 |
7. 使用封裝時的常見錯誤
錯誤 1:所有欄位都宣告為 public。 這是新手最常犯的錯。這樣的程式碼很快就會失控:任何人都能在你不知情的情況下修改任何資料。別這麼做——就算你很想省時間!
錯誤 2:沒有驗證與邏輯的 getter 與 setter。 既然你寫了存取方法,就用它們來做驗證:不要允許指定不合理的值。單純把參數值「複製」到欄位並不總是好主意。
錯誤 3:過早暴露內部結構。 如果你預先為所有欄位都做了 getter/setter「以防萬一」,你可能會暴露太多細節,使得日後更難修改。
錯誤 4:直接回傳可變物件。 如果欄位是可變物件(例如 List),不要直接透過 getter 回傳它。最好回傳副本,或讓它變成不可變。
GO TO FULL VERSION