乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

参数传递 详解

本章定位:深入剖析Java中参数传递的底层机制——值传递的真相。大翔在飞翔科技带新人白歌时,发现她对"Java到底是值传递还是引用传递"这个问题一直含糊不清。本章通过内存模型图解和翔实的代码实验,从栈帧、堆内存、引用副本等底层概念出发,彻底揭开Java参数传递的面纱,帮助白歌和读者建立清晰的认知模型。


定义与语法

属性说明
值传递(Pass by Value)方法调用时,将实参值的副本传递给形参;Java中永远只有值传递
引用类型参数传递的是对象引用值(内存地址)的副本,而非对象本身的副本
基本类型参数传递的是基本类型数值的副本,方法内修改形参不影响实参
形参(Parameter)方法定义时声明的变量,如 public void setName(String name) 中的 name
实参(Argument)方法调用时实际传入的值,如 obj.setName("白歌") 中的 "白歌"
栈帧(Stack Frame)JVM为每个方法调用分配的栈内存区域,存储局部变量表、操作数栈等
堆内存(Heap)JVM中存放所有对象实例和数组的内存区域,由GC管理
引用值指向堆中对象的指针(类似内存地址),Java中引用值对外不可见
局部变量表栈帧中存放方法参数和局部变量的区域,基本类型存值,引用类型存引用值

核心结论:Java中一切皆值传递。对于基本类型,传递的是数值的副本;对于引用类型,传递的是引用值(指向对象的内存地址)的副本。通过引用副本可以修改对象的状态,但不能让引用副本指向新对象后影响原始引用。这正是Java与C++引用传递的本质区别。


完整代码示例

示例一:小崔验证基本类型值传递——交换两个int变量

在飞翔科技的薪资系统开发中,小崔写了一个swap方法试图交换两个员工的工资金额。他发现无论怎么交换,调用处的变量值始终不变。这让他深刻理解了基本类型值传递的本质。

/** 飞翔科技 - 基本类型参数传递验证:swap方法无法交换原始变量 */
public class PrimitiveSwapDemo {
    public static void main(String[] args) {
        int salaryA = 8888;   // 员工A的薪资
        int salaryB = 6666;   // 员工B的薪资

        System.out.println("【交换前】");
        System.out.println("  salaryA = " + salaryA + ", salaryB = " + salaryB);

        // 尝试交换
        swap(salaryA, salaryB);

        System.out.println("【交换后——调用处变量未变】");
        System.out.println("  salaryA = " + salaryA + ", salaryB = " + salaryB);
        System.out.println("  >>> 结论:基本类型是值传递,形参修改不影响实参");
    }

    /** 飞翔科技 - 交换两个整数(此方法无法影响调用处的原始变量) */
    public static void swap(int a, int b) {
        System.out.println("  [swap内部 - 交换前] a = " + a + ", b = " + b);
        int temp = a;
        a = b;
        b = temp;
        System.out.println("  [swap内部 - 交换后] a = " + a + ", b = " + b);
    }
}
【交换前】
  salaryA = 8888, salaryB = 6666
  [swap内部 - 交换前] a = 8888, b = 6666
  [swap内部 - 交换后] a = 6666, b = 8888
【交换后——调用处变量未变】
  salaryA = 8888, salaryB = 6666
  >>> 结论:基本类型是值传递,形参修改不影响实参

场景解读:小崔发现,swap方法内部的a和b确实交换了,但main方法中的salaryA和salaryB纹丝不动。这是因为调用swap(salaryA, salaryB)时,JVM将salaryA和salaryB的值(8888和6666)复制给了形参a和b。a和b是salaryA和salaryB的副本,它们在swap方法栈帧中独立存在,交换副本不会影响原始变量。


示例二:白歌理解引用类型参数传递——修改Employee对象属性

白歌在飞翔科技人事系统中负责员工信息管理。她写了一个方法修改员工的薪资,发现对象的状态确实被改变了。但当她尝试让形参指向一个新对象时,原始引用纹丝不动——这让她彻底理解了"引用值副本"的含义。

/** 飞翔科技 - 引用类型参数传递:修改对象属性 vs 重定向引用 */
class Employee {
    String name;
    double salary;

    Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Employee{name='" + name + "', salary=" + salary + "}";
    }
}

