目录

Java-值传递与引用传递

Java 值传递与“引用传递”

文章目录

1. Java 的值传递

通常认为 Java 方法传参数都是值传递 ,关于 值传递 的定义如下:

在方法被调用时,实参通过形参把它的 内容副本传入方法内部 ,此时 形参接收到的内容是实参值的一个拷贝 ,因此在方法内对形参的任何操作,都仅仅是对这个副本的操作,不影响原始值的内容

简单的代码示例和打印结果如下

   public static void main(String[] args) {
        int a = 10;
        changeTest(a);
        System.out.println("main:" + a);
    }

    public static void changeTest(int src) {
        System.out.println("changeTest before:" + src);
        src++;
        System.out.println("changeTest after:" + src);
    }
changeTest before:10
changeTest after:11
main:10

以上 变量a定义为基本数据类型 int,我们知道它是存储在虚拟机栈内存中的 。 虚拟机栈是Java方法执行的内存模型,栈中存放着栈帧,每个栈帧分别对应一个被调用的方法,方法的调用过程其实就是栈帧在虚拟机中入栈到出栈的过程。当程序中某个线程开始执行一个方法时就会相应的创建一个栈帧并且入栈(位于栈顶),在方法结束后,栈帧出栈

栈帧
用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素,每个栈帧中包括:
  1. 局部变量表

    用来存储方法中的局部变量(非静态变量、函数形参)。 当变量为基本数据类型时,直接存储值,当变量为引用类型时,存储的是指向具体对象的引用

  2. 操作数栈

    Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中的栈就是指操作数栈

  3. 指向运行时常量池的引用

    存储程序执行时可能用到常量的引用

  4. 方法返回地址

    存储方法执行完成后的返回地址

了解以上情况后其实很好解释示例代码的结果:

  1. 首先调用 main() 方法,此时JVM为 main() 方法往虚拟机栈中压入一个栈帧,即为 当前栈帧 ,用来存放 main() 中的局部变量表(包括参数)、操作栈、方法出口等信息,如 amain() 方法中的局部变量,此时虚拟机栈如图所示

    https://i-blog.csdnimg.cn/blog_migrate/ee7488294e405a441ccf1481921c2aaa.png#pic_center

  2. 当执行到 changeTest() 方法时,JVM也为其往虚拟机栈中压入一个栈帧,用来存放 changeTest() 中的局部变量等信息,因此方法参数 src 是在 changeTest() 方法所在的栈帧中,而其值是从 a 复制得到的。此时虚拟机栈如图所示,在 changeTest() 方法栈帧内部操作变量 src 显然不会影响到 main() 方法栈帧变量 a

    https://i-blog.csdnimg.cn/blog_migrate/36894df0ebda6bd408ead80a56ab83c4.png#pic_center

2. Java 的"引用传递"

引用传递 的定义如下:

引用 也就是指向真实内容的地址值,在方法调用时, 实参的地址通过方法调用被传递给相应的形参 ,在方法体内,形参和实参指向 同一块内存地址,对形参的操作会影响到真实内容

上文解释了 值传递 的过程,但是以下 方法入参为引用类型数据 的 示例代码1 的打印结果与其明显不符。可以看到,在 main() 方法中, List 类型变量 a 没有做任何的元素操作,但是经过 changeTest() 方法添加了元素 99之后, main() 方法中的 List 变量 a 也被加入了元素 99。 内容发生了改变,这似乎完全符合“引用传递”的定义,对形参的操作,改变了实际对象的内容

    public static void main(String[] args) {
        List<Integer> a = new ArrayList<>();
        changeTest(a);
        System.out.println("main:" + a);
    }
    
    public static void changeTest(List<Integer> src) {
        System.out.println("changeTest before:" + src);
        src.add(99);
        System.out.println("changeTest after:" + src);
    }
changeTest before:[]
changeTest after:[99]
main:[99]

但当我们使用几乎完全相同的 示例代码2 ,只是添加一行代码之后,结果又不一样了 : main() 方法中的 List 类型变量 a 的内容并没有被 changTest() 方法中的操作改变

    public static void main(String[] args) {
        List<Integer> a = new ArrayList<>();
        changeTest(a);
        System.out.println("main:" + a);
    }
    
    public static void changeTest(List<Integer> src) {
        System.out.println("changeTest before:" + src);
         // 添加以下一行代码
        src = new ArrayList<>();
        src.add(99);
        System.out.println("changeTest after:" + src);
    }
changeTest before:[]
changeTest after:[99]
main:[]

要解释以上结果,首先我们要知道 变量a定义为对象类型 List,它是存储在虚拟机堆内存中的 。JVM 中堆内存是一块线程共享的区域,对象和数组一般都分配在这一块内存中。这样,我们就可以很容易地解释:

  1. 程序执行到 main() 方法中的代码 List<Integer> a = new ArrayList<>(); 时,JVM会在堆内开辟一块内存,用来存储 List 对象的所有内容,同时在 main() 方法所在线程的栈帧中创建一个引用 a 存储堆区中 List 对象的内存地址,如图

    https://i-blog.csdnimg.cn/blog_migrate/88577052f015d0ce44aca67074bb45ed.png#pic_center

  2. 对于 示例代码1 ,执行到 changeTest() 方法时,JVM也为其往虚拟机栈中压入一个栈帧,其中变量 src 的值为 main() 方法栈帧中变量 a 的副本,故其指向了同一块内存区域

    https://i-blog.csdnimg.cn/blog_migrate/f2eb9108b21a38ba03c96d2b771c5543.png#pic_center

  3. 对于 示例代码2 ,执行到 changeTest() 方法时,在方法内部的代码 src = new ArrayList<>(); 重复了 步骤 1 的过程,首先在堆中开辟内存,存储新建的对象,再将新对象内存地址赋值给变量 src 。这样在对 src 的操作其实是 在操作新对象的内存地址,也就是改变新对象的内容

    https://i-blog.csdnimg.cn/blog_migrate/1da3990a73a7bc2f43687320b1f9e0c0.png

结语

通过以上分析,我们知道 在Java中所有的参数传递,不管基本类型还是引用类型,都是值传递 ,只是在这个过程也分为两种情况:

1.如果是对基本数据类型的数据进行操作, 由于原始内容和副本都是存储实际值,并且是在不同的栈帧内 ,因此形参的操作,不影响原始内容

2.如果是对引用类型的数据进行操作,分两种情况, 一种是形参和实参保持指向同一个对象地址 ,则形参的操作,会影响实参指向的对象的内容。 一种是形参被改动指向新的对象地址(如重新赋值引用) ,则形参的操作,不会影响实参指向的对象的内容