1. 什麼是內部類別(inner class)?
在 Java 中,類別不僅可以在最外層(檔案層級)宣告,也可以在另一個類別內宣告。這樣的類別稱為巢狀(nested class)。若該類別在宣告時未使用 static 修飾子,則稱為內部(non-static inner class,或簡稱 inner class)。
內部類別是宣告在另一個類別之內,並且與該外部類別實例關聯的類別。它可以存取外部類別的所有欄位與方法,即使它們是 private。就好像物件有一位「祕密助手」,被允許做任何事!
可以把它想成房子與房間。房子是外部類別,房間是內部類別。房間不能離開房子獨立存在,但能使用房子的資源:照明、暖氣、家具。若房子消失,房間也會消失。Java 的內部類別正是如此運作!
為什麼需要內部類別?
- 邏輯關聯:當某個類別只用來配合另一個類別、在其之外沒有意義時。
- 封裝:能隱藏實作細節,不會汙染套件的命名空間。
- 存取私有成員:內部類別可以存取外部類別的 private 欄位與方法。
- 精簡:減少套件層級的「雜項」類別數量。
內部類別的宣告語法
宣告內部類別很簡單:在另一個類別的主體內宣告,且不使用 static 修飾子。
class Outer {
// 外部類別的欄位與方法
class Inner {
// 內部類別的欄位與方法
void printHello() {
System.out.println("Hello from Inner!");
}
}
}
這裡的 Inner 相對於 Outer 是內部類別。
示意圖:視覺化
Outer
│
├─ 欄位/方法
│
└─ Inner(內部類別)
└─ 自己的欄位/方法
2. 如何建立內部類別的實例
這裡藏著新手最常踩的坑之一!內部類別的實例永遠與某個外部類別物件綁定。
範例:
Outer outer = new Outer(); // 建立外部類別的物件
Outer.Inner inner = outer.new Inner(); // 透過外部物件建立內部類別!
inner.printHello(); // Hello from Inner!
若只寫 new Inner(),會導致編譯錯誤,因為 Java 不知道要把這個內部類別綁定到哪一個外部類別物件。
為什麼?
因為內部類別可以存取外部類別的欄位與方法,所以它必須「知道」自己是與哪一個外部類別物件一起工作的。
3. 內部類別的使用範例
我們來看一個內部類別真正有用的情境。
範例 1:模型「背包與物品」
假設我們有一個 Backpack 類別,可以存放物品(Item)。但我們希望 Item 只在 Backpack 內可用,因為離開背包我們不關心物品。
public class Backpack {
private String owner;
public Backpack(String owner) {
this.owner = owner;
}
// 內部類別——物品只能存在於背包之中!
class Item {
private String name;
public Item(String name) {
this.name = name;
}
public void printInfo() {
// 魔法!看得到外部類別的 private 欄位
System.out.println(owner + " 擁有物品:" + name);
}
}
}
使用方式:
Backpack bp = new Backpack("瓦夏");
Backpack.Item item = bp.new Item("Java 教科書");
item.printInfo(); // 瓦夏 擁有物品:Java 教科書
請注意:Item 即使在欄位 owner 是 private 的情況下也能存取!
範例 2:自訂集合的迭代器
在 Java 中,集合常以內部類別實作迭代器。我們來做一個簡單的集合,並用內部類別實作迭代器。
public class IntList {
private int[] data = new int[10];
private int size = 0;
public void add(int value) {
data[size++] = value;
}
// 內部類別——迭代器!
class Iterator {
private int index = 0;
public boolean hasNext() {
return index < size;
}
public int next() {
return data[index++];
}
}
}
使用方式:
IntList list = new IntList();
list.add(10);
list.add(20);
IntList.Iterator it = list.new Iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
4. 內部類別的特性
存取外部類別的成員
內部類別可以存取任何(即使是 private)外部類別的欄位與方法。這很方便,但也需要小心:若外部類別改變,內部類別可能會突然「壞掉」。
class Outer {
private int secret = 42;
class Inner {
void showSecret() {
System.out.println("祕密:" + secret);
}
}
}
內部類別不能包含靜態成員
內部(non-static inner)類別不能包含靜態欄位或方法,唯有靜態的 final 常數例外(例如,public static final int MY_CONST = 123;)。原因是內部類別總是「繫結」到外部類別的實例。
如果你需要宣告靜態巢狀類別——請使用 static 修飾子(下一堂課會介紹)。
內部類別的可見性
內部類別可以是 private、protected、public,或具有套件層級(package-private)的可見性。
public class Outer {
private class Inner { /* ... */ }
}
5. 實用細節
何時應使用內部類別
內部類別不是為了好看,而是為了邏輯性地組織程式碼。使用它們於下列情況:
- 類別只在一處需要(例如輔助用的迭代器、處理器、複雜結構的一部分)。
- 類別與外部類別高度耦合,離開它便沒有意義。
- 希望將實作細節隱藏於其他套件內的類別。
不要為了趨勢而使用內部類別! 有時若該類別在其他地方也可能有用,把它提到最外層會更好。
內部類別與 this
在內部類別中,可以使用 OuterClassName.this 存取外部類別的欄位與方法。
class Car {
private String model = "Tesla";
class Engine {
void printModel() {
// 明確地參照外部物件
System.out.println("型號:" + Car.this.model);
}
}
}
當內部與外部類別有同名欄位時,這種語法特別有用。
什麼時候不該使用內部類別
不建議使用於以下情況:
- 內部類別不會存取外部類別的欄位/方法
- 該類別可能在程式的其他地方也有用
- 外部類別變得過於龐大且複雜
在這些情況下更好的做法是:
- 將類別改為靜態巢狀類別(static class)
- 把類別拆到獨立檔案
- 以一般方法取代整個類別
6. 使用內部類別時的常見錯誤
錯誤一:嘗試在沒有外部物件的情況下建立內部類別。
如果寫 new Inner(),編譯器會拋出錯誤:"No enclosing instance of type Outer is accessible"。務必透過外部類別的物件來建立內部類別:outer.new Inner()。
錯誤二:嘗試在內部類別中宣告靜態欄位或方法。
內部類別不能包含靜態成員(常數除外)。若有需要——請使用靜態巢狀類別,也就是用 static 修飾子來宣告類別。
錯誤三:過度使用內部類別。
如果內部類別不使用外部類別的欄位/方法,它很可能應該是 static,或乾脆拉到最外層。不要「以防萬一」就建立內部類別。
錯誤四:混淆對外部類別欄位的參照。
當內部與外部類別有同名欄位時,請使用 OuterClassName.this.field 明確地參照外部類別的欄位。
GO TO FULL VERSION