1. 引言

在方法调用中,参数传递主要有两种方式:值传递(Pass-by-Value)引用传递(Pass-by-Reference)。不同编程语言对这两种机制的实现各不相同。⚠️但需要明确一点:Java 中所有参数传递都是严格意义上的值传递

本文将深入剖析 Java 在处理不同类型参数时的具体行为,帮你彻底搞懂“为什么对象能被修改,基本类型却不能”,避免日常开发中踩坑。


2. 值传递 vs 引用传递

常见的参数传递机制有以下几种:

  • 值传递(Pass-by-Value)
  • 引用传递(Pass-by-Reference)
  • 结果传递(Pass-by-Result)
  • 值-结果传递(Pass-by-Value-Result)
  • 名称传递(Pass-by-Name)

现代主流语言中最常见的是前两种。我们重点对比前两者:

2.1 值传递(Pass-by-Value)

✅ 调用方和被调用方法操作的是两个独立变量,彼此是副本关系。
❌ 对形参的任何修改,不会影响原始变量。

换句话说:传的是实参的“拷贝”。即使你在方法内部改得天花乱坠,原变量纹丝不动。

2.2 引用传递(Pass-by-Reference)

✅ 调用方和被调用方法操作的是同一个变量。
✅ 方法内对参数的修改会直接反映到原始变量上。

本质上,传递的是变量的内存地址(或句柄),双方指向同一块数据区域。

📌 注意:Java 并不支持这种模式。别被某些“Java 是引用传递”的错误说法误导了!


3. Java 中的参数传递机制

Java 的核心概念是 值(value)引用(reference)

  • 基本类型变量:直接存储值,放在栈上
  • 引用类型变量:存储的是对象的引用(即地址),也放在栈上;实际对象在堆中

⚠️关键点来了:Java 所有参数传递都是值传递
也就是说,无论是基本类型还是引用类型,传的都是“拷贝”:

  • 基本类型:拷贝的是值本身
  • 引用类型:拷贝的是“引用”(即地址),不是对象本身

这个“引用的拷贝”仍然指向堆中同一个对象,所以你可以在方法里修改对象内容 —— 但这不等于“引用传递”!

下面我们用代码实例来验证。


3.1 基本类型的值传递

Java 有 8 种基本类型(int, boolean, double 等)。它们的变量直接存值在栈上。

传参时,JVM 会在栈上为形参分配新空间,把实参的值复制过去。方法结束后,形参随栈帧销毁。

来看个简单例子:

public class PrimitivesUnitTest {
 
    @Test
    public void whenModifyingPrimitives_thenOriginalValuesNotModified() {
        
        int x = 1;
        int y = 2;
       
        // 修改前
        assertEquals(x, 1);
        assertEquals(y, 2);
        
        modify(x, y);
        
        // 修改后,原始值不变
        assertEquals(x, 1);
        assertEquals(y, 2);
    }
    
    public static void modify(int x1, int y1) {
        x1 = 5;
        y1 = 10;
    }
}

📌 内存过程解析:

  1. xy 在栈上创建,值为 1 和 2
  2. 调用 modify() 时,x1y1xy 的副本,也位于栈上但地址不同
  3. modify() 内修改 x1y1,只影响副本,不影响原始变量

baeldung - pass by value - passing primitives


3.2 引用类型的“值传递”

对象本身存储在堆中,栈上只存一个指向它的引用变量。

当传递对象时,传的是“引用的副本” —— 两个引用(实参和形参)指向堆中同一个对象。

所以:

  • ✅ 修改对象内部状态(如字段) → 原对象可见
  • ❌ 给形参重新赋值(指向新对象) → 不影响原引用

来看代码:

public class NonPrimitivesUnitTest {
 
    @Test
    public void whenModifyingObjects_thenOriginalObjectChanged() {
        Foo a = new Foo(1);
        Foo b = new Foo(1);

        // 修改前
        assertEquals(a.num, 1);
        assertEquals(b.num, 1);
        
        modify(a, b);
        
        // 修改后:a.num 变了,b.num 没变
        assertEquals(a.num, 2);
        assertEquals(b.num, 1);
    }
 
    public static void modify(Foo a1, Foo b1) {
        a1.num++;           // 修改对象内容 → 原对象受影响
       
        b1 = new Foo(1);    // b1 指向新对象 → 原 b 不受影响
        b1.num++;
    }
}
 
class Foo {
    public int num;
   
    public Foo(int num) {
        this.num = num;
    }
}

📌 内存演变过程:

初始状态ab 分别指向堆中两个独立的 Foo 实例

baeldung - pass by value - passing primitives - initial

调用 modify() 后a1b1ab 的引用副本,仍指向原对象

baeldung - pass by value - passing primitives - before method ca

modify() 执行中

  • a1.num++ → 修改的是 a 所指对象,所以 a.num 变成 2
  • b1 = new Foo(1)b1 指向新对象,与原 b 脱离关系

baeldung - pass by value - passing primitives - after method cal

💡 简单粗暴总结:
“Java 是值传递” + “引用也是值” = 你传的是“引用的拷贝”
所以能改内容,但改不了“谁指向谁”


4. 结论

Java 的参数传递机制统一且简单:永远是值传递

区别在于传递的“值”是什么:

类型 传递的内容 能否修改原始变量 能否修改对象内容
基本类型 值的拷贝
引用类型 引用的拷贝(地址)

✅ 正确认知:
“对象能被修改” 不是因为“引用传递”,而是因为“多个引用指向同一个堆对象”。

⚠️ 常见误区:
不要说“Java 基本类型是值传递,对象是引用传递” —— 这是错误的!统一都是值传递,只是值的类型不同。

文中所有代码示例均可在 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-lang-oop-others


» 下一篇: Logback 使用指南