「Java 是值傳遞還是引用傳遞?」這個問題困擾了無數開發者,甚至引發激烈筆戰。本文從 JVM 內存模型、語言規範權威定義、字節碼執行、實戰驗證四個維度,給出鐵板釘釘的結論,並澄清常見誤區,徹底終結這場爭論。
《Java 核心技術 卷 I》Horstmann 原文:
「Java 程式設計語言總是採用值調用。也就是說,方法得到的是所有參數值的一個拷貝,特別是,方法不能修改傳遞給它的任何參數變量的內容。」
Java Language Specification (JLS) §8.4.1:
「當方法被調用時,每個參數變量被創建並初始化為對應實參表達式的值。」
結論:Java 嚴格採用按值傳遞。無例外、無特殊情況、無「對象引用傳遞」這一說法。
因為引用本身也是值。當傳遞對象時,拷貝的是引用的值(記憶體地址),而非對象本身。這導致:
// 實參在呼叫者棧幀
StringBuilder sb = new StringBuilder("hello"); // sb 在棧,對象在堆
// 被調用者棧幀創建參數變量
void appendExclamation(StringBuilder param) {
param.append("!"); // 修改堆對象內容 → 可見
param = new StringBuilder("world"); // 修改參數變量指向 → 僅影響局部變量
}
圖解:
sb → [heap: "hello"],param 未存在param = sb(拷貝引用值),雙雙指向同一堆對象append("!"):堆對象變為 "hello!",雙雙可見param = new ...:param 指向新對象,sb 仍指向舊對象param 隨棧幀銷毀,sb 不變int a = 10;
modify(a);
System.out.println(a); // 10,不變
void modify(int x) { x = 20; }
User u = new User("Alice");
changeName(u);
System.out.println(u.getName()); // "Bob",變了!
void changeName(User user) {
user.setName("Bob"); // 修改堆對象欄位
}
User u = new User("Alice");
replace(u);
System.out.println(u.getName()); // "Alice",沒變!
void replace(User user) {
user = new User("Bob"); // 只改參數指向
}
String s = "hello";
modifyString(s);
System.out.println(s); // "hello",不可變對象無法修改內容
void modifyString(String str) {
str = str + " world"; // 產生新字串,str 指向新對象
}
| 誤區 | 真相 |
|---|---|
| 「對象是引用傳遞,基本類型是值傳遞」 | 錯。統統是值傳遞,對象傳的是引用的值。 |
| 「能修改對象內容就是引用傳遞」 | 錯。修改的是共享堆對象,而非參數變量本身。引用傳遞定義是「可修改實參變量指向」。 |
| 「String 是特殊的值傳遞」 | 錯。String 同樣傳引用值,只是不可變導致無法修改內容,重新賦值同樣只影響局部參數。 |
| 「Swap 寫不出來證明是引用傳遞」 | 錯。Swap 無法實現正是值傳遞的鐵證:無法修改實參變量指向。 |
new ArrayList<>(list))或用不可變視圖(Collections.unmodifiableList())。Java 永遠是值傳遞。傳對象時,拷貝的是引用(地址)這個值。方法可修改共享對象內容,但絕不能改變調用方變量的指向。
理解這一點,不僅能終結爭論,更能指導你寫出語義清晰、副作用可控、並發安全的 Java 代碼。