Java 建構子是什麼?初學者常見錯誤與正確寫法一次搞懂【Thinking in Java筆記(4-1) 】

Java 建構子是什麼?初學者常見錯誤與正確寫法一次搞懂【Thinking in Java筆記(4-1) 】
Photo by orbtal media / Unsplash

為什麼要學建構子?

在初學 Java 的時候,很多人會寫出這種程式:

class Something {
  void initialize() {
    // 一堆初始化程式碼
  }
}

然後... 忘記呼叫 initialize()

來看個實際例子:

class User {
  String name;
  String email;

  void initialize(String name, String email) {
    this.name = name;
    this.email = email;
  }
}

public class Main {
  public static void main(String[] args) {
    User u = new User();
    // 如果忘了這一行,就 GG 了
    u.initialize("Amy", "amy@example.com");
    System.out.println(u.name); // null
  }
}

換成建構子的版本就不會出錯:

class User {
  String name;
  String email;

  User(String name, String email) {
    this.name = name;
    this.email = email;
  }
}

public class Main {
  public static void main(String[] args) {
    User u = new User("Amy", "amy@example.com");
    System.out.println(u.name); // Amy
  }
}

建構子的好處是:沒辦法忘記初始化,因為你沒寫參數,編譯器會直接抱怨。它就像 Java 在幫你看著資料表單說:「欸欸欸欸你 email 還沒填啦!」

為了解決這種「人腦不可靠」的問題,Java 提供了建構子(constructors)。建構子是一段特殊的程式碼,當你用 new 創建物件時,它會自動執行。這樣你就不會忘了初始化了,因為你根本不需要自己呼叫它。

建構子的名字必須跟類別名字一樣,像是 Java 在跟你說:「我不是一般方法,我是這個類別的開場白。」

建構子語法與方法差異

你可以這樣看建構子(constructor)跟方法(method)的差別:

  • 建構子是「你出生時醫院幫你打的疫苗」,在物件一創出來時就自動執行
  • 方法是「你長大之後去診所看病」,要你手動叫它做什麼事它才會動
class Dog {
  // 這是建構子,跟類別名字一樣,沒有 return 型態
  Dog() {
    System.out.println("A dog is born.");
  }

  // 這是方法,名字可以自訂,有 return 型態(這邊是 void)
  void bark() {
    System.out.println("Woof!");
  }
}

public class Main {
  public static void main(String[] args) {
    Dog d = new Dog(); // 呼叫建構子,自動印出 "A dog is born."
    d.bark();          // 呼叫方法,手動印出 "Woof!"
  }
}

所以記得:建構子不是你平常會直接叫來用的東西,而是物件一出生就偷偷跑一次的「開場儀式」

預設建構子:讓物件自動長出來

來看個簡單的例子:

class Rock {
  Rock() {
    System.out.print("Rock ");
  }
}

public class SimpleConstructor {
  public static void main(String[] args) {
    for(int i = 0; i < 10; i++)
      new Rock();
  }
}

輸出:

Rock Rock Rock Rock Rock Rock Rock Rock Rock Rock

這段程式碼每次建立 Rock 物件時都會印出 "Rock"。這種建構子叫做「預設建構子」,也叫「無參數建構子(no-arg constructor)」。

就像每次你結帳,店員都會說「會員載具?」一樣,這個建構子會在物件出生時自動被叫一次,讓你不用手動初始化

帶參數建構子:物件客製化

但有時你不只想要一顆石頭,你想要一顆寫著編號的石頭:

class Rock2 {
  Rock2(int i) {
    System.out.print("Rock " + i + " ");
  }
}

public class SimpleConstructor2 {
  public static void main(String[] args) {
    for(int i = 0; i < 8; i++)
      new Rock2(i);
  }
}

輸出:

Rock 0 Rock 1 Rock 2 Rock 3 Rock 4 Rock 5 Rock 6 Rock 7

這就像你點飲料時說:「我要少冰半糖」,建構子接收參數,讓每個物件可以不一樣

建構子不能有返回值(就算是 void 也不行),因為它不是用來「回傳」東西的,而是「誕生」一個東西

方法多載(Overloading):同一個名字,不同的用法

在寫 Java 的時候,你可能會遇到一個狀況:你需要一個功能,但它根據輸入的資料不同,應該有不同的行為。這種時候,Java 提供一種設計叫做「方法多載」(Method Overloading)