public class ReferencePassDemo {
    public static void main(String[] args) {
        Employee emp = new Employee("白歌", 8888.88);
        System.out.println("【初始状态】");
        System.out.println("  emp = " + emp);

        // 场景一:通过引用副本修改对象属性
        modifySalary(emp, 12888.88);
        System.out.println("【方法一调用后——对象被修改了!】");
        System.out.println("  emp = " + emp);

        // 场景二:尝试让引用指向新对象
        reassignReference(emp);
        System.out.println("【方法二调用后——引用未被改变!】");
        System.out.println("  emp = " + emp);
        System.out.println("  >>> 结论:引用类型也是值传递,传递的是引用值的副本");
    }

    /** 飞翔科技 - 通过引用副本修改对象内部状态(可以生效) */
    public static void modifySalary(Employee e, double newSalary) {
        System.out.println("  [modifySalary] 修改前:" + e);
        e.salary = newSalary;  // 修改堆中对象的属性
        System.out.println("  [modifySalary] 修改后:" + e);
    }

    /** 飞翔科技 - 尝试让引用指向新对象(不影响原始引用) */
    public static void reassignReference(Employee e) {
        System.out.println("  [reassignReference] 原始引用指向:" + e);
        e = new Employee("大翔", 25000.00);  // 让副本指向新对象
        System.out.println("  [reassignReference] 副本已指向新对象:" + e);
    }
}
【初始状态】
  emp = Employee{name='白歌', salary=8888.88}
  [modifySalary] 修改前:Employee{name='白歌', salary=8888.88}
  [modifySalary] 修改后:Employee{name='白歌', salary=12888.88}
【方法一调用后——对象被修改了!】
  emp = Employee{name='白歌', salary=12888.88}
  [reassignReference] 原始引用指向:Employee{name='白歌', salary=12888.88}
  [reassignReference] 副本已指向新对象:Employee{name='大翔', salary=25000.0}
【方法二调用后——引用未被改变!】
  emp = Employee{name='白歌', salary=12888.88}
  >>> 结论:引用类型也是值传递,传递的是引用值的副本

场景解读:modifySalary方法接收了emp引用值的副本e。e和emp指向堆中同一个Employee对象,所以通过e修改salary能反映到emp上。但reassignReference方法中,e = new Employee(...)只是让副本e指向了新对象,原始的emp仍然指向原来的Employee对象。这就是"引用值副本"的精确含义。


示例三:朱璐测试String参数——不可变对象的"假引用传递"

朱璐在飞翔科技日志系统中发现了一个有趣的现象:将String传给方法后修改,原始String也不变。她一度以为String也是基本类型,后来才明白是因为String的不可变性。

/** 飞翔科技 - String作为引用类型参数的特殊表现:不可变性导致的"假值传递"效果 */
public class StringPassDemo {
    public static void main(String[] args) {
        String dept = "研发部";
        System.out.println("【调用前】dept = " + dept);

        changeString(dept);
        System.out.println("【调用后】dept = " + dept);
        System.out.println("  >>> String内容未变!因为String是不可变对象");

        // 使用StringBuilder对比——可变对象
        StringBuilder sb = new StringBuilder("飞翔科技");
        System.out.println("\n【StringBuilder对比】");
        System.out.println("【调用前】sb = " + sb);
        modifyStringBuilder(sb);
        System.out.println("【调用后】sb = " + sb);
        System.out.println("  >>> StringBuilder是可变的,内容被修改了!");
    }

    /** 飞翔科技 - 修改String参数(实际上让副本指向了新对象) */
    public static void changeString(String s) {
        System.out.println("  [changeString] 原始:" + s);
        s = s + " - 深圳分部";  // 创建新String对象,让副本s指向它
        System.out.println("  [changeString] 副本指向新对象:" + s);
    }

    /** 飞翔科技 - 修改StringBuilder的内容(同一个对象) */
    public static void modifyStringBuilder(StringBuilder sb) {
        sb.append(" - 深圳分部");  // 修改同一个StringBuilder对象的内容
    }
}
【调用前】dept = 研发部
  [changeString] 原始:研发部
  [changeString] 副本指向新对象:研发部 - 深圳分部
【调用后】dept = 研发部
  >>> String内容未变!因为String是不可变对象

【StringBuilder对比】
【调用前】sb = 飞翔科技
【调用后】sb = 飞翔科技 - 深圳分部
  >>> StringBuilder是可变的,内容被修改了!

