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. 内存本质:引用变量存储在栈中,是堆中对象的地址;对象本身存储在堆中,所有指向该地址的引用,都能修改堆中的对象内容。
相关推荐
来杯@Java9 小时前
图书管理系统(基于springboot+vue前后端分离的项目)计算机毕业设计java
java·spring boot·spring·vue·毕业设计·mybatis·课程设计
卷毛的技术笔记10 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
编程大师哥10 小时前
匿名函数 lambda + 高阶函数
java·python·算法
isyangli_blog10 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb20081110 小时前
FastAPI APIRouter
开发语言·python
Benszen10 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆10 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木10 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
adrninistrat0r10 小时前
Java调用链MCP分析工具
java·python·ai编程
杨充11 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法