「Java 只有值傳遞」是面試經典,但「引用類型傳遞的是引用的值」才是完整真相。本文通過代碼實證、內存模型圖解、JVM 字節碼視角,徹底講清引用傳值機制,避開「以為能修改對象實則修改失效」的坑。
public void testString() {
String original = "原來的值"; // 原始引用指向堆中 "原來的值" 對象
modifyString(original); // 傳遞引用副本
System.out.println(original); // 輸出: 原來的值
}
public void modifyString(String param) { // param = original 的副本,指向同一對象
param = "改變的值"; // param 指向新對象 "改變的值",original 不受影響
// 此時內存: original → "原來的值", param → "改變的值"
}
關鍵:`String` 不可變,任何「修改」實為創建新對象。賦值操作 `param = ...` 僅改變局部變量 `param` 指向,斷開與原對象關聯。
public void testStringBuffer() {
StringBuffer sb = new StringBuffer(); // sb 指向堆中 StringBuffer 對象
sb.append("原來的值");
modifyBuffer(sb); // 傳遞引用副本
System.out.println(sb.toString()); // 輸出: 原來的值改變的值
}
public void modifyBuffer(StringBuffer param) { // param = sb 的副本,指向同一對象
param.append("改變的值"); // 通過副本調用方法,**修改堆中共享對象內部狀態**
// param = new StringBuffer(); // 若取消註釋此行:param 指向新對象,後續操作不影響 sb
}
關鍵:`param.append()` 是通過引用副本訪問**同一堆對象**並修改其內部字節數組,外部 `sb` 可見變更。但 `param = new ...` 會切斷關聯。
public void swap(Object a, Object b) {
Object temp = a;
a = b;
b = temp;
// 僅交換了局部副本,調用者引用完全不變
}
// 方法調用前棧幀局部變量表
LocalVariableTable:
0: original -> ref#1001 (指向堆對象 "原來的值")
// 方法調用時:執行 invokespecial/invokevirtual
// 參數傳遞 = 將 ref#1001 複製到被調用方法棧幀的局部變量槽 0
// 兩個槽位存儲相同的引用值 (地址),指向同一堆對象
// 方法內 param = "新值" (ldc + astore_0)
// 僅修改槽位 0 存儲的引用值為新對象地址 ref#2002
// 原調用者槽位仍存 ref#1001,完全無影響
// C++
void modify(std::string& ref) { // 參數是別名,綁定調用者變量本身
ref = "新值"; // 直接修改調用者的變量
}
std::string s = "舊值";
modify(s); // s 變為 "新值"
Java 無此機制。若需「輸出參數」效果,常用方案:
基本類型: 值 ──────► 副本值 (獨立內存) ──修改→ 不影響原值
引用類型: 引用值(地址) ──► 副本引用值(同地址)
│
├─ 調用方法/訪問字段 ──► 修改【堆中共享對象】 ──► 外部可見 ✅
│
└─ 賦值新對象 (param = new ...) ──► 修改【局部槽位引用值】 ──► 外部不可見 ❌
理解「引用值副本」與「堆中對象」的區別,是寫出正確、可維護 Java 代碼的分水嶺。下次面試或代碼評審遇到參數傳遞爭議,拿這張圖說話。