Java 基础(五)值传递

Java值传递与引用传递核心知识点随堂笔记

前言

承接Java面向对象、String类系列内容,本次笔记彻底讲透Java方法参数传递的核心机制,纠正新手最容易踩坑的"Java基本类型值传递、引用类型引用传递"的错误认知,完整拆解课堂中的两个核心代码示例,结合内存模型讲透底层原理,补充面试高频考点,适配Java入门复盘与巩固需求。

一、核心概念与终极结论

1.1 两个核心定义

首先必须明确值传递和引用传递的官方定义,这是判断Java传递类型的唯一标准:

  • 值传递(Pass By Value) :方法调用时,传递的是实参的副本(拷贝),而非实参本身。方法内对副本的任何修改,都不会影响到方法外部的原始实参。
  • 引用传递(Pass By Reference) :方法调用时,传递的是实参的内存地址本身(而非副本),方法内对参数的所有修改,都会直接作用到方法外部的原始实参上。

1.2 Java的终极结论

Java语言中只有值传递,没有引用传递。无论参数是基本数据类型,还是引用数据类型,传递的始终是实参的副本,而非实参本身。

  • 对于基本数据类型:传递的是数据值的副本
  • 对于引用数据类型:传递的是对象引用地址的副本(对应课堂核心句:值传递是将值的地址传递过去)

二、Java内存模型基础铺垫

要彻底理解参数传递,必须先搞清楚Java中变量在内存中的存储位置,这是所有原理的基础:

  1. 栈内存:存储方法中的局部变量(包括基本数据类型的变量、引用数据类型的变量),方法执行完毕后栈帧自动释放。
  2. 堆内存:存储通过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 执行原理拆解

  1. main方法中定义变量a,栈中存储a的值为10。
  2. 调用change(a)时,会将a的值拷贝一份副本,传递给方法的形参num,此时栈中num的值是10,和原变量a完全独立。
  3. 方法内修改num=20,修改的只是副本的值,原变量a的存储空间完全不受影响。
  4. 方法执行完毕,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;
    }
}
执行流程与内存拆解
  1. Two two = new Two();:栈中创建two变量,存储堆中Two对象的地址(比如0x1),堆中对象的x属性默认值为0。
  2. 调用fix(two)时,会将two变量中存储的地址拷贝一份副本 ,传递给形参tt。此时栈中tt变量存储的地址也是0x1,和原two变量指向堆中的同一个对象
  3. 执行tt.x = 42:通过地址副本,找到堆中0x1的对象,修改其x属性为42。因为原two变量也指向这个对象,所以方法外通过two.x获取到的值也变成了42。
  4. 方法返回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中引用类型传递的也是值(地址的副本),是值传递,而非引用传递

五、新手高频避坑指南

  1. 永远记住:Java只有值传递,不要再说"基本类型值传递,引用类型引用传递",这是面试高频错误点。
  2. 区分两个完全不同的操作
    • 修改引用指向的对象的属性:会影响原对象,因为副本和原引用指向同一个堆对象。
    • 修改引用变量本身(让它指向新对象):不会影响原变量,因为修改的只是地址副本。
  3. 不要用"是否修改了原对象内容"来判断传递类型 ,判断的唯一标准是:传递的是实参本身,还是实参的副本
  4. 基本类型的包装类(Integer、String等):因为是不可变对象,方法内修改只会创建新对象,不会影响原变量,和基本类型表现一致。

六、笔记核心总结

  1. 核心定义:值传递传递的是实参的副本,引用传递传递的是实参本身的地址;Java只有值传递,没有引用传递。
  2. 基本类型传递:传递的是数据值的副本,方法内修改副本不会影响原变量。
  3. 引用类型传递:传递的是对象引用地址的副本,副本和原引用指向堆中的同一个对象;修改对象的属性会影响原对象,修改引用本身的指向不会影响原变量。
  4. 核心证据:交换两个对象引用的方法,无法改变方法外原变量的指向,彻底证明Java不是引用传递。
  5. 内存本质:引用变量存储在栈中,是堆中对象的地址;对象本身存储在堆中,所有指向该地址的引用,都能修改堆中的对象内容。
相关推荐
Seven972 小时前
【从0到1构建一个ClaudeAgent】协作-Worktree+任务隔离
java
会编程的土豆2 小时前
【日常做题】栈 中缀前缀后缀
开发语言·数据结构·算法
阿扬ABCD2 小时前
python项目:外星人入侵小游戏
开发语言·python·pygame
倒霉蛋小马2 小时前
SpringBoot3中配置Knife4j
java·spring boot·后端
NotFound4862 小时前
实战分享怎样实现Spring Boot 中基于 WebClient 的 SSE 流式接口操作
java·spring boot·后端
青衫码上行2 小时前
【从零开始学习JVM】程序计数器
java·jvm·学习·面试
不吃香菜学java10 小时前
Redis的java客户端
java·开发语言·spring boot·redis·缓存
captain37610 小时前
事务___
java·数据库·mysql
北漂Zachary11 小时前
四大编程语言终极对比
android·java·php·laravel