Java值传递与引用传递核心知识点随堂笔记
前言
承接Java面向对象、String类系列内容,本次笔记彻底讲透Java方法参数传递的核心机制,纠正新手最容易踩坑的"Java基本类型值传递、引用类型引用传递"的错误认知,完整拆解课堂中的两个核心代码示例,结合内存模型讲透底层原理,补充面试高频考点,适配Java入门复盘与巩固需求。
一、核心概念与终极结论
1.1 两个核心定义
首先必须明确值传递和引用传递的官方定义,这是判断Java传递类型的唯一标准:
- 值传递(Pass By Value) :方法调用时,传递的是实参的副本(拷贝),而非实参本身。方法内对副本的任何修改,都不会影响到方法外部的原始实参。
- 引用传递(Pass By Reference) :方法调用时,传递的是实参的内存地址本身(而非副本),方法内对参数的所有修改,都会直接作用到方法外部的原始实参上。
1.2 Java的终极结论
Java语言中只有值传递,没有引用传递。无论参数是基本数据类型,还是引用数据类型,传递的始终是实参的副本,而非实参本身。
- 对于基本数据类型:传递的是数据值的副本
- 对于引用数据类型:传递的是对象引用地址的副本(对应课堂核心句:值传递是将值的地址传递过去)
二、Java内存模型基础铺垫
要彻底理解参数传递,必须先搞清楚Java中变量在内存中的存储位置,这是所有原理的基础:
- 栈内存:存储方法中的局部变量(包括基本数据类型的变量、引用数据类型的变量),方法执行完毕后栈帧自动释放。
- 堆内存:存储通过new关键字创建的对象本身(包括对象的成员属性),由JVM垃圾回收器管理。
核心关键点:引用类型的变量,栈中存储的是堆中对象的内存地址,变量本身不存储对象内容 。
比如Student zhangsan = new Student("张三",18);
- 栈内存中:zhangsan变量存储的是堆中Student对象的内存地址(比如0x1)
- 堆内存中:存储着Student对象本身,包含name="张三"、age=18两个属性
三、基本数据类型的值传递
3.1 代码示例
java
public class BasicTypeDemo {
public static void main(String[] args) {
int a = 10;
System.out.println("方法调用前,a的值:" + a); // 输出:10
change(a);
System.out.println("方法调用后,a的值:" + a); // 输出:10
}
// 方法接收int类型参数
public static void change(int num) {
num = 20;
System.out.println("方法内,num的值:" + num); // 输出:20
}
}
3.2 执行原理拆解
- main方法中定义变量a,栈中存储a的值为10。
- 调用change(a)时,会将a的值拷贝一份副本,传递给方法的形参num,此时栈中num的值是10,和原变量a完全独立。
- 方法内修改num=20,修改的只是副本的值,原变量a的存储空间完全不受影响。
- 方法执行完毕,num变量随栈帧释放,main方法中的a还是原来的10。
这是最典型的值传递,副本的修改不会影响原变量。
四、引用数据类型的值传递(课堂核心示例)
引用类型的参数传递是新手最容易混淆的知识点,核心原因是:传递的是地址副本,副本和原引用指向堆中的同一个对象,修改对象的属性会生效,但修改引用本身不会生效。下面完整拆解课堂中的两个核心示例。
4.1 示例1:修改对象的属性,会影响原对象
这是课堂中Two类的示例,先修正语法错误,给出可运行的完整代码:
java
package com.qcby;
/**
* 课堂示例:修改引用对象的属性
*/
class Two {
// 成员属性x,默认值0
byte x;
}
public class ReferenceTypeDemo1 {
public static void main(String[] args) {
ReferenceTypeDemo1 student = new ReferenceTypeDemo1();
student.start();
}
void start() {
// 创建Two对象,栈中two变量存储堆对象的地址(比如0x1)
Two two = new Two();
System.out.print(two.x + " "); // 输出:0
// 调用fix方法,传递two的地址副本
Two two2 = fix(two);
System.out.println(two.x + " " + two2.x); // 输出:42 42
}
// 形参tt接收的是地址的副本,和原two变量指向同一个堆对象
Two fix(Two tt) {
// 通过地址副本,修改堆中对象的x属性
tt.x = 42;
return tt;
}
}
执行流程与内存拆解
Two two = new Two();:栈中创建two变量,存储堆中Two对象的地址(比如0x1),堆中对象的x属性默认值为0。- 调用
fix(two)时,会将two变量中存储的地址拷贝一份副本 ,传递给形参tt。此时栈中tt变量存储的地址也是0x1,和原two变量指向堆中的同一个对象。 - 执行
tt.x = 42:通过地址副本,找到堆中0x1的对象,修改其x属性为42。因为原two变量也指向这个对象,所以方法外通过two.x获取到的值也变成了42。 - 方法返回tt,赋值给two2,此时two2和two、tt都指向同一个堆对象,所以two2.x也是42。
关键误区纠正
很多人在这里误以为是引用传递,其实不是:方法内修改的不是引用本身,而是引用指向的堆中对象的内容。如果我们在方法内修改引用本身(让tt指向新对象),原变量不会受任何影响,示例如下:
java
Two fix(Two tt) {
// 让tt指向一个全新的对象,地址副本变成了0x2
tt = new Two();
tt.x = 42;
return tt;
}
此时执行结果会变成:0 0 42,原two变量的x还是0,因为方法内只是修改了地址副本的指向,原two变量的地址完全没变,还是指向原来的0x1对象,这就是值传递的核心证据。
4.2 示例2:交换两个对象的引用,不会影响原对象
这是课堂中Student类的核心示例,先修正所有语法错误,给出可运行的完整代码:
java
package com.qcby;
/**
* 课堂示例:交换两个对象的引用
*/
public class Student {
private String name;
private int age;
// 构造方法
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// 重写toString方法,方便打印
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
// 交换两个Student对象的name属性
public static void change(Student s1, Student s2) {
// 创建临时对象,用于交换
Student temp = new Student("王五", 20);
// 交换s1和s2的name属性
temp.name = s1.name;
s1.name = s2.name;
s2.name = temp.name;
}
// 进阶:交换两个引用本身
public static void swap(Student s1, Student s2) {
Student temp = s1;
s1 = s2;
s2 = temp;
}
public static void main(String[] args) {
Student zhangsan = new Student("张三", 18);
Student lisi = new Student("李四", 20);
System.out.println("调用change前:");
System.out.println(zhangsan); // 输出:Student [name=张三, age=18]
System.out.println(lisi); // 输出:Student [name=李四, age=20]
// 调用change方法,交换属性
Student.change(zhangsan, lisi);
System.out.println("\n调用change后:");
System.out.println(zhangsan); // 输出:Student [name=李四, age=18]
System.out.println(lisi); // 输出:Student [name=张三, age=20]
// 调用swap方法,交换引用本身
Student.swap(zhangsan, lisi);
System.out.println("\n调用swap后:");
System.out.println(zhangsan); // 还是:Student [name=李四, age=18]
System.out.println(lisi); // 还是:Student [name=张三, age=20]
}
}
核心执行原理拆解
1. change方法:交换对象的属性,会生效
- main方法中,zhangsan变量存储地址0x1(对应张三18的对象),lisi变量存储地址0x2(对应李四20的对象)。
- 调用change方法时,传递的是0x1和0x2的地址副本,形参s1=0x1,s2=0x2,和原变量指向同一个堆对象。
- 方法内通过s1和s2的地址副本,直接修改了堆中两个对象的name属性,所以方法外的原对象属性也会跟着变化。
2. swap方法:交换引用本身,完全不生效(核心证据)
- 调用swap方法时,传递的还是zhangsan和lisi的地址副本,形参s1=0x1,s2=0x2。
- 方法内的
Student temp = s1; s1 = s2; s2 = temp;,只是交换了形参s1和s2这两个副本的地址指向,s1变成了0x2,s2变成了0x1。 - 整个过程,main方法中的zhangsan和lisi变量的地址完全没有被修改,还是分别指向0x1和0x2的对象,所以交换完全不生效。
终极结论验证
如果Java是引用传递,那么swap方法交换引用后,main方法中的zhangsan和lisi应该会互换指向,但实际完全没有变化,这就彻底证明了:Java中引用类型传递的也是值(地址的副本),是值传递,而非引用传递。
五、新手高频避坑指南
- 永远记住:Java只有值传递,不要再说"基本类型值传递,引用类型引用传递",这是面试高频错误点。
- 区分两个完全不同的操作 :
- 修改引用指向的对象的属性:会影响原对象,因为副本和原引用指向同一个堆对象。
- 修改引用变量本身(让它指向新对象):不会影响原变量,因为修改的只是地址副本。
- 不要用"是否修改了原对象内容"来判断传递类型 ,判断的唯一标准是:传递的是实参本身,还是实参的副本。
- 基本类型的包装类(Integer、String等):因为是不可变对象,方法内修改只会创建新对象,不会影响原变量,和基本类型表现一致。
六、笔记核心总结
- 核心定义:值传递传递的是实参的副本,引用传递传递的是实参本身的地址;Java只有值传递,没有引用传递。
- 基本类型传递:传递的是数据值的副本,方法内修改副本不会影响原变量。
- 引用类型传递:传递的是对象引用地址的副本,副本和原引用指向堆中的同一个对象;修改对象的属性会影响原对象,修改引用本身的指向不会影响原变量。
- 核心证据:交换两个对象引用的方法,无法改变方法外原变量的指向,彻底证明Java不是引用传递。
- 内存本质:引用变量存储在栈中,是堆中对象的地址;对象本身存储在堆中,所有指向该地址的引用,都能修改堆中的对象内容。