前言
Java 帶有強烈的工程特質,直接把物件導向融入語法。但初學者沒有物件導向觀念,有時會不小時寫出語法正確但不符合設計初衷的程式碼。
本文整理了一些常見情境,讓讀者避開這些因 Java 語法帶來的坑。
這些坑大多不是語法錯誤,而是「設計責任不清」導致的問題。
其實你不需要物件導向
由於 Java 的工程特質,學習者被迫一開始就用物件導向的思維寫程式碼。這是誤用語法的來源。
C 的主函式很單純:
int main (int argc, char **argv)
{
/* Implement your code here. */
}
但 Java 的主函式就稍微長一點:
public class MainProgram
{
public static void main (String[] args)
{
/* Implement your code here. */
}
}
一般來說,Java 講師會說「這是樣板,照著寫。」這是為你好。因為一開始就講太多細節,反而對學習有害。
不過,如果你想控制你的 Java 程式碼,減少不必要的物件導向,可以參考這個寫法:
/* A driver class. */
public final class MainProgram
{
/* The main function of a Java program. */
public static void main (String[] args)
{
/* Implement your code here. */
}
/* A home-made assert. */
private static void assertCond (boolean cond)
{
if (!cond)
throw new AssertionError("Wrong condition");
}
/* A home-made assert with a custom statement. */
private static void assertCond (boolean cond, String stat)
{
if (!cond)
throw new AssertionError(stat);
}
}
這種寫法有幾個特點:
MainProgram代表這是一個沒有語意的容器- 使用
final明確表示這個類別不參與繼承設計;它只是語意上的容器。 - 只有在必要處用
public,其他都用private,不開放不必要的權限 - 用 class method 模擬 C、C++ 的 function
這裡的重點不是「不要用物件導向」,而是:
在問題還很單純時,不要為了符合語法而強行套入物件設計。
很多初學者會卡住,是因為同時在學兩件事:
- Java 語法
- 物件導向設計
這兩者其實是不同層次的問題。
上帝物件
我們前面的樣板在展示小程式時很好用,但在程式碼變大時,會衍生另一個問題:權責不清的類別。
簡單地說,就是一個完全不劃分程式功能的超大物件,把所有程式碼都塞在一起。
上帝物件通常不是一開始就存在,而是這樣長出來的:
- 一開始所有程式碼都寫在
main - 發現太長 → 抽成 method
- method 還是很多 → 全部留在同一個 class
- 最後變成「什麼都做的 class」
也就是說,它其實是「沒有拆責任」的結果,而不是設計錯誤的起點。
若要防止程式碼滑向上帝物件,同時要避免過度物件導向,可以參考以下寫法:
/** Just a namespace for assertions. */
final class AssertUtils
{
/* Prevent object creation. */
private AssertUtils() {
throw new AssertionError();
}
/* A home-made assert. */
static void assertCond(boolean cond)
{
if (!cond)
throw new AssertionError("Assertion failed");
}
/* A home-made assert with a custom statement. */
static void assertCond(boolean cond, String stat)
{
if (!cond)
throw new AssertionError(stat);
}
}
public final class MainProgram
{
public static void main (String[] args)
{
AssertUtils.assertCond(3 == (1 + 2));
}
}
這種寫法有幾個特點:
- 同樣使用
final明確表示這個類別不參與繼承設計 - 刻意阻止建構物件,將類別保持在命名空間的角色
- 將程式碼的權責區分開來,防止上帝物件
這種「工具型 class」有一個重要前提:
它不應該有狀態(stateless)
像 AssertUtils 這類 class:
- 不需要建構物件
- 不應該保存資料
- 只負責純計算或檢查
一旦開始出現狀態(例如 member variable),就應該重新思考是否仍適合用這種設計。
實務上,也有將類別當成命名空間的做法,像是 java.lang.Math 類別。這個類別很單純,包了一些常見的數學函式,完全無狀態,不需要生成物件就可以使用。
將欄位公開
對於有狀態的類別來說,將欄位公開幾乎沒有任何益處。你無法預期類別使用者使用欄位。你寫的欄位檢查函式形同失效。
由於 Java 沒有 struct。偶爾會有程式設計者把 class 當成 struct 來用。這時候你要知道那是個 struct,不要試圖將它長成 class。
初學者會這樣寫,通常是因為:
- 想模仿 C 的
struct - 覺得 getter / setter 很麻煩
但在 Java 中,這會讓你失去一個關鍵能力:
控制狀態變化(state transition)
例如:
user.age = -10; // 編譯不會錯,但邏輯壞了
如果你真的只是需要一個「純資料結構」,可以考慮:
- 明確把它當 DTO(Data Transfer Object)
- 或使用
record(Java 16+)
public record User(String name, int age) {}
這樣比手動開欄位更清楚,也更安全。
過度抽象
函式、類別的本質是抽象化。但沒控制好的話,有可能做過頭,只是增加程式碼閱讀的困難度。
以下範例程式碼用來模擬 C 的主函式:
public final class MainProgram
{
public static void main (String[] args)
{
int exitCode = run(args);
System.exit(exitCode);
}
private static int run (String[] args)
{
/* Implement your code here. */
return 0;
}
}
如果你沒有想要測試 run 函式的話,這樣的寫法其實是過度設計。
過度設計還蠻常見的。例如:只有一個類別時就寫 interface,過早撰寫 dependency injection。這在教學上有時是不可避免的。那是在展示語法或模式。但實際撰寫程式碼,不該讓抽象大於需求。
可以用一個簡單標準來判斷是否過度抽象:
如果拿掉這層抽象,程式碼會變更難維護嗎?
如果答案是「不會」,那這層抽象很可能是不必要的。
抽象應該用來「隔離變化」,而不是「預測未來」。
結語
這些問題看起來分散,但本質都一樣:
沒有清楚定義「這個 class 的責任是什麼」
當你在設計 class 時,可以問自己三件事:
- 這個 class 負責什麼?
- 它的狀態應該由誰控制?
- 如果需求改變,這個 class 會不會變得很難改?
如果這三個問題答不出來,通常就會開始踩坑。