老生常谈--Java值传递和引用传递

起因

前两天面试被问到了这个问题,虽然之前老早就了解过这个问题,但是并没有深入了解,所以面试的时候一下子慌了,菜是原罪,今天菜鸡来补补基础知识。

其实这个问题一直是被讨论的,常见的三种说法就是,1,Java 值传递引用传递都有,2,只有值传递,3只有引用传递,今天查了很多资料,我发现这个问题并不是随随便便就能说清楚。

先说传参

方法的参数可以分为实参和形参,实参是指被调用时传入的实际的值,在方法调用前就已经初始化完毕。而形参是指方法中用来“承接”实参的参数,它是在这个方法里有效,即作用域。方法执行结束之后即被销毁。

OK,接下来说所谓的值和引用。

Java数据类型

1,基本数据类型,这种是Java中最小粒度的数据类型,包括:byte,short,int,long,float,double,char,boolean。

2,引用类型:是编程语言中存放实际内容所在地址的一种数据类型。一般是类,接口,数组。

 在这里不得不说一下内存区域划分:

在Java文件经过编译器之后,转化成为class文件,之后JVM将class文件通过类加载加载到运行时数据区来存储程序运行时需要用到的数据和相关信息。

运行时数据区

1,虚拟机栈,它是线程私有的,所以是线程隔离的,栈里面存储的是栈帧,每一个栈帧表示被调用的一个方法,方法调用的过程对应着栈帧从入栈到出栈的过程,栈帧中存储的是方法运行需要的信息,它是用来支持方法调用和方法执行的数据结构:

(1),局部变量表(方法中的局部变量,变量为基本类型时,直接存储值,为引用类型时,存储指向具体对象的引用)

(2),方法出口地址(方法执行完返回的地址)

(3),操作数栈(JVM被称为基于栈的执行引擎)

(4),指向常量池的引用

(5),一些附加信息。

2,堆,是所有线程共享的,用来存储对象和数组,在JVM中,只有一个堆。

3,方法区:是一块所有线程共享的内存逻辑区域,在JVM这个中也只有一个方法区,用来存储一些线程可共享的内容,同时它也是线程安全的。方法区存储的信息有:

类的全路径名,类的直接超类的全限定名,类的访问修饰符,类的类型,类的直接接口的全限定名的有序列表,常量池等。

4,本地方法栈:本地方法栈类似于虚拟机栈,也是线程私有,不同的是,虚拟机栈是为Java方法服务,而本地方法栈是为native方法服务。

5,程序计数器,也是线程私有,字节码解释器工作就是通过改变程序计数器里的值来选取下一条执行的字节码指令,分支循环等都需要它来实现。

数据是如何存储的?

JVM 在运行过程中,涉及内存分配有:堆,栈,静态方法区,常量区。内存分配的策略有:堆式,栈式,静态。

基本数据类型的存储:

1,基本数据类型的局部变量以及他的值都是存储在栈上,也就是虚拟机栈的栈帧中。

如:int a = 30;事实上这个过程分为两步,第一步是创建一个age变量, 第二步是先查找栈中是否有30这个值,如果有,则直接将age指向它,没有则开辟一块内存来存储30.

再将age指向它。对它再赋值也是相同的道理,如age = 50,先查找栈中是否有50,有则指向,没有则开辟一块空间存50,再将age指向它。所以足以见之,基本数据类型的赋值是不改变自身数据的,而是像上面那样先查找栈中是否存在,有则指向它,没有则开辟一块内存来装下新的值。

2,基本数据类型的成员变量

与上面的不同的是,它存放在堆里,因为它跟随着一个实例对象,跟实例对象的生命周期相同。

3,基本数据类型的静态变量,与上面都不同,静态变量和它的值都存储在运行时常量区,它是跟随类加载的,随类的消失而消失。

引用数据类型的存储:

引用数据类型分为两部分,一部分是变量,存在栈中,用来存储实际内容的地址,同时在堆中有一块内存来存实际内容。

所谓值传递和引用

值传递

在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时的形参接收到的实际上是实参的副本,在方法体内的任何操作,都是对副本的操作,对实参不影响。如:

public class Main {

    public static void main(String[] args)  {
        int a = 10;
        function(a);
        System.out.println(a);
    }
    private static void function(int a){
        a = 50;
    }
}

输出结果:10

结合之前的铺垫,首先JVM会将其中的main()压入虚拟机栈,用一个栈帧存储,即为当前栈帧,栈帧存储着方法的局部变量表,操作栈,方法出口地址等。之后执行到function方法时,JVM也创建为它创建一个
栈帧,用来存放function的相关信息,因此a的值是在function的栈帧中,而它的值是从实参复制得来的,而再方法体内部进行赋值的时候,也不会去改变main所在的栈帧中的a的值,而是在当前的栈帧中,对值进行修改。而栈帧之间是隔离的,所以不会对实参产生影响。

引用传递

引用传递和值传递的不同点在于,调用方法的时候,实参的地址通过方法传给形参,所以对实参的操作会影响本身的内容,但是但是,这里面还需要分情况。如:

public class Main {
    public static void main(String[] args)  {
        StringBuilder a = new StringBuilder("10");
        function(a);
        System.out.println(a);
    }
    private static void function(StringBuilder str){
        str.append("20");
    }
}

输出:1020 

public class Main {
    public static void main(String[] args)  {
        StringBuilder a = new StringBuilder("10");
        function(a);
        System.out.println(a);
    }
    private static void function(StringBuilder str){
        str = new StringBuilder("20");
    }
}

输出:10

这是为何呢,之前有说,变量是存储在栈,实际内容存储在堆,而造成这里不同的原因就是,Java中的赋值的操作(=)都不是将自身的内容进行改变,而是先检查有没有已经存在的并且等于它的值,有则指向

它,没有则另外创建一个,再指向它,所以在第二段代码里,str=new StringBuilder("20")并没有修改原有的自身的内容而是指向了一个新对象,也就是形参的str和实参a只想了不同的地址,自然对形参没有影响。而在第一段代码中,str.append();是对指向的内存进行操作,而指向的内存也就是实参引用所指向的内存,故有影响。

所以我觉得这里可以做个总结,那就是Java之间并没有引用传递,无论基本数据类型还是引用类型,传的都是值,都是实参的副本,但是不同的是,基本数据类型是实参内容的复制品,而引用类型也是,但是赋值的内容是地址罢了,即实参和形参指向一个同一块内存(对象),形参变化是否会改变实参取决于操作是否是对指向的这块内存进行直接修改。如果是赋值操作这种,那么形参操作对实参就没有影响。

原文地址:https://www.cnblogs.com/Yintianhao/p/12335584.html