示例四:Frank设计数组参数传递——数组也是引用类型

Frank在飞翔科技考勤系统中需要批量调整员工出勤天数。他利用数组是引用类型的特性,通过方法直接修改数组元素。

/** 飞翔科技 - 数组参数传递:数组是引用类型,方法内修改元素会影响原始数组 */
public class ArrayPassDemo {
    public static void main(String[] args) {
        int[] attendanceDays = {22, 20, 18, 21, 19};  // 5位员工的出勤天数
        System.out.println("【调整前】出勤天数:");
        printArray(attendanceDays);

        // 批量调整:每人增加2天
        addAttendanceDays(attendanceDays, 2);
        System.out.println("【调整后】出勤天数:");
        printArray(attendanceDays);
    }

    /** 飞翔科技 - 为每位员工增加出勤天数 */
    public static void addAttendanceDays(int[] arr, int delta) {
        for (int i = 0; i < arr.length; i++) {
            arr[i] += delta;  // 通过引用副本修改堆中数组元素
        }
    }

    /** 飞翔科技 - 打印数组 */
    public static void printArray(int[] arr) {
        System.out.print("  [");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i]);
            if (i < arr.length - 1) System.out.print(", ");
        }
        System.out.println("]");
    }
}
【调整前】出勤天数:
  [22, 20, 18, 21, 19]
【调整后】出勤天数:
  [24, 22, 20, 23, 21]

深度原理分析

一、JVM内存模型视角下的参数传递

下图展示了基本类型参数和引用类型参数在JVM栈和堆中的传递过程。当main方法调用swap(a, b)时,JVM为swap分配新的栈帧,将实参的值复制到swap栈帧的局部变量表中。对于引用类型,复制的是引用值(指向堆中对象的指针),而非对象本身。

关键理解:

  1. 基本类型变量在栈帧中直接存储数值本身,复制时复制数值。
  2. 引用类型变量在栈帧中存储引用值(指向堆的指针),复制时复制引用值。
  3. 无论哪种类型,传递的都是值的副本——这就是值传递的普适定义。
  4. 引用副本与原始引用指向同一个堆对象,因此可以通过副本修改对象状态。

二、Java值传递 vs C++引用传递对比

这是面试中最常见的混淆点。Java与C++在参数传递上的根本区别:

本质区别:

  • Java中,形参是实参的副本,两者独立存储。修改形参的值(包括让它指向新对象)不影响实参。
  • C++引用传递中,形参是实参的别名,两者代表同一内存位置。修改形参就是修改实参。
  • Java中通过引用副本修改对象状态是可行的,这常被误称为"引用传递",但本质上仍是值传递。

三、图解:为什么swap永远无法在Java中实现?

为什么数组元素交换可以成功? 因为传入的是数组引用值的副本,通过该副本可以访问并修改堆中的数组元素。swap的是数组元素(堆中数据),而非引用本身。


易错场景

反例一:❌ 误以为Java有引用传递

// ❌ 错误认知:以为Java像C++一样支持引用传递
public static void wrongSwap(Integer a, Integer b) {
    Integer temp = a;
    a = b;
    b = temp;  // 只是交换了形参副本,实参不变!
}
public static void main(String[] args) {
    Integer x = 10001, y = 10002;
    wrongSwap(x, y);
    System.out.println(x + ", " + y);  // 输出 10001, 10002 —— 根本没交换!
}

✅ 正确认知:Java只有值传递。Integer虽是引用类型,但方法中a和b是x和y引用值的副本,交换副本不影响原始变量。更何况Integer还是不可变类,连通过副本修改对象状态都做不到。


反例二:❌ 混淆"修改对象"和"修改引用"

// ❌ 错误写法:试图在方法内创建新对象并"返回"给调用者
public static void createNewEmployee(Employee e) {
    e = new Employee("孔蓝", 15888.88);  // 只是让副本e指向新对象
    // 原始引用不受影响!
}
// ✅ 正确写法一:通过返回值传递新对象
public static Employee createNewEmployee() {
    return new Employee("孔蓝", 15888.88);  // 返回新对象引用
}

// ✅ 正确写法二:如果真的需要修改引用,使用数组或包装对象
public static void createNewEmployee(Employee[] holder) {
    holder[0] = new Employee("孔蓝", 15888.88);  // 修改数组元素
}

