Java中的值传递和引用传递

6 阅读5分钟

基本理论

为准备面试,把这部分基础知识梳理了一下,纯古法手搓,无AI。

基本类型和引用类型

  • 基本类型:byte、short、int、long、float、double、boolean、char
  • 引用类型:对象(包括:数组、包装类)

创建变量时发生了什么

基本类型:

int a = 100;

jvm在栈上创建了一个int类型的区域,并保存当前的值。

引用类型:

Student stu = new Student();

此时jvm

  1. 在栈上创建一个Student类型的引用stu
  2. 在堆上创建一个新的Student对象
  3. 把stu引用指向新的Student对象的实际地址

实参和形参

  • 声明一个函数时,函数中的参数称为形参,形参在不调用的时候不知道它的值
  • 调用一个函数时,传入的值称为实参,也就是调用时实际的值
  • 值传递和引用传递的区别在于:修改形参会不会影响实参

值传递和引用传递

值传递:修改形参不会影响实参。

    public static void main(String[] args) {
        int a = 100;
        modify(a);
        System.out.println(a); // a 没有被影响,输出100
    }

    public static void modify(int b) {
        b = 999999; // 只改变了当前方法内的形参,不影响方法外的实参
    }

通常理解的引用传递(并非真正的引用传递,Java中只有值传递):修改形参会影响实参

    public static void main(String[] args) {
        Student stu = new Student();
        stu.name = "张三";
        modify(stu);
        System.out.println(stu.name); //  当前参数指向的实际对象被修改,输出李四
    }

    public static void modify(Student b) {
        b.name = "李四"; // 由于Student是引用类型,实际的对象也被修改
    }

为什么说Java只有值传递?

明明看到形参修改了对象的属性,为什么说引用类型也是值传递呢?

引用类型分为两部分:栈上的引用变量、堆上的实际对象

刚刚的例子验证了堆上的实际对象可以被修改,那么,当传递引用类型的参数时,参数的指向能否修改呢?

    public static void main(String[] args) {
        Student stu = new Student();
        stu.name = "张三";
        modify(stu);
        System.out.println(stu.name); //  当前参数指向的实际对象没有被修改,输出张三
    }

    public static void modify(Student b) {
        b = new Student(); // 形参改变了引用对象,由于引用值是值传递,并没有改变实参的引用对象
        b.name = "李四";
    }

当函数接到参数时,如果参数是引用类型,我们只能接到引用的值,并创建新的变量保存了引用的值。虽然可以对引用指向的实际对象做修改,但不能修改原始的引用。

这就是为什么说“Java只有值传递”:对于基本类型,拿到的变量值的副本;对于引用类型,拿到的对象引用的副本。

C++ 的引用传递允许直接操作变量本身(类似于别名),而 Java 为了安全性和内存管理的简化,屏蔽了指针操作,统一使用“值传递引用地址”的方式。

特殊类型的传值实验

String:

    public static void main(String[] args) {
        String str = new String();
        str = "张三";
        modify(str);
        System.out.println(str); //  当前参数指向的实际对象没有被修改,输出张三
    }

    public static void modify(String b) {
        b = "李四"; // 字符串的直接赋值等于修改引用,String本身是不可变对象
    }

String是不可变对象,修改、拼接、拆分等操作本质上都是创建新对象+更新引用。

包装类

    public static void main(String[] args) {
        Integer num = Integer.valueOf(100);
        modify(num);
        System.out.println(num); //  当前参数指向的实际对象没有被修改,输出100
    }

    public static void modify(Integer b) {
        b = 9999; // 包装类的直接赋值等于修改引用,包装类也是不可变对象
    }

包装类也是不可变对象,同上

数组

    public static void main(String[] args) {
        int[] num = new int[10];
        num[0] = 100;
        modify(num);
        System.out.println(num[0]); //  当前参数指向的实际对象被修改,输出9999
    }

    public static void modify(int[] b) {
        b[0] = 9999; // 数组是对象,修改元素相当于修改实际引用对象的属性
    }

数组也是对象,修改元素的操作可以理解成修改对象属性的语法糖,所以函数外的实参也是会被修改的。

由于“Java只有值传递”,如果对数组重新赋值呢?

    public static void main(String[] args) {
        int[] num = new int[10];
        num[0] = 100;
        modify(num);
        System.out.println(num[0]); //  当前参数指向的实际对象没有被修改,输出100
    }

    public static void modify(int[] b) {
        b = new int[10]; // 此处发生了引用变化,由于是值传递,未修改实参的引用
        b[0] = 9999;     // 实际修改的数组已经不是当时传过来的数组了
    }

显然,重新赋值的行为改变了形参的地址,但未改变实参的地址。

如果不想改变原始值怎么办?

拷贝

  • 拷贝是指将当前对象克隆一份,并传递克隆对象的引用地址。
  • 浅拷贝:基本类型直接复制,引用类型复制引用,不创建新的引用对象
  • 深拷贝:基本类型直接赋值,引用类型递归拷贝,创建新的引用对象

受限于篇幅,拷贝的内容先不写了。面试中需要注意:对于包含引用类型成员的类,浅拷贝会导致新旧对象共享同一个成员对象,修改其中一个对象的成员,依然会影响另一个对象。

总结

  • 值传递和引用传递的区别:修改形参是否影响实参
  • Java中只有值传递
  • 对于基本类型,传递的是数值的副本
  • 对于引用类型,传递的是引用(内存地址)的副本

扩展

如果你受够了重复写增删改查、配权限、搭流程……试试 JNPF。基于 SpringBoot + Vue.js,Java/.Net 双核驱动,全源码交付。拖拽生成后台、表单、报表,支持国产化环境,代码随时导出二次开发。不是“低代码玩具”,是真正给研发人员提效的引擎。点击免费体验,把精力留给更难的挑战。