想像你寫了一個 print() 方法,傳進來的是整數、浮點數或字串時,希望它能做出對應的輸出格式。難道你要寫三個叫做 printInt()printFloat()printString() 的方法嗎?不。這樣超麻煩而且很亂

方法多載的意思是:你可以用同一個方法名稱,但根據傳入的參數數量、類型或順序不同,讓 Java 自動知道你要叫哪一個版本的 print()

我們接下來會用一棵樹(Tree 類別)來解釋這個概念,因為樹的成長狀況與初始化參數有關,也方便展示建構子和方法的不同變形方式。這樣你不只會知道什麼叫多載,也會知道它為什麼存在,以及在什麼情境下會派上用場

class Tree {
  int height;
  Tree() {
    print("Planting a seedling");
    height = 0;
  }
  Tree(int initialHeight) {
    height = initialHeight;
    print("Creating new Tree that is " +
      height + " feet tall");
  }	
  void info() {
    print("Tree is " + height + " feet tall");
  }
  void info(String s) {
    print(s + ": Tree is " + height + " feet tall");
  }
}

public class Overloading {
  public static void main(String[] args) {
    for(int i = 0; i < 5; i++) {
      Tree t = new Tree(i);
      t.info();
      t.info("overloaded method");
    }
    // Overloaded constructor:
    new Tree();
  }	
} /* Output:
Creating new Tree that is 0 feet tall
Tree is 0 feet tall
overloaded method: Tree is 0 feet tall
Creating new Tree that is 1 feet tall
Tree is 1 feet tall
overloaded method: Tree is 1 feet tall
Creating new Tree that is 2 feet tall
Tree is 2 feet tall
overloaded method: Tree is 2 feet tall
Creating new Tree that is 3 feet tall
Tree is 3 feet tall
overloaded method: Tree is 3 feet tall
Creating new Tree that is 4 feet tall
Tree is 4 feet tall
overloaded method: Tree is 4 feet tall
Planting a seedling
*/

這個範例展示了建構子和方法的多載如何根據不同的參數呼叫不同版本的邏輯:

  • Tree():這是無參數建構子,代表你種下了一棵新樹苗,會印出 "Planting a seedling"
  • Tree(int initialHeight):這是有參數建構子,表示你直接栽種一棵已經長到某個高度的樹
  • info()info(String s):這兩個方法名稱相同,但一個沒參數、一個有字串參數。前者只是印出樹的高度,後者加上額外的標籤文字

這就像你對 Siri 說:「打給媽媽」和「打給媽媽用擴音」,Siri 會根據你說的話判斷要怎麼執行。Java 也是這樣透過參數來決定呼叫哪個對應的方法版本

怎麼分辨多載的方法?

多載不是亂來的,要有不同的:

  • 參數數量
  • 參數型態
  • 參數順序

不可以只改返回型態。例如:

int doSomething();
String doSomething(); // ❌ 這會讓編譯器抓狂

因為你呼叫 doSomething() 時,Java 只能根據方法名稱與參數來判斷該執行哪一個方法版本。如果你直接這樣呼叫:

doSomething();

Java 會困惑:你到底是想要 int 還是 String 的那個?更麻煩的是,這個方法可能只是為了觸發某個副作用,你甚至沒有把它的回傳值存起來

也就是說,光靠方法名稱和參數,Java 才能決定要呼叫哪一個。回傳型態根本不參與這個比對過程,就像你打電話給客服說「我要處理一件事」,但完全沒說是什麼事情,他們也不可能靠聲音判斷你是誰、要什麼

多載陷阱:參數順序雖然能分辨,但真的適合嗎?

你當然可以用參數順序來區分多載,但當實作內容也跟著變化時,這種寫法很容易埋下地雷,讓後來維護的人一臉懵

我們換一個更貼近生活的例子:假設你寫了一個 sendMessage 方法,要發送訊息給聯絡人,有時候是給朋友,有時候是給主管。你可能會根據對象的不同使用不同的語氣,例如:

class Messenger {
  void sendMessage(String name, String time) {
    System.out.println("Yo " + name + "! Let's meet at " + time + ".");
  }

  void sendMessage(String time, String name) {
    System.out.println("Dear " + name + ", our meeting is scheduled at " + time + ".");
  }
}
  

