baeldung 2024-01-08
1. 引言
向方法传递参数最常见的两种方式是“值传递(Pass-by-Value)”和“引用传递(Pass-by-Reference)”。不同的编程语言以不同的方式使用这些概念。就 Java 而言,所有参数传递都严格采用值传递机制。
在本教程中,我们将通过示例说明 Java 在处理不同类型参数时是如何进行传递的。
2. 值传递 vs 引用传递
让我们先了解一些常见的参数传递机制:
- 值传递(value)
- 引用传递(reference)
- 结果传递(result)
- 值-结果传递(value-result)
- 名称传递(name)
在现代编程语言中,最常用的两种机制是“值传递”和“引用传递”。在继续之前,我们先分别讨论这两种机制:
2.1 值传递(Pass-by-Value)
当参数采用值传递方式时,调用方(caller)和被调用方(callee)操作的是两个彼此独立的变量,它们互为副本。对其中一个变量的任何修改都不会影响另一个变量。
这意味着,在调用方法时,传递给被调用方法的参数是原始参数的克隆。被调用方法中对参数所做的任何修改,都不会影响调用方法中的原始参数。
2.2 引用传递(Pass-by-Reference)
当参数采用引用传递方式时,调用方和被调用方操作的是同一个对象。
这意味着,当变量以引用方式传递时,会将该对象的唯一标识符(即内存地址)发送给方法。对参数实例成员的任何修改,都会直接反映到原始对象上。
3. Java 中的参数传递机制
任何编程语言的基本概念都离不开“值”和“引用”。在 Java 中:
- 基本类型(Primitive)变量直接存储实际的值;
- **非基本类型(Non-Primitive,即对象)**变量存储的是引用,这些引用指向堆内存中对象的实际地址。
Java 中的所有参数都是按值传递的。在方法调用期间,每个参数(无论是基本类型的值还是对象的引用)都会在栈内存中创建一个副本,并将该副本传递给方法。
- 对于基本类型:其值会被直接复制到栈内存中,然后传递给被调用方法;
- 对于非基本类型(对象):栈中存储的是指向堆中对象的引用。当传递对象时,会复制该引用(即复制栈中的引用值),并将这个新的引用传递给方法。新旧引用都指向堆中的同一个对象。
接下来,我们通过代码示例来具体说明。
3.1 传递基本类型
Java 语言定义了八种基本数据类型。基本类型变量的值直接存储在栈内存中。当以基本类型变量作为参数传递时,实际参数的值会被复制到形式参数中,这些形式参数在栈内存中拥有自己独立的空间。
这些形式参数的生命周期仅限于方法执行期间;方法返回后,这些参数就会从栈中清除并丢弃。
让我们通过以下代码示例来理解:
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;
}
}
让我们通过分析内存中的存储方式来理解上述断言:
- 主方法中的变量
x和y是基本类型,其值直接存储在栈内存中; - 调用
modify()方法时,会为这两个变量分别创建精确的副本,并存储在栈内存的不同位置; - 对这些副本的任何修改仅影响副本本身,不会改变原始变量。
3.2 传递对象引用
在 Java 中,所有对象在底层都动态存储在堆(Heap)空间中,并通过引用变量进行访问。
与基本类型不同,Java 对象的存储分为两个部分:
- 引用变量存储在栈内存中;
- 实际对象存储在堆内存中。
当对象作为参数传递时,会创建该引用变量的一个精确副本,这个副本仍然指向堆中原来的同一个对象。
因此,如果在方法内部对对象的内容进行修改,这种修改会反映到原始对象上。但是,如果在方法内部将新的对象赋值给传入的引用变量,则不会影响原始对象。
下面通过代码示例来说明:
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);
// 修改后
assertEquals(a.num, 2);
assertEquals(b.num, 1);
}
public static void modify(Foo a1, Foo b1) {
a1.num++; // 修改原对象的内容
b1 = new Foo(1); // b1 现在指向一个新对象
b1.num++;
}
}
class Foo {
public int num;
public Foo(int num) {
this.num = num;
}
}
我们来分析上述程序中的断言:
- 初始时,对象
a和b都具有num = 1,它们的引用分别指向堆中两个不同的对象;
在本文中,我们探讨了 Java 在处理基本类型和非基本类型(对象)参数时的传递机制。
我们了解到:Java 中的参数传递始终是值传递。但根据参数类型的不同,其表现形式有所差异:
- 对于基本类型:传递的是值的副本;
- 对于对象类型:传递的是对象引用的副本(即“引用的值”被复制),因此方法内可以修改原对象的内容,但不能改变原始引用所指向的对象本身。
简而言之:
Java 是值传递,但对于对象,传递的是引用的值(即地址的副本),而不是对象本身。