反例三:❌ 在方法内修改传入List的引用

// ❌ 错误:试图重置List引用
public static void resetList(List<String> list) {
    list = new ArrayList<>();  // 副本指向新List,原始list不变!
    list.add("孙鹤");
}
// ✅ 正确:通过引用副本操作同一个List对象
public static void resetListCorrect(List<String> list) {
    list.clear();              // 清空原始List
    list.add("孙鹤");          // 向原始List添加元素
}

反例四:❌ 误认为基本类型包装类参数可以修改原值

// ❌ Integer是不可变类,无法通过方法修改其值
public static void incrementSalary(Integer salary) {
    salary = salary + 1000;  // 自动拆箱→加法→自动装箱→新Integer对象
    // salary副本指向新对象,原始引用不变!
}
// ✅ 方案一:使用返回值
public static Integer incrementSalary(Integer salary) {
    return salary + 1000;
}

// ✅ 方案二:使用可变容器(如AtomicInteger或自定义包装类)
public static void incrementSalary(AtomicInteger salary) {
    salary.addAndGet(1000);  // 修改同一个AtomicInteger对象的值
}

面试考点

Q1: Java中的参数传递是值传递还是引用传递?请详细说明。

答:Java中永远只有值传递。具体而言:

  • 对于基本类型(int, double, boolean等),传递的是基本类型数值的副本。方法内对形参的任何修改都不会影响实参。
  • 对于引用类型(对象、数组等),传递的是对象引用值的副本。通过这个副本可以访问和修改堆中同一个对象的状态(如修改对象的属性、数组的元素),但无法让副本指向新对象后影响原始引用。

很多人误以为Java有引用传递,是因为通过引用副本修改对象状态看起来像引用传递。但真正的引用传递(如C++的&引用)中,形参是实参的别名,修改形参本身就是修改实参(包括让形参指向新对象)。Java做不到这一点,因此Java是纯值传递语言。

Q2: 以下代码的输出是什么?为什么?

public static void change(int[] arr) {
    arr[0] = 999;
    arr = new int[]{1, 2, 3};
    arr[1] = 888;
}
public static void main(String[] args) {
    int[] a = {10, 20, 30};
    change(a);
    System.out.println(Arrays.toString(a));
}

答:输出 [999, 20, 30]。原因分析:

  1. change(a) 将a的引用值副本传给arr,arr和a指向同一个数组对象。
  2. arr[0] = 999 通过副本修改了堆中数组的第一个元素,所以a看到的数组也变了。
  3. arr = new int[]{1, 2, 3} 让副本arr指向了一个新数组,原始的a仍然指向旧数组。
  4. arr[1] = 888 修改的是新数组的元素,对a指向的旧数组无影响。
  5. 因此最终a指向的数组是 [999, 20, 30]。

Q3: String作为参数传递时为什么表现得像基本类型?

答:String是引用类型,传递的是引用值的副本,这没错。但其"不可变"的特性导致了类似基本类型的行为:

  • String对象一旦创建,其内容(char数组)不可修改。
  • 任何对String的"修改"操作(如拼接、replace)都会创建新的String对象。
  • 方法内的 s = s + "..." 实际上是让形参副本s指向了新创建的String对象,而原始引用仍然指向原来的String。
  • 结论:String的特殊表现不是因为参数传递机制不同,而是因为String的不可变性。同样的逻辑适用于所有不可变类(Integer、LocalDate等)。

Q4: 如何在Java中实现"交换两个对象引用"的效果?

答:Java无法直接通过方法交换两个对象引用(因为值传递的限制),但可以通过以下方式间接实现:

  1. 使用数组包装:将两个引用放入数组,交换数组元素。
    public static <T> void swap(T[] arr, int i, int j) {
        T temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;
    }
    
  2. 使用包装类:创建一个持有引用的可变容器类。
  3. 返回值方式:不交换引用本身,而是返回需要的结果。
  4. AtomicReference:利用CAS操作交换引用(适用于并发场景)。

理解Java值传递的深层原理,是写出正确Java代码、通过技术面试的基础。大翔总结道:"记住一句话——Java中,所有参数传递都是把值的副本压入栈帧。基本类型复制数值,引用类型复制引用值。就这么简单。"

上一页
this 关键字
下一页
枚举