這種參數順序做為方法多載依據的寫法,看起來像是聰明的設計,實際上是寫給未來的自己挖坑。當下一個人看到 sendMessage("David", "10:00 AM") 時,他可能會以為這是一個日常訊息,但實際上是主管的正式通知版本,或反之

錯誤使用的結果不只是輸出錯內容,還可能會產生嚴重的商業誤會。如果今天把「Yo 老闆,來喝咖啡」這種訊息誤發給主管,後果你自己想像

不要迷信多載。當你開始讓方法的語意依賴「參數的順序」來決定行為,那代表這個方法其實已經有兩個意思了,就該拆開來。

class Messenger {
  void sendFriendMessage(String name, String time) {
    System.out.println("Yo " + name + "! Let's meet at " + time + ".");
  }

  void sendWorkMessage(String name, String time) {
    System.out.println("Dear " + name + ", our meeting is scheduled at " + time + ".");
  }
}

這樣看的人不用猜、用的人不會怕。方法名字就是說明書,不要讓人要逐行看code才能理解這在幹嘛

  • 每個多載的方法必須具有獨一無二的參數列表,包括參數的數量、型態或參數的排列順序。
  • 參數順序的變更雖然技術上可行以區分多載方法,但可能造成程式碼難以維護。

型態轉換與基本型態多載

數字在 Java 裡預設是 int。如果你定義了 void f(float x),然後呼叫 f(5),Java 會自動幫你轉型(int ➡ float)

但如果你定義 f(byte x),然後你寫 f(300),那會炸。因為 300 超過 byte 的範圍

Java 的轉型行為是「有禮貌但死板」:它會幫忙,但不會猜測你心裡在想什麼

預設建構子(Default Constructors)是什麼?什麼時候 Java 會自動幫你加?

在 Java 裡,當你沒有明確定義任何建構子時,編譯器會很貼心地幫你塞進一個「預設建構子」,也就是沒有參數的那種:

class Cat {
  // Java 自動加入這樣的建構子:Cat() {}
}

public class Main {
  public static void main(String[] args) {
    Cat c = new Cat(); // ✅ 沒問題
  }
}

這樣你就可以直接建立 Cat 的實例,而不用特別去定義建構子。但這個貼心只在你完全沒有定義建構子的情況下才有效

一旦你自己動手寫了建構子,Java 就不再幫你了

class Dog {
  Dog(String name) {
    System.out.println("Dog name is " + name);
  }
}

public class Main {
  public static void main(String[] args) {
    new Dog(); // ❌ 編譯錯誤!
  }
}

這會噴錯,因為你定義了一個 Dog(String name) 的建構子,但卻沒有定義 Dog(),Java 就不會自動幫你加。這是它的規則:「你都說你想自己來了,我就不多管閒事。」

所以如果你需要同時支援有參數和無參數的建立方式,你就得兩個建構子都寫上去:

class Dog {
  Dog() {
    System.out.println("Default dog.");
  }

  Dog(String name) {
    System.out.println("Dog name is " + name);
  }
}

總結一下:預設建構子的原則

  • 如果你「完全沒有」寫建構子,Java 會自動加上 ClassName()
  • 一旦你寫了「任何一個」建構子,Java 就不會自動幫你加了
  • 所以如果你還想保留無參數建構方式,要自己手動補上

這就是 Java 給你的一個「默默幫你但不提醒你」的特性,懂了就能少踩一個坑

結語:建構子不是魔法,但會救你一命

看完這篇,你應該已經對 Java 建構子的運作方式有了具體的認識。它不是什麼高深魔法,而是幫你保證物件一出生就處於「可用狀態」的保險機制。你不用每次都記得呼叫 initialize(),Java 幫你記住了這一步

而建構子的多載設計、參數順序的風險、型態轉換帶來的混淆,全都是寫程式會遇到的「現實問題」。真正的程式設計不是把程式寫給自己跑,而是寫給別人看得懂、用得穩

記住這幾件事:

  • 沒有寫建構子?Java 幫你生
  • 自己寫了?Java 就不幫你加無參版本
  • 同名方法參數順序不同雖然能跑,但可能害你出事
  • 方法名字要能說明用途,別只想耍帥用多載

如果你未來寫出的 class 讓別人一眼就知道該怎麼初始化、怎麼用,那你就真的把 constructor 的精神學到了

下次你看到 new Something(...),就知道背後其實藏著多少貼心(或陷阱)。寫好建構子,是你跟 Java 相處融洽的第一步