核心結論:Java 只有「值傳遞」
Java 中方法參數傳遞永遠是「值傳遞」。但「值」的含義因類型不同而異:
- 基本類型:傳遞的是**數值本身的副本**。方法內修改形參,**不影響**實參。
- 引用類型:傳遞的是**對象引用 (地址) 的副本**。方法內:
- 修改形參指向 (如 `param = new Object()`):**不影響**實參指向。
- 通過形參調用方法修改對象內部狀態 (如 `param.setName("new")`):**影響**實參指向的同一對象。
三個問題逐一解答
1. 實參會隨著形參改變而改變嗎?
- 基本類型:**不會**。形參是副本,修改獨立。
- 引用類型:
- 若方法內**重新賦值形參** (`param = new Obj()`):**不會**,實參仍指向原對象。
- 若方法內**調用形參方法修改對象內容** (`param.setX(1)`):**會**,實參指向的對象被修改。
2. 多線程中實參改變,線程內形參變嗎?
**同理**。關鍵在於「改變」的含義:
- 主線程將實參引用指向**新對象** (`ref = new Obj()`):子線程形參仍指向**舊對象** (引用副本未變)。
- 主線程調用實參方法**修改對象內容** (`ref.modify()`):子線程形參指向**同一對象**,可見修改 (需考慮線程安全:`volatile`、`synchronized`、不可變對象)。
3. 基本類型 vs 非基本類型 (引用類型) 一樣嗎?
**不一樣**,核心差異如上。補充說明:
- String 是特殊的引用類型:不可變。任何「修改」操作 (`str = str + "a"`, `str.replace()`) 實際都是創建**新 String 對象**,原對象不變。因此傳遞 String 給方法,方法內無法修改原字符串對象,也無法讓實參指向新字符串 (除非返回新值並重新賦值)。
- 包裝類 同 String,不可變。
- 數組、集合、自定義類** 可變,方法內可修改內容影響實參。
代碼驗證
public class ParamTest {
// 基本類型
static void primitive(int x) { x = 20; }
// 引用類型 - 重新賦值
static void refReassign(StringBuilder sb) { sb = new StringBuilder("new"); }
// 引用類型 - 修改內容
static void refModify(StringBuilder sb) { sb.append(" modified"); }
// String (不可變引用)
static void stringTest(String s) { s = "changed"; }
public static void main(String[] args) {
int a = 10; primitive(a); System.out.println(a); // 10 不變
StringBuilder b = new StringBuilder("old");
refReassign(b); System.out.println(b); // "old" 不變
StringBuilder c = new StringBuilder("old");
refModify(c); System.out.println(c); // "old modified" 變了!
String d = "hello"; stringTest(d); System.out.println(d); // "hello" 不變
}
}
多線程場景最佳實踐
- **不可變對象**:傳遞 `final` 引用指向不可變對象 (String、包裝類、Guava ImmutableList、自定義不可變類),天然線程安全。
- **可變對象 + 線程封閉**:對象僅由單線程訪問,或使用 `ThreadLocal` 隔離副本。
- **可變對象 + 同步**:加鎖 (`synchronized`、`ReentrantLock`)、使用並發容器 (`ConcurrentHashMap`)、原子類 (`AtomicInteger`)。
- **防御性拷貝**:方法邊界進出時拷貝可變對象 (`new ArrayList<>(list)`、`obj.clone()`),切斷共享引用。