Java 初始化順序怎麼判斷?搞懂變數、建構子與 static 初始化順序【Thinking in Java筆記(4-4)】
寫 Java 的時候,你可能有想過:「為什麼我沒設定值,Java 就能幫我填好變數?」「建構子跟 static 到底有什麼玄機?」這篇文章就是為了解開這些 Java 黑魔法的面紗。看完你會更懂得 Java 背後的設計邏輯,而不是照本宣科用卻永遠不知所以然
為什麼我們要在意成員初始化?
因為 Java 就像一個緊張兮兮的直升機家長,看到你少寫一個初始值就馬上攔下來:「不可以!你這樣會出事!」它寧願報錯讓你生氣,也不讓你在 runtime 跌個狗吃屎
void f() {
int i;
i++; // Error -- i not initialized
}
像這樣,局部變數沒初始化會直接噴錯,因為 Java 認為你很可能是忘了給值。這不是 bug,這是保母式保護機制
雖然編譯器可以為 i
賦予一個預設值,但未初始化的變數更可能是疏忽,因此強制提供初始值可以幫助我們找出程式中的缺陷
類別成員的預設初始化值
但對類別成員變數(field)就沒那麼兇了。Java 幫你預設好值:
public class InitialValues {
boolean t;
char c;
byte b;
short s;
int i;
long l;
float f;
double d;
InitialValues reference;
void printInitialValues() {
System.out.println("Data type Initial value");
System.out.println("boolean " + t);
System.out.println("char [" + c + "]");
System.out.println("byte " + b);
System.out.println("short " + s);
System.out.println("int " + i);
System.out.println("long " + l);
System.out.println("float " + f);
System.out.println("double " + d);
System.out.println("reference " + reference);
}
public static void main(String[] args) {
InitialValues iv = new InitialValues();
iv.printInitialValues();
/* You could also say:
new InitialValues().printInitialValues();
*/
}
} /* Output:
Data type Initial value
boolean false
char [ ]
byte 0
short 0
int 0
long 0
float 0.0
double 0.0
reference null
*/
這就像你剛搬進一間新房,雖然你什麼都沒帶,系統已經先幫你擺好了最基本的家具——可能是 IKEA 特價款,但總比赤裸裸一間空屋好得多
如果那項家具是一個「物件」參考(reference),Java 不會亂幫你買一個,而是先放個佔位牌子寫著「目前尚未安裝」,也就是 null
。你要是沒注意就對它下指令,比如叫它開燈或倒水(也就是呼叫方法),系統就會爆炸,大聲警告你:這東西根本還沒搬進來!這就是傳說中的 NullPointerException
指定(顯式)初始化:你也可以自己擺家具
你不滿意預設值?可以在定義時直接給值
public class InitialValues2 {
boolean bool = true;
char ch = 'x';
byte b = 47;
short s = 0xff;
int i = 999;
long lng = 1;
float f = 3.14f;
double d = 3.14159;
}
連物件也一樣:
class Depth {}
public class Measurement {
Depth d = new Depth();
// ...
}
但要記得,如果你只是宣告一個物件參考,像是 Depth d;
,而沒有使用 new
來建立實體,那麼 d 裡面其實什麼都沒有,它的值是 null
。此時如果你硬要呼叫 d.someMethod()
,就會出現經典的 NullPointerException
。這就像你寫下了一個地址,但那個房子根本還沒蓋起來,當你按門鈴時,系統會跳出來說:「這裡根本沒房子啊你在按什麼?」
用方法初始化:打開工廠模式
有些初始值不是固定的,就可以用方法來產生:
public class MethodInit {
int i = f();
int f() { return 11; }
}
public class MethodInit2 {
int i = f();
int j = g(i);
int f() { return 11; }
int g(int n) { return n * 10; }
}
public class MethodInit3 {
//! int j = g(i); // Illegal forward reference
int i = f();
int f() { return 11; }
int g(int n) { return n * 10; }
}
注意順序,這段範例其實是在說:你不能在變數還沒被初始化前,就拿它來做進一步的計算。像下面這一行:int j = g(i);
,這時候 i 還沒被設定(因為初始化是照程式碼從上到下執行的),你卻急著把它拿去算 g(i)
,就好比你還沒煮好飯,就打算把它裝進便當盒裡,系統當然會跳出來大叫:「這碗根本沒東西!」
這種錯誤叫做 forward reference,也就是往前引用,Java 編譯器會擋下這種操作,保你一命
建構子初始化:當你需要根據情境設定東西
建構子是在你 new
物件時執行的初始化程式碼。你可以把它想像成買一台新手機第一次開機時,它會問你語言、Wi-Fi、Google 帳號等設定,幫你把該準備的東西一次設好,之後你就可以正常使用。建構子做的事也一樣:在物件正式開始運作之前,先處理好它該有的內部狀態
不過,就算你什麼都不設定,Java 也不會讓你完全裸奔。它會自動幫所有類別的成員變數初始化為預設值,例如整數會被設為 0,布林值為 false,物件參考則是 null
。這就像你開新手機時,雖然還沒連網、沒登入帳號,但系統已經確保不會一打開就是黑畫面當機。Java 幫你鋪好底層,等你來決定要怎麼客製
public class Counter {
int i;
Counter() { i = 7; }
// ...
}
你可以用參數來決定初始化邏輯,這就是建構子最大的彈性。舉例來說,你可能會寫一個 Person
類別,然後在建構子的參數中傳入名字、年齡等資料:
public class Person {
String name;
int age;
Person(String nameInput, int ageInput) {
name = nameInput;
age = ageInput;
}
}
這樣當你 new Person("Alice", 25)
,就能一口氣設定這個人的名字和年齡。建構子提供了一個讓物件「出生時就配好資料」的機會,不用再事後一個個設定欄位。搭配 Java 自動提供的預設值(像 int 是 0,物件參考是 null),即便你沒設值,也不會整個爆炸,這讓 Java 的建構流程又穩又彈性
成員變數的初始化順序:先填表單,再見面
你可能以為建構子裡的程式碼最早執行,但其實在你呼叫 new
建立物件時,Java 是照著「先初始化所有成員變數,再跑建構子」這個劇本來進行。也就是說,建構子只是壓軸登場,成員變數早就在你喊 Action 之前就偷偷上台了
這段範例展示了這個順序:
// When the constructor is called to create a
// Window object, you'll see a message:
class Window {
Window(int marker) { System.out.println("Window(" + marker + ")"); }
}
class House {
Window w1 = new Window(1); // Before constructor
House() {
// Show that we're in the constructor:
print("House()");
w3 = new Window(33); // Reinitialize w3
}
Window w2 = new Window(2); // After constructor
void f() { print("f()"); }
Window w3 = new Window(3); // At end
}
public class OrderOfInitialization {
public static void main(String[] args) {
House h = new House();
h.f(); // Shows that construction is done
}
} /* Output:
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()
*/
那這個順序到底代表什麼?
你可以想像你要開一場重要的簡報會議,進會議室前,助理(也就是成員變數的初始化區塊)已經幫你把講義印好、PPT打開、茶水準備好。等你(建構子)一走進會議室,只要再做些個人風格微調就能直接開講。甚至你也可以把某些資料重新再準備一次(像範例裡 w3 被重新賦值為 new Window(33)
)
簡單來說:Java 一律先照成員變數定義順序初始化,再跑建構子。這個規則讓程式行為更可預測,不會因為一個變數擺錯位置就產生意料之外的 bug
如果你在建構子內部又對某個變數賦值,會直接蓋掉原本的初始化,務必要小心避免不必要的重工或邏輯錯誤
static 的世界:一間公司只需要一個會計部
到目前為止,我們講的成員變數初始化,都跟「每個物件有自己的版本」有關。但有時候,我們只需要一份共用的資料,不需要每個物件都複製一份,這時候 static 就出場了
你可以把 static 想成公司裡的會計:不管你公司有幾百個員工,他們都共用一組會計系統(static 資料),不會一人一份(那也太浪費)
在 Java 裡,static 成員變數屬於「類別」本身,不屬於任何「實例」。換句話說,只要類別被載入一次,這些 static 資料就初始化一次,之後不管你建立幾個物件,都會共用那份資料
來看個例子,並解釋一下到底發生了什麼:
class Bowl {
Bowl(int marker) {
print("Bowl(" + marker + ")");
}
void f1(int marker) {
print("f1(" + marker + ")");
}
}
class Table {
static Bowl bowl1 = new Bowl(1);
Table() {
System.out.println("Table()");
bowl2.f1(1);
}
void f2(int marker) {
System.out.println("f2(" + marker + ")");
}
static Bowl bowl2 = new Bowl(2);
}
class Cupboard {
Bowl bowl3 = new Bowl(3);
static Bowl bowl4 = new Bowl(4);
Cupboard() {
System.out.println("Cupboard()");
bowl4.f1(2);
}
void f3(int marker) {
System.out.println("f3(" + marker + ")");
}
static Bowl bowl5 = new Bowl(5);
}
public class StaticInitialization {
public static void main(String[] args) {
System.out.println("Creating new Cupboard() in main");
new Cupboard();
System.out.println("Creating new Cupboard() in main");
new Cupboard();
table.f2(1);
cupboard.f3(1);
}
static Table table = new Table();
static Cupboard cupboard = new Cupboard();
} /* Output:
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)
*/
這段程式碼展示了 static 與非 static 的初始化順序,並透過 print()
來觀察各種變數何時被初始化:
Table
和Cupboard
類別中都定義了 static 和非 static 的Bowl
實例- static 的變數會在類別第一次被載入時就初始化一次
- 非 static 的變數則會在每次物件被建立時重新初始化
在 main()
的最一開始,先定義了
static Table table = new Table();
static Cupboard cupboard = new Cupboard();
這段其實是靜態初始化區塊的一部分,會在 main()
執行前就先跑
這會導致:
- Table 的
bowl1
和bowl2
被初始化 → 分別印出Bowl(1)
和Bowl(2)
- 建構子跑起來 → 印出
Table()
- 呼叫
bowl2.f1(1)
→ 印出f1(1)
接著是 Cupboard 的 static:
bowl4
和bowl5
初始化(static) → 印出Bowl(4)
和Bowl(5)
- 然後建構 Cupboard 的物件 → 初始化
bowl3
(非 static)→ 印出Bowl(3)
- 建構子本體執行 → 印出
Cupboard()
,再呼叫bowl4.f1(2)
→ 印出f1(2)
最後 main()
開始跑:
- 印出
Creating new Cupboard()
,然後 new 出一個新的Cupboard()
:又會執行非 static 的bowl3
初始化 → 印Bowl(3)
,接著建構子跑完
這整段程式的重點是:
- static 的變數只初始化一次(類別第一次被載入時)
- 非 static 的變數會在每次物件建立時被初始化一次
這種行為特別重要,當你希望某些資源全程共用時(例如資料庫連線、常數、設定檔),就該放 static。 反之,屬於物件自己的資料(像是每個人有不同的名字)就不該用 static,不然就變成大家共用同一個名字了,非常詭異
額外補充:類別載入與 .class 的真相
在上一段討論 static 的初始化順序時,還有一個容易被忽略但非常關鍵的概念:Java 不是一開始就把所有 .class
檔都載入進記憶體,而是「有用到才載入」。這叫做延遲載入(lazy loading)
假設我們有一個類別:
class Dog {
static int count = 0;
Dog() {
count++;
}
}
當你執行程式但完全沒用到 Dog
類別時(沒 new,也沒引用 Dog.count
),這個類別根本不會被載入。這意味著:
Dog.class
檔案還在硬碟上等著你叫它- 它裡面的 static 初始化程式碼也完全沒被執行
一旦你呼叫 new Dog()
,或是存取了 Dog.count
,Java 才會:
- 去找
.class
檔 - 載入它
- 執行所有 static 初始化
- 然後才開始 new 或執行你要的程式邏輯
這也說明為什麼 static 初始化「只會執行一次」——因為類別只會載入一次
所以如果你看到某些 static 變數或 static block 沒有執行,請先確認這個類別有沒有被真正用到
這個機制是為了效能與記憶體管理設計的,不會一開始就把整個世界灌進 JVM,只有「被叫到的才進場」。這點對初學者來說很容易被忽略,但理解這件事會讓你在日後處理大型專案、除錯載入行為時更有底氣
顯式的靜態初始化
延續上面對 .class
載入機制的說明,Java 還提供一個特殊的寫法:static 區塊(static block),讓你在類別被載入的時候執行一段一次性的靜態初始化邏輯
這種 static block 非常適合用來做「組合式的初始化」——當你有一堆 static 欄位需要依賴其他欄位或做邏輯處理時,就可以放進 static 區塊裡
來看一個最簡單的範例:
public class Spoon {
static int i;
static {
i = 47;
}
}
看起來像是方法,但其實不是。這段 static block 的意思是:「當 Spoon
類別被載入時,把 i
設成 47。」
也就是說,只要你呼叫 Spoon.i
或 new 出一個 Spoon
物件,這段 static block 就會自動執行一次。不會重複、不會失憶——正好一次
這種方式特別適合用在:
- 初始化需要一點小邏輯的 static 資料(不能直接指定值)
- 一開始要跑某些與靜態變數有關的程式(例如 log 設定、計算常數等)
簡單來說,當你需要「一進場就幹一件事,然後全場都知道」的情境,static block 就是你的武器
來看這段程式碼:
class Cup {
Cup(int marker) {
System.out.println("Cup(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
class Cups {
static Cup cup1;
static Cup cup2;
static {
cup1 = new Cup(1);
cup2 = new Cup(2);
}
Cups() {
System.out.println("Cups()");
}
}
public class ExplicitStatic {
public static void main(String[] args) {
System.out.println("Inside main()");
Cups.cup1.f(99); // (1)
}
// static Cups cups1 = new Cups(); // (2)
// static Cups cups2 = new Cups(); // (2)
} /* Output:
Inside main()
Cup(1)
Cup(2)
f(99)
*/
- 類別
Cups
裡面有一個 static 區塊,裡面初始化了兩個靜態變數cup1
和cup2
- 當
main()
裡第一次「使用」到Cups
類別(這裡是Cups.cup1.f(99)
)時,Java 會馬上去把Cups.class
載入進來 - 一旦載入,就會執行 static 區塊
- 所以你會看到
Cup(1)
和Cup(2)
被印出來 - 然後
f(99)
被呼叫
這就像一間日式餐廳(Cups
類別)早上開門營業的準備流程(static block):第一次有人打開門(使用這個類別)時,店員會一次性開燈、煮水、鋪好榻榻米、設定背景音樂。這些準備工作只做一次,不管之後有幾組客人進來(也就是 new 幾個物件),這些設定都已經就緒,不會每次重頭來過。這就是 static block 的概念——只要類別第一次被使用,Java 就會幫你完成所有靜態資源的初始化,後續的使用者就能享受一個早就備妥的環境
你雖然在main()
裡什麼都沒寫new Cups()
,但只要你有用到Cups
的 static 變數,Java 就會載入整個類別,並執行它的 static block。這是理解 static 區塊的關鍵
這個機制非常適合拿來做一些「只要一次」的初始化,例如載入設定檔、建立資料庫連線池、或是 log 設定等情境
非靜態實例初始化:建構子外掛
有時候你會寫一個類別裡面有兩三個建構子,結果每一個建構子裡都要重複貼上初始化程式碼。久而久之就變得很容易出錯或難維護。這時候就可以使用 Java 提供的一個機制:非靜態初始化區塊(instance initializer block)
這個區塊會在每次你 new 一個物件的時候都執行,而且它的執行順序比建構子還早,但在成員變數初始化之後。你可以把它想像成是「建構子的前菜」,不管你叫哪一道主菜(哪個建構子),這道前菜都會先上
class Mug {
Mug(int marker) {
System.out.println("Mug(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
public class Mugs {
Mug mug1;
Mug mug2;
{
mug1 = new Mug(1);
mug2 = new Mug(2);
print("mug1 & mug2 initialized");
}
Mugs() {
System.out.println("Mugs()");
}
Mugs(int i) {
System.out.println("Mugs(int)");
}
public static void main(String[] args) {
System.out.println("Inside main()");
new Mugs();
System.out.println("new Mugs() completed");
new Mugs(1);
System.out.println("new Mugs(1) completed");
}
} /* Output:
Inside main()
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs()
new Mugs() completed
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs(int)
new Mugs(1) completed
*/
你會發現,不管呼叫哪個建構子,初始化區塊都會在物件建立過程中跑一次,把 mug1
和 mug2
都準備好
所以它的用處是什麼?
這種語法非常適合:
- 你有多個建構子,但都需要共用某段初始化邏輯
- 你不想在每個建構子裡都貼上重複的程式碼
- 你想要確保某些初始化「永遠都會在建構子之前」執行
想像你是一個組裝模型的工廠,無論你選的是哪個版本的模型(建構子),工廠都會先準備兩個標準零件(mug1 和 mug2)。這樣你就可以專注在後面組裝的細節,不用一直重做一樣的前置準備
總之,如果你討厭複製貼上,這段語法值得學起來
而且這種非靜態初始化區塊在匿名內部類別(anonymous inner class)裡特別重要。什麼是匿名內部類別呢? 它就是沒有名字、現場直接定義的小型類別,常常會在你需要快速實作一個介面或繼承類別時出現
例如:
Button b = new Button();
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// 這裡就是匿名內部類別
}
});
由於匿名類別不能有自己的建構子,你就只能靠非靜態初始化區塊來做事情。這樣不論你用什麼方式產生它,初始化邏輯都還是能被正確執行,這讓它在某些需要「微客製但懶得命名」的場景中非常實用
結語:這些初始化不是語法糖,而是設計哲學
從預設值到建構子、從 static block 到 instance block,這整套初始化機制不是 Java 在耍帥,也不是單純讓語法變「短一點」。它背後藏的是一種哲學:讓程式的行為更可預期、讓資源管理更有效率、讓開發者能專注在該做的事上
Java 的這些「看似自動」的行為,其實都建立在一套明確的時序與原則上——你可以不去干涉它,但你最好理解它。因為唯有知道哪些事情「會自動發生」、哪些「要你自己做」,你才不會寫出執行順序不如預期的code,還在懷疑是不是乖乖過期了
希望你看完這篇,能理解為什麼一個物件能在你 new 出來時就一切就緒;為什麼你沒用到的類別根本沒被載入;為什麼 static 的東西只做一次;為什麼匿名類別還是能初始化。這些不是巧合,而是工程設計
當你寫 Java 的時候,請記得:它不是在幫你偷懶,它是在替你設好每一張椅子、調整每一盞燈,等你上場
Comments ()