1. 存取修飾子的概覽
在 Java 中,類別、欄位、方法與建構子有四種存取層級:
| 修飾子 | 可在哪裡存取? |
|---|---|
|
到處皆可:類別內、其他類別、其他套件 |
|
在類別內、子類別(繼承者)、同一套件的其他類別 |
|
僅限套件內(當未明確指定修飾子時) |
|
僅限當前類別內 |
接下來逐一說明——搭配範例、小幽默與一些意想不到的轉折。
public — 公開存取
public 就像向全世界公告:「大家都可以進來!」若類別、欄位、方法或建構子被宣告為 public,便能從任何其他類別存取,甚至位於不同套件也可以。
範例:
public class Cat {
public String name;
public void sayMeow() {
System.out.println("喵!");
}
}
這個類別及其欄位/方法可以從任何地方存取。當你希望類別對所有人可用時(例如撰寫函式庫),這會很方便。
不過! 公開欄位並不總是好主意(見上一堂課)。通常只會將需要對外提供的功能性方法設為公開,而欄位幾乎一律保持為 private。
private — 僅限類別內部存取
private 就像上了密碼鎖的保險箱:除了該類別本身,沒有人能存取這些成員。就連繼承者(子類別)也看不到私有欄位與方法!
範例:
public class Cat {
private String secretName;
public void setSecretName(String name) {
secretName = name;
}
public String getSecretName() {
return secretName;
}
}
這裡的 secretName 無法從其他類別直接讀取或修改。只有 Cat 本身(或其方法)能做到。這是封裝的基礎:我們隱藏內部細節,並僅透過方法對外提供存取。
protected — 受保護的存取
protected 好比 VIP 通行證:允許該類別本身、其繼承者(即使在其他套件)以及同一套件中的所有類別存取。
範例:
public class Animal {
protected int age;
protected void growOlder() {
age++;
}
}
任何繼承自 Animal 的類別都能存取欄位 age 與方法 growOlder()。
public class Cat extends Animal {
public void haveBirthday() {
growOlder();
System.out.println("這隻貓已經 " + age + " 歲了!");
}
}
此外,同一套件中的所有類別也可以存取 protected 成員。
(package-private) — 套件內部存取
若未指定任何存取修飾子,該成員會被視為 package-private(或稱「預設存取」)。就像沒有上鎖的門,但只對自己人開放:只能被同一套件內的類別存取。
範例:
class Dog {
String name; // package-private
void bark() { // package-private
System.out.println("汪!");
}
}
Dog 類別、它的欄位 name 與方法 bark() 只能在同一套件中使用。嘗試自其他套件存取將導致編譯錯誤。
2. 修飾子在欄位與方法上的使用
為什麼欄位幾乎一律設為 private
類別的欄位代表其內部狀態。若將它們設為公開,任何外部程式碼都能隨時修改。這就像讓陌生小孩把你的家具當樂高玩:某天你醒來,發現冰箱被倒放在浴室裡。
封裝不良的範例:
public class Person {
public String name;
public int age;
}
良好封裝的範例:
public class Person {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
透過方法(getter 與 setter)可以控制外部類別能如何修改欄位。例如,可以禁止出現負的年齡。
何時將方法設為 public、protected 或 package-private
- public — 當方法需要對所有人可用。通常是類別的主要功能。
- protected — 當方法只需要給繼承者或套件內部使用(例如在子類別可能有用的輔助方法)。
- package-private — 當方法只需要在套件內部使用、但不該對外公開(例如實作細節)。
- private — 當方法只在類別內部使用(例如內部邏輯的輔助方法)。
範例:
public class BankAccount {
private double balance;
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
protected void applyInterest() {
balance *= 1.05;
}
void internalAudit() {
// package-private: 僅供套件內的類別使用
}
private void logAction(String action) {
// 僅供類別內部使用
}
}
3. 程式碼範例:含有不同存取層級的類別
讓我們建立一個同時擁有公開、私有、受保護與套件層級成員的類別,並嘗試從其他類別存取它們,看看會發生什麼事。
package zoo;
public class Animal {
public String publicName = "人人可用";
protected String protectedName = "僅供子類與同套件";
String packageName = "僅供套件內";
private String privateName = "僅供 Animal";
public void publicMethod() {
System.out.println("公開方法");
}
protected void protectedMethod() {
System.out.println("受保護的方法");
}
void packageMethod() {
System.out.println("套件層級的方法");
}
private void privateMethod() {
System.out.println("私有方法");
}
}
現在嘗試在同一套件中的另一個類別存取這些成員:
package zoo;
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
System.out.println(animal.publicName); // OK
System.out.println(animal.protectedName); // OK
System.out.println(animal.packageName); // OK
System.out.println(animal.privateName); // 錯誤:private
animal.publicMethod(); // OK
animal.protectedMethod(); // OK
animal.packageMethod(); // OK
animal.privateMethod(); // 錯誤:private
}
}
再從另一個套件中嘗試存取這些成員:
package other;
import zoo.Animal;
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
System.out.println(animal.publicName); // OK
System.out.println(animal.protectedName); // 錯誤:protected
System.out.println(animal.packageName); // 錯誤:package-private
System.out.println(animal.privateName); // 錯誤:private
animal.publicMethod(); // OK
animal.protectedMethod(); // 錯誤:protected
animal.packageMethod(); // 錯誤:package-private
animal.privateMethod(); // 錯誤:private
}
}
結論:
- public — 到處都能存取。
- protected — 可在同一套件與子類別中存取(即使位於其他套件,只要透過繼承即可)。
- package-private — 僅限套件內。
- private — 僅限類別內。
4. 最佳實務:如何選擇存取修飾子
最小化可見範圍
能看到(或碰到)你的欄位或方法的程式碼越少越好。只公開外部程式碼真正需要的部分。這稱為最小權限原則(principle of least privilege)。
- 欄位幾乎一律應為 private。例外僅限真正的常數(public static final),細節將在後續課程說明。
- 方法僅在屬於對外 API 時才設為 public。
- 輔助方法(內部邏輯)— private。
- 提供給子類別的方法 — protected。
- 套件內部的服務性方法 — package-private。
為什麼這很重要?
- 若實作細節被暴露,任何變動都可能破壞他人的程式碼。
- 類別會變得更難測試與維護。
- 偶發的錯誤(例如欄位被錯誤修改)可能導致 bug。
有時新手會想:「何必這麼麻煩,不如全部設成 public!」但等到專案逐漸擴大,你就得重寫半個程式,只因為有人直接修改了類別的欄位。
5. 使用存取修飾子時的常見錯誤
錯誤 № 1: 將欄位維持 public 或預設為 package-private。
若未指定修飾子,欄位或方法將可被同一套件內的所有類別存取。當有人開始直接修改你的欄位時,這可能會導致意料之外的狀況。
錯誤 № 2: 嘗試從其他類別存取 private 成員。
編譯器不會允許——你會得到錯誤。但如果你試圖透過 reflection(反射)繞過——歡迎來到充滿 bug 與意外崩潰的世界。
錯誤 № 3: 過度使用 public。
若凡事都宣告為 public,類別就像一個打開的線路盒——任何人都可能拉錯一條線而把一切搞壞。
錯誤 № 4: 沒有把僅供子類別使用的方法設為 protected。
如果方法只為了在子類別擴充時使用,請把它設為 protected,而不是 public。
錯誤 № 5: 隱性的 package-private 可見性。
有時會忘了指定修飾子,結果方法對整個套件都可存取。若你原本以為它是 private,這就會成為一個意外。
GO TO FULL VERSION