这篇文章旨在用最流行的语言讲述最枯燥的基础知识。难点,有时记得语法但记不住实际怎么用,有时知道怎么用却讲不清原理,市场上讨论的话题充满争议:有的论坛帖子说Java只能传值,有的博客说两者都有;这个有点迷惑,我们来研究一下这个话题,在书籍、论坛和博客上考证一下,得到一个可信的答案。其实对于值传递和引用传递的语法和应用,百度可以提供相当多的解释和例子。也许你看例子就能明白,但是当你参加面试,做这个知识点的笔试题时,感觉自己会,胸怀成熟地写下答案,却发现不对,或者你根本做不到。是什么原因?那是因为你对知识点没有深入了解,只知道表面。熟悉一个语法很容易,看懂一行代码也不难,但是要将所学的知识融会贯通串联起来理解就非常困难了。在这里,关于值传递和引用传递,小编将从前面学过的基础知识开始,从内存模型出发,一步步介绍值传递和引用传递的本质原理,所以文比较长,知识点比较多。希望读者多多包涵。1、形式参与实参先回顾一组语法:形式参:调用方法时需要传入的参数,如:ainfunc(inta),只有调用func时才有意义,即会分配内存空间。方法func执行后,a会被销毁,释放空间,即没有实参:方法调用时,是传入的实际值,在方法调用前存储。已经初始化并在调用方法时传入。举个栗子:1publicstaticvoidfunc(inta){2a=20;3System.out.println(a);4}5publicstaticvoidmain(String[]args){6inta=10;//实参7func(a);8}复制代码代码示例中inta=10;a在被调用之前已经被创建和初始化。func方法调用的时候是作为参数传入的,所以这个a是实参。func(inta)中的a只有在func被调用的时候才开始它的生命周期,func调用结束后,它也会被JVM释放,所以这个a是一个形参。2.Java数据类型所谓数据类型是一种编程语言中对内存的抽象表达。我们知道一个程序是由代码文件和静态资源组成的。在程序运行之前,这些代码存在于硬盘中,程序开始运行时,这些代码会被转换成计算机可以识别的内容,放入内存中执行。因此,数据类型本质上是用来定义同一类型数据在编程语言中的存储形式,即决定在计算机的内存中如何存储代表这些值的位。因此,数据在内存中的存储是根据数据类型来划定存储形式和存储位置的。那么Java的数据类型有哪些呢?原始类型:编程语言中内置的最小粒度数据类型。它包括四大类八种类型:4种整数类型:byte、short、int、long2种浮点类型:float、double1种字符类型:char1布尔类型:boolean引用类型:reference也叫handle,引用类型,它是编程语言中定义的一种数据形式,存储句柄中实际内容所在地址的地址值。主要包括:借助类接口数组的数据类型,JVM可以规范程序数据的管理。不同的数据类型有不同的存储形式和位置。要想了解JVM是如何存储各类数据的,就必须先了解JVM的内存划分以及各部分的作用。3、JVM内存的划分和作用Java语言本身是不能操作内存的。其中的一切都由JVM管理和控制。所以Java内存区域的划分也是JVM区域的划分。我们说的是JVM的内存划分。之前,我们先看一下Java程序的执行过程,如下图所示:p1-jj.byteimg.com/tos-cn-i-t2...从图中可以看出:Java代码被编译器编译成字节码,JVM开辟了一块内存空间(也叫运行时数据区),通过类加载器添加到运行时数据区,用来存放需要运行的数据和相关信息在程序执行期间使用。在这个数据区中,它由以下几部分组成:1.虚拟机栈2.堆3.程序计数器4.方法区5.本地方法栈下面我们来看看每一部分的原理以及用到的数据是什么存放程序执行过程。虚拟机栈虚拟机栈是Java方法执行的内存模型。栈帧存储在栈中,每个栈帧对应一个被调用的方法。过程。栈是线程私有的,即线程之间的栈是隔离的;当程序中的一个线程开始执行一个方法时,它会创建一个相应的栈帧并将其压入栈中(在栈顶)。方法结束后,栈帧被弹出。下图是一个Java栈的模型和栈帧的组成:data:image/svg+xml;utf8,栈帧:是用来支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中虚拟机栈的栈元素。每个栈帧包括:局部变量表:用于存放方法中的局部变量(非静态变量、函数参数)。当变量是基本数据类型时,直接存储值,当变量是引用类型时,存储对具体对象的引用。操作数栈:Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,这里所说的栈就是指操作数栈。对运行时常量池的引用:对存储程序执行期间可能使用的常量的引用。方法返回地址:存放方法执行完成后的返回地址。堆:堆用于存放对象本身和数组。JVM中只有一个堆,所以堆是所有线程共享的。方法区:方法区是所有线程共享的内存逻辑区域。JVM中只有一个方法区,用来存放一些线程共享的内容。它是线程安全的,多个线程同时访问方法区中的相同内容,只有一个线程可以加载数据,其他线程只能等待。方法区可以存放的内容包括:类的全路径名、类的直接超类的全限定名、类的访问修饰符、类的类型(类或接口)、以及类Lists、常量池(字段、方法信息、静态变量、类型引用(类))等的直接接口的全限定名的顺序局部方法栈:局部方法栈的作用基本是和虚拟机栈一样,也是线程私有的。它们的区别在于虚拟机栈是用来执行Java方法的,而本地方法栈是用来执行本地方法的。.可能有人会疑惑:什么是本地方法?为什么Java仍然调用本地方法?程序计数器:线程专用。记录当前线程执行的字节码的行号指示符。在程序运行过程中,字节码解释器通过改变这个计数器的值来选择下一条要执行的字节码指令,分支、循环、异常处理、线程恢复等基本功能都需要依靠计数器来完成.4.数据如何存储在内存中?从上面的程序运行图我们可以看出程序运行时JVM内存分配的地方有3个:栈静态方法区常量区对应的每个存储区都有自己的内存分配策略:堆类型:栈类型staticus众所周知,Java中的数据类型包括基本数据类型和引用数据类型,那么这些数据的存储采用了哪种策略呢?这里有以下几种情况可以探讨:基本数据类型的存储:A.基本数据类型的局部变量B.基本数据类型的成员变量C.基本数据类型的静态变量2.基本数据类型的引用数据类型的存储让我们分别研究存储:A.基本数据类型的局部变量定义基本数据类型的局部变量,数据直接存储在内存中的栈上,也就是上面说的“虚拟机栈”。数据本身的值存储在栈空间中。data:image/svg+xml;utf8,如上图所示,方法中定义的变量直接入栈,如1intage=50;2intweight=50;3intgrade=6;当我们写"intage=50;"的时候,其实分为两步:1intage;//定义变量2age=50;//赋值copycodecopycode首先JVM创建一个名为age的变量,存放在局部变量表,然后去栈中查找是否有字面值为50的内容,如果有则直接将age指向这个地址。如果不是,JVM会在栈中开辟一块空间存放内容“50”,并将age指向这个地址。所以我们可以知道:当我们声明和初始化基本数据类型的局部变量时,变量名和字面值存储在栈中,它们才是真正的内容。再看“intweight=50;”,按照刚才的思路:栈中已经存在字面值为50的内容,所以weight直接指向这个地址。可以看出,栈中的数据是在当前线程下共享的。那么如果再次执行下面的代码呢?1权重=40;copycodecopycode在代码中重新赋值weight变量时,JVM会去栈中查找字面值为40的内容,如果发现没有内容,就会开辟一块内存空间存储40的内容,权重指向Thisaddress。由此可见,基本数据类型的数据本身是不会变化的。局部变量重新赋值时,并没有改变内存中的字面量内容,而是重新查找栈中已经存在的相同数据。如果不存在,则重新打开内存存放新的数据,将要重新赋值的局部变量的引用指向新数据所在的地址。B.基本数据类型的成员变量成员变量:顾名思义,就是定义在类体中的变量。看下图:data:image/svg+xml;utf8,我们看到per的地址指向堆内存中的一个区域,我们还原代码:1publicclassPerson{2privateintage;3私有字符串名称;4privateint等级;5//长度较长,省略settergetter方法6staticvoidrun(){7System.out.println("run....");8};9}1011//调用12Personper=newPerson();copycodecopycode也是一个局部变量age,name,grade,但是存放在堆中为perobject开辟的空间。因此可以看出,基本数据类型的成员变量名和值都存放在堆中,其生命周期与对象一致。C.基本数据类型的静态变量上面说过,方法区是用来存放一些共享数据的,所以基本数据类型的静态变量名和值存放在方法区的运行时常量池中,而静态变量加载类加载。类消失了,引用数据类型的存储也消失了:上面说了:堆是用来存储对象本身和数组的,引用(句柄)存储的是实际内容的地址值,所以也可以是从上面的程序运行图可以看出,当我们定义一个对象1Personper=newPerson();copycodecopycode其实也有两个过程:1Personper;//定义变量2per=newPerson();//赋值copycodecopycode在执行Personper;的时候,JVM首先在虚拟机栈中的变量表存放per变量。在执行per=newPerson()时,JVM会创建一个Person类的实例对象,并在堆中开辟一块内存。内存存储这个实例,同时把实例的地址值赋值给per变量。因此可以看出,对于引用数据类型的对象/数组来说,变量名存放在栈中,变量值存放的是对象的地址,而不是对象的实际内容。6.值传递和引用传递前面已经介绍了形参和实参,以及数据类型和数据在内存中的存储形式。接下来,就是文章的主题:按值传递和按引用传递。值传递:当方法被调用时,实参通过形参将其内容的副本传递给方法。此时,形参接收到的内容是实参值的一个拷贝,所以方法中对形参的任何操作,都只是对这个拷贝的操作,不影响原值的内容。让我们看一个例子:1publicstaticvoidvalueCrossTest(intage,floatweight){2System.out.println("Incomingage:"+age);3System.out.println("传入重量:"+weight);4岁=33;5重量=89.5f;6System.out.println("方法中重新赋值后的年龄:"+age);7System.out.println("方法中重新赋值后的权重:"+weight);8}910//测试11publicstaticvoidmain(String[]args){12inta=25;13floatw=77.5f;14valueCrossTest(a,w);15System.out.println("methodageafter执行:"+a);16System.out.println("方法执行后的权重:"+w);17}copycodecopycodeoutputresult:1传入年龄:252传入体重:77.534方法中重赋值后年龄:335方法中重赋值后权重:89.567方法执行后年龄:258方法执行后权重:77.5Copycode复制代码从上面的打印结果可以看出,a和w作为实参传入valueCrossTest后,无论在方法中做了什么操作,a和w最终都不会改变。这是什么形状?!!下面我们就根据上面学习到的知识点进行详细的分析:首先,程序运行时,会调用main()方法。此时JVM会为main()方法向虚拟机栈中压入一个栈帧,即当前栈帧,用于存放main()中的局部变量表(包括参数)、操作栈、方法出口等信息,比如a和w是main()方法中的局部变量,所以可以断定a和w在mian方法所在的栈帧如图:data:image/svg+xml;utf8,而在执行valueCrossTest()方法时,JVM也会为其压入一个栈到虚拟机栈中,就是当前的栈帧。它用于存放valueCrossTest()中的局部变量等信息,所以age和weight位于valueCrossTest方法所在的栈帧中,它们的值是通过拷贝一份值得到的a和w的,如图:data:image/svg+xml;utf8,.所以a、age、w、weight对应的内容可能不一致,所以在方法中重新赋值时,实际过程如图:data:image/svg+xml;utf8,即也就是说,年龄和体重的变化,只是改变了当前栈帧(valueCrossTest方法所在的栈帧)的内容。当方法执行时,这些局部变量会被销毁,mian方法所在的栈帧返回栈顶,成为当前栈帧。输出a和w时,还是初始化时的内容。因此:值传递传递的是真实内容的一个副本,对副本的操作不会影响原来的内容,即形参如何变化不会影响实参对应的内容。引用传输:“引用”是指向真实内容的地址值。调用方法时,通过方法调用将实参的地址传递给对应的形参。在方法体中,形参和实参指向公共内存地址。对形参的操作会影响到的实际内容。举个栗子:首先定义一个对象:1publicclassPerson{2privateStringname;3privateint年龄;45publicStringgetName(){6返回名称;7}8publicvoidsetName(Stringname){9this.name=name;10}11publicintgetAge(){113}14publicvoidsetAge(intage){15this.age=age;16}17}复制代码复制代码让我们写一个函数来测试:1publicstaticvoidPersonCrossTest(Personperson){2System.out.println("传入的人名:"+person.getName());3person.setName("我是张小龙");4System.out.println("方法中重新赋值后的名字:"+person.getName());5}6//test7publicstaticvoidmain(String[]args){8Personp=newPerson();9p.setName("我是马化腾");10p.setAge(45);11PersonCrossTest(p);12System.out.println("方法执行后的名字:"+p.getName());13}copycode复制代码输出结果:1传入的人名:我是马化腾2方法中重新赋值后的名字:我是张小龙3方法执行后的名字:我是张小龙复制代码复制代码中,我们可以看到personCrossTest()方法执行后,person的内容发生了变化,印证了上面说的“传引用”。参数的操作改变了实际对象的内容。那么,问题到此结束了吗?不,没那么简单,只有选对了例子,才能看到想要的效果!!!我们把上面的例子稍微修改一下,增加一行代码,1publicstaticvoidPersonCrossTest(Personperson){2System.out.println("传入的人的名字:"+person.getName());3person=newPerson();//添加这行代码4person.setName("我是张小龙");5System.out.println("方法中重新赋值后的名字:"+person.getName());6}复制代码copy代码输出结果:1传入的人名:我是马化腾2方法中重新赋值后的名字:我是张小龙3方法执行后的名字:我是马化腾和上次有区别吗时间?看看有什么问题?根据上面提到的JVM内存模型,我们可以知道Java堆区中存放的是对象和数组,堆区是共享的,所以当程序在main()方法中执行如下代码时1Personp=newPerson();2p.setName("我是马化腾");3p.setAge(45);4PersonCrossTest(p);复制代码复制代码JVM会在堆中开辟一块内存来存放p对象的所有内容,同时在main中创建p对象在p存储堆区的真实地址的引用在()方法所在线程的栈区,如图:p1-jj.byteimg.com/tos-cn-i-t2...PersonCrossTest()方法执行时,因为方法中有这么一行代码:1person=newPerson();copycodecopycodeJVM需要在堆中再开辟一块内存来存放newPerson(),如果地址是“xo3333”,那么形参person指向的就是这个地址,如果真的是引用传递,那么上面提到:在引用传递中,形参和实参指向同一个对象,对形参的操作会改变实参对象的变化。可以推导出实参也应该指向新创建的person对象的地址,所以PersonCrossTest()执行结束后,最终输出的应该是后面创建的对象的内容。然而实际上,最终的输出和我们推测的不一样,最终输出的还是一开始创建的对象的内容。可见Java中不存在引用传递。但是可能有人会疑惑:为什么在第一个例子中,在方法中修改形参的内容,会导致原来对象的内容发生变化呢?这是因为:无论是基本类型还是引用类型,实参传入形参时,都是按值传递的,也就是说传递的是一个副本,而不是内容本身。p1-jj.byteimg.com/tos-cn-i-t2...从图中可以看出,方法中的形参person与实参p没有实际关系,只是复制了一个指向对象的地址从p开始,此时:p和person都指向同一个对象。因此,在第一个例子中,对形参p的操作会影响到实参对应的对象的内容。在第二个例子中,JVM在执行完newPerson()之后,在堆中开辟了一块空间来存放新的对象,并将person的地址修改为指向新的对象。这时候:p还是指向旧对象,person指向新对象的地址。所以此时对person的操作实际上是对new对象的操作,与实参p中对应的对象无关。结论由此可见:Java中所有的参数传递,不管是基本类型还是引用类型,都是值传递,或者拷贝传递。仅在传输过程中:如果对基本数据类型的数据进行操作,由于原始内容和副本都存储实际值,并且在不同的栈区,所以对形参的操作不影响原始内容。如果操作引用类型的数据,有两种情况。一是形参和实参一直指向同一个对象地址,形参的操作会影响实参指向的对象的内容。一种是把形参改成指向一个新的对象地址(比如重新赋值引用),那么对形参的操作就不会影响到实参指向的对象的内容。以上就是小编对“传值与传引用”问题的思考与论证。关于这个问题一直有很多争论。希望在此与广大读者共同探讨、学习。理性评论,不喜勿喷。
