java 参数传递
首先,记住最关键的结论,这能帮你理解一切相关讨论:
Java中只有按值传递,没有按引用传递。
但是,这个“值”分为两种情况:
- 对于基本数据类型(如
int,double,char,boolean等),传递的是变量存储的实际数值的副本。 - 对于引用数据类型(如
Object, 数组,String等),传递的是对象引用的副本(你可以把引用理解成对象的“地址”或“遥控器”)。
下面我们通过代码和图示来彻底理解这两种情况。
1. 基本数据类型的参数传递
当传递基本数据类型时,方法接收到的是原值的一个拷贝。在方法内部对这个参数的任何修改,都只影响这个拷贝,而不会影响原始的变量。
public class ParameterPassing {
public static void main(String[] args) {
int x = 10;
int y = 20;
System.out.println("交换前: x = " + x + ", y = " + y); // 输出:x = 10, y = 20
// 尝试调用swap方法交换x和y的值
swap(x, y);
System.out.println("交换后: x = " + x + ", y = " + y); // 输出:x = 10, y = 20 (未改变!)
}
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("swap方法内: a = " + a + ", b = " + b); // 输出:a = 20, b = 10
}
}
内存模型与过程分析:
-
main方法开始:在栈内存中,为
x和y分配空间,并存储值 10 和 20。[栈内存] main的栈帧: x -> 10 y -> 20 -
调用swap(x, y):
- Java将
x和y的值(10和20)拷贝一份,传递给swap方法的形参a和b。 - 此时,在栈上为
swap方法创建新的栈帧,里面有a,b,temp。
[栈内存] swap的栈帧: a -> 10 (x的拷贝) b -> 20 (y的拷贝) temp -> ? --- main的栈帧: x -> 10 y -> 20 - Java将
-
执行swap方法体:
temp = a;-> temp 变为 10。a = b;-> a 变为 20。b = temp;-> b 变为 10。- 此时,
swap栈帧中的a和b成功交换。
[栈内存] swap的栈帧: a -> 20 b -> 10 temp -> 10 --- main的栈帧: x -> 10 (未变!) y -> 20 (未变!) -
swap方法结束:
swap的栈帧被销毁,其中的a,b,temp全部消失。程序回到main方法,x和y的值没有任何变化。
结论:对于基本类型,方法内对参数的修改,影响的是副本,与原变量无关。
2. 引用数据类型的参数传递
这是容易让人误解为“按引用传递”的地方。我们传递的是引用的副本,而不是对象本身。
情况一:通过引用副本修改对象状态
这种情况让人感觉像是“按引用传递”,因为方法内部对对象的修改影响了原对象。
class Person {
String name;
public Person(String name) {
this.name = name;
}
}
public class ParameterPassing {
public static void main(String[] args) {
Person person = new Person("Alice");
System.out.println("修改前: " + person.name); // 输出:Alice
// 调用方法修改person对象的name
changeName(person);
System.out.println("修改后: " + person.name); // 输出:Bob (改变了!)
}
public static void changeName(Person p) {
p.name = "Bob"; // 通过引用p,修改了它指向的对象的属性
}
}
内存模型与过程分析:
-
main方法开始:
person是一个引用变量,它在栈中存储了一个地址(比如0x100)。- 这个地址指向堆内存中一个
Person对象,其name为 “Alice”。
[栈内存] [堆内存] main的栈帧: person -> 0x100 ---> 0x100: Person对象 { name: "Alice" } -
调用changeName(person):
- Java将
person变量存储的值(地址0x100)拷贝一份,传递给changeName方法的形参p。 - 现在,
person和p这两个不同的引用变量,存储着相同的地址,指向同一个对象。
[栈内存] [堆内存] changeName的栈帧: p -> 0x100 ---> --- | main的栈帧: | person -> 0x100 ---> 0x100: Person对象 { name: "Alice" } - Java将
-
执行changeName方法体:
p.name = "Bob";意味着:“找到p指向的那个对象(地址0x100),并将其name属性改为 “Bob””。- 由于
person和p指向的是同一个对象,所以通过p修改对象,person看到的自然也是修改后的结果。
[栈内存] [堆内存] changeName的栈帧: p -> 0x100 ---> --- | main的栈帧: | person -> 0x100 ---> 0x100: Person对象 { name: "Bob" } <-- 对象被修改了 -
changeName方法结束:
p随着栈帧销毁而消失,但person仍然指向那个已经被修改的对象。
结论:方法可以通过传入的引用副本,修改其所指向的对象的状态。
情况二:试图改变引用本身(重新赋值)
这种情况证明了它是“按值传递”,因为方法内部无法让外部的原始引用指向一个新的对象。
public class ParameterPassing {
public static void main(String[] args) {
Person person = new Person("Alice");
System.out.println("重赋值前: " + person.name); // 输出:Alice
// 尝试让person指向一个新的对象
reassignPerson(person);
System.out.println("重赋值后: " + person.name); // 输出:Alice (未改变!)
}
public static void reassignPerson(Person p) {
// 这里让p指向了一个全新的对象
p = new Person("Bob");
System.out.println("reassignPerson方法内: " + p.name); // 输出:Bob
}
}
内存模型与过程分析:
-
main方法开始和调用reassignPerson(person) 与之前类似:
[栈内存] [堆内存] reassignPerson的栈帧: p -> 0x100 ---> --- | main的栈帧: | person -> 0x100 ---> 0x100: Person对象 { name: "Alice" } -
执行reassignPerson方法体:
p = new Person("Bob");这行代码创建了一个新的Person对象(假设在地址0x200)。- 它将形参
p的值从 0x100 改为 0x200。 - 注意:这改变的是
p这个拷贝的引用,而main方法中的person引用仍然忠诚地指向 0x100。
[栈内存] [堆内存] reassignPerson的栈帧: p -> 0x200 ---> 0x200: Person对象 { name: "Bob" } (新对象) --- main的栈帧: person -> 0x100 ---> 0x100: Person对象 { name: "Alice" } (原对象,未受影响) -
reassignPerson方法结束:
p和它指向的新对象(0x200)之间的连接断开(如果该对象没有其他引用,稍后会被GC回收)。main方法中的person毫无变化。
结论:方法内部无法通过修改引用的副本来改变原始引用变量的指向。这有力地证明了是“按值传递”(传递的是引用的值)。
关于String的特殊说明
String 是一个特殊的引用类型,因为它具有不可变性。任何对String的修改(如 concat, +, replace)都会创建一个全新的String对象。
public static void changeString(String str) {
str = "新的内容"; // 这相当于 str = new String("新的内容");
}
public static void main(String[] args) {
String s = "原始内容";
changeString(s);
System.out.println(s); // 输出: "原始内容"
}
这个行为与上面的“情况二”完全相同。str = "新的内容"; 只是让方法的局部变量 str 指向了一个新对象,对外部的 s 没有影响。
总结对比表
| 特性 | 基本数据类型 (如 int) | 引用数据类型 (如 Person, 数组) |
|---|---|---|
| 传递的内容 | 变量值的副本 | 对象引用(地址)的副本 |
| 能否修改原始变量本身的值 | 不能 | 不能(无法让原始引用指向新对象) |
| 能否修改原始对象的状态 | (不适用,无状态) | 能(通过引用的副本操作同一个对象) |
| 通俗比喻 | 给你一份文件的复印件,你怎么涂改都不影响原件。 | 给你一个遥控器(引用)的复制品。你们操作的是同一个电视(对象)。你可以换台(修改状态),但你不能通过扔掉你的复制品遥控器来让我的遥控器也失灵(改变引用本身)。 |
引用数据类型是一种不直接存储数据值本身,而是存储对象在内存中地址(即"引用")的数据类型。变量本身不包含对象的实际数据,而是像一个"遥控器"或"名片",告诉你如何找到内存中真正的对象。
引用数据类型 vs 基本数据类型
为了更好地理解,我们先对比一下它们:
| 特性 | 基本数据类型 | 引用数据类型 |
|---|---|---|
| 存储内容 | 直接存储数据值本身 | 存储对象在内存中的地址(引用) |
| 内存位置 | 变量和值都存储在栈内存 | 引用(变量名)在栈内存,实际对象在堆内存 |
| 默认值 | 有默认值(如 int 是 0,boolean 是 false) | 默认值为 null(表示不指向任何对象) |
| 示例类型 | int, double, char, boolean 等8种 | String, 数组, 自定义类(如 Person), Integer 等 |
| 赋值操作 | 创建值的副本 | 创建引用的副本(多个变量可指向同一个对象) |
| 比较操作 | == 比较值是否相等 | == 比较内存地址是否相同(是否同一个对象) |
引用数据类型的具体类型
-
类
- 系统预定义类:如
String、Integer、Scanner、File等。 - 自定义类:你自己创建的类,如
Person、Student、Car等。
- 系统预定义类:如
-
接口
- 如
List、Map、Runnable等。虽然不能直接实例化,但用于声明引用变量,指向实现了该接口的类的对象。
- 如
-
数组
- 无论是基本类型的数组(如
int[])还是对象数组(如String[]),数组本身都是引用数据类型。
- 无论是基本类型的数组(如
内存模型详解(核心)
这是理解引用数据类型最关键的部分。我们通过一个例子来看:
public class ReferenceExample {
public static void main(String[] args) {
// 基本数据类型
int age = 25;
// 引用数据类型 - String
String name = "Alice";
// 引用数据类型 - 自定义类
Person person = new Person("Bob", 30);
// 引用数据类型 - 数组
int[] scores = {90, 85, 95};
}
}
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
内存中的状态如下图所示:
[栈内存 (Stack)] [堆内存 (Heap)]
-------------------------------- --------------------------------
main方法的栈帧:
- age: 25 (基本类型,值直接存储)
- name: 0x100 ---> [String对象 "Alice"] 在字符串常量池
- person: 0x200 ---> [Person对象]
|- name: 0x300 ---> [String对象 "Bob"]
|- age: 30
- scores: 0x400 ---> [int[]数组对象]
|-> index0: 90
|-> index1: 85
|-> index2: 95
解释:
-
栈内存:存储方法调用信息和局部变量。
age是基本类型,其值25直接存储在栈上。name,person,scores是引用变量,它们在栈上存储的是一个内存地址(如0x100,0x200,0x400)。
-
堆内存:存储所有对象和数组的实际数据。
- 当执行
new Person(...)或new int[...]时,就在堆内存中开辟空间创建对象。 - 引用变量通过存储的地址"指向"堆中的对象。
- 当执行
引用数据类型的关键行为
1. 多个引用可以指向同一个对象
Person p1 = new Person("Charlie", 25);
Person p2 = p1; // p2 和 p1 指向同一个对象
p2.age = 26; // 通过 p2 修改对象
System.out.println(p1.age); // 输出 26,因为 p1 也指向同一个对象
内存模型:
[栈] [堆]
p1 -> 0x500 ---> [Person对象] { name: "Charlie", age: 26 }
p2 -> 0x500 ---/
2. 使用 == 和 equals() 的比较
==比较的是引用(内存地址)是否相同。equals()比较的是对象的内容是否逻辑相等(默认行为同==,但可被重写,如String)。
String s1 = new String("Hello");
String s2 = new String("Hello");
String s3 = s1;
System.out.println(s1 == s2); // false,不同对象,地址不同
System.out.println(s1 == s3); // true,指向同一个对象
System.out.println(s1.equals(s2)); // true,String重写了equals,比较内容
3. 特殊值 null
null 表示引用不指向任何对象。
Person p = null; // p 不指向任何对象
System.out.println(p); // 输出 null
// p.name = "John"; // 这里会抛出 NullPointerException
为什么需要引用数据类型?
c
- 动态内存管理:对象在堆上动态创建,生命周期更灵活。
- 复杂数据结构:可以创建包含多个属性和方法的复杂对象。
- 面向对象特性:支持继承、多态等面向对象的核心特性。
- 共享数据:多个引用可以指向同一个对象,方便数据共享。
- 高效内存使用:传递对象时只需传递引用(地址),而不需要复制整个对象。
总结
引用数据类型是Java面向对象编程的基石。理解它的关键在于分清"引用"和"对象":
- 引用是变量,存储在栈上,内容是对象的地址。
- 对象是实际的数据结构,存储在堆上。
- 一个对象可以被多个引用指向。
- 操作引用实际上是通过地址间接操作堆上的对象。
c
更多推荐


所有评论(0)