This article brings you a detailed explanation of value passing and reference passing in Java. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.
This article aims to tell the boring basic knowledge in the most popular language
Anyone who has learned the basics of Java knows that value passing and reference passing are a difficult point when first coming into contact with Java. Sometimes I remember the syntax but not how to actually use it. Sometimes I can use it but can't explain the principle. Moreover, the topic discussed in the market is full of controversy: some forum posts say that Java only has value passing, and some blogs say that it is both. Yes; this makes people a little confused. Let’s do some discussion on this topic and conduct research on the statements in books and forum blogs to get a trustworthy answer.
In fact, for the syntax and application of value passing and reference passing, you can find a considerable number of explanations and examples on Baidu. Maybe you will understand it by looking at the examples, but when you participate in an interview, do this When you take the written test questions on knowledge points, you feel that you know how to do it and write the answers maturely, but you find that they are wrong, or you don't know how to do them at all.
what is the reason?
That's because you don't have a thorough understanding of the knowledge points and only know the surface. It is easy to be familiar with a grammar, and it is not difficult to understand a line of code, but it is very difficult to integrate the learned knowledge and connect it in series to understand. Here, regarding value transfer and reference transfer, the editor will learn from the past Starting from the basic knowledge learned, starting from the memory model, the essential principles of value transfer and reference transfer are introduced step by step. Therefore, the article is relatively long and contains many knowledge points. I hope readers will bear with me.
Let’s first review a set of syntax:
1. Formal parameters: parameters that need to be passed in when the method is called, such as: The a in func(int a) is meaningful only when func is called, that is, it will be allocated memory space. After the method func is executed, a will be destroyed to release the space, that is, it will no longer exist.
2. Actual parameters: The actual value passed in when the method is called. It has been initialized before the method is called and is passed in when the method is called.
For example:
public static void func(int a){ a=20; System.out.println(a); } public static void main(String[] args) { int a=10;//实参 func(a); }
In the example, a in
int a=10; has been created and initialized before being called. When calling the func method, he is used as a parameter. Passed in, so this a is an actual parameter.
The life cycle of a in func(int a) only begins when func is called, and after the func call ends, it is also released by the JVM, so this a is a formal parameter.
The so-called data type is an abstract expression of memory in a programming language. We know that a program is composed of code files and static resources. When the program is Before running, these codes exist in the hard disk, and when the program starts running, these codes will be converted into content that the computer can recognize and placed in the memory for execution.
Therefore
So, the storage of data in memory is based on the data type to delineate the storage form and storage location.
So
What are the data types of Java?
Basic type: The smallest granular data type built into the programming language. It includes four major categories and eight types:
4 integer types: byte, short, int, long
2 floating point types: float, double
1 character type: char
1 Boolean type: boolean
Reference type: Reference is also called a handle. The reference type is a data form defined in a programming language that stores the address value of the address of the actual content in the handle. It mainly includes:
Class
Interface
Array
With data types, JVM's management of program data is standardized. Different data types, their storage forms and The locations are different. If you want to know how the JVM stores various types of data, you must first understand the memory division of the JVM and the functions of each part.
The Java language itself cannot operate memory. Everything in it is managed and controlled by the JVM. Therefore, the division of Java memory areas is also It is the area division of the JVM. Before talking about the memory division of the JVM, let us first take a look at the execution process of the Java program, as shown below:
As you can see from the picture: after the Java code is compiled into bytecode by the compiler, the JVM opens up a memory space (also called the runtime data area) and adds it to the runtime data area through the class loader for storage. Data and related information needed during program execution. In this data area, it consists of the following parts:
1. Virtual machine stack
2. Heap
3. Program counter
4. Method area
5. Local method stack
Let’s take a look at the principle of each part and its specific use to store the program execution process Which data.
虚拟机栈是Java方法执行的内存模型,栈中存放着栈帧,每个栈帧分别对应一个被调用的方法,方法的调用过程对应栈帧在虚拟机中入栈到出栈的过程。
栈是线程私有的,也就是线程之间的栈是隔离的;当程序中某个线程开始执行一个方法时就会相应的创建一个栈帧并且入栈(位于栈顶),在方法结束后,栈帧出栈。
下图表示了一个Java栈的模型以及栈帧的组成:
栈帧:是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。
每个栈帧中包括:
局部变量表:用来存储方法中的局部变量(非静态变量、函数形参)。当变量为基本数据类型时,直接存储值,当变量为引用类型时,存储的是指向具体对象的引用。
操作数栈:Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指操作数栈。
指向运行时常量池的引用:存储程序执行时可能用到常量的引用。
方法返回地址:存储方法执行完成后的返回地址。
堆是用来存储对象本身和数组的,在JVM中只有一个堆,因此,堆是被所有线程共享的。
3. 方法区:方法区是一块所有线程共享的内存逻辑区域,在JVM中只有一个方法区,用来存储一些线程可共享的内容,它是线程安全的,多个线程同时访问方法区中同一个内容时,只能有一个线程装载该数据,其它线程只能等待。
方法区可存储的内容有:类的全路径名、类的直接超类的权全限定名、类的访问修饰符、类的类型(类或接口)、类的直接接口全限定名的有序列表、常量池(字段,方法信息,静态变量,类型引用(class))等
4. 本地方法栈:本地方法栈的功能和虚拟机栈是基本一致的,并且也是线程私有的,它们的区别在于虚拟机栈是为执行Java方法服务的,而本地方法栈是为执行本地方法服务的。
有人会疑惑:什么是本地方法?为什么Java还要调用本地方法?
5. 程序计数器:线程私有的。
记录着当前线程所执行的字节码的行号指示器,在程序运行过程中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。
从上面程序运行图我们可以看到,JVM在程序运行时的内存分配有三个地方:
堆
栈
静态方法区
常量区
相应地,每个存储区域都有自己的内存分配策略:
堆式:
栈式
静态
我们已经知道:Java中的数据类型有基本数据类型和引用数据类型,那么这些数据的存储都使用哪一种策略呢?
这里要分以下的情况进行探究:
A. 基本数据类型的局部变量
B. 基本数据类型的成员变量
C. 基本数据类型的静态变量
2. 引用数据类型的存储
1. 基本数据类型的存储
我们分别来研究一下:
A.基本数据类型的局部变量
定义基本数据类型的局部变量以及数据都是直接存储在内存中的栈上,也就是前面说到的“虚拟机栈”,数据本身的值就是存储在栈空间里面。
如上图,在方法内定义的变量直接存储在栈中,如
int age=50; int weight=50; int grade=6;
当我们写“int age=50;”,其实是分为两步的:
int age;//定义变量 age=50;//赋值
首先JVM创建一个名为age的变量,存于局部变量表中,然后去栈中查找是否存在有字面量值为50的内容,如果有就直接把age指向这个地址,如果没有,JVM会在栈中开辟一块空间来存储“50”这个内容,并且把age指向这个地址。因此我们可以知道:
我们声明并初始化基本数据类型的局部变量时,变量名以及字面量值都是存储在栈中,而且是真实的内容。
我们再来看“int weight=50;”,按照刚才的思路:字面量为50的内容在栈中已经存在,因此weight是直接指向这个地址的。由此可见:栈中的数据在当前线程下是共享的。
那么如果再执行下面的代码呢?
weight=40;
当代码中重新给weight变量进行赋值时,JVM会去栈中寻找字面量为40的内容,发现没有,就会开辟一块内存空间存储40这个内容,并且把weight指向这个地址。由此可知:
基本数据类型的数据本身是不会改变的,当局部变量重新赋值时,并不是在内存中改变字面量内容,而是重新在栈中寻找已存在的相同的数据,若栈中不存在,则重新开辟内存存新数据,并且把要重新赋值的局部变量的引用指向新数据所在地址。
B. 基本数据类型的成员变量
成员变量:顾名思义,就是在类体中定义的变量。
看下图:
我们看per的地址指向的是堆内存中的一块区域,我们来还原一下代码:
public class Person{ private int age; private String name; private int grade; //篇幅较长,省略setter getter方法 static void run(){ System.out.println("run...."); }; } //调用 Person per=new Person();
同样是局部变量的age、name、grade却被存储到了堆中为per对象开辟的一块空间中。因此可知:基本数据类型的成员变量名和值都存储于堆中,其生命周期和对象的是一致的。
C. 基本数据类型的静态变量
前面提到方法区用来存储一些共享数据,因此基本数据类型的静态变量名以及值存储于方法区的运行时常量池中,静态变量随类加载而加载,随类消失而消失
2. 引用数据类型的存储:上面提到:堆是用来存储对象本身和数组,而引用(句柄)存放的是实际内容的地址值,因此通过上面的程序运行图,也可以看出,当我们定义一个对象时
Person per=new Person();
实际上,它也是有两个过程:
Person per;//定义变量 per=new Person();//赋值
在执行Person per;时,JVM先在虚拟机栈中的变量表中开辟一块内存存放per变量,在执行per=new Person()时,JVM会创建一个Person类的实例对象并在堆中开辟一块内存存储这个实例,同时把实例的地址值赋值给per变量。因此可见:
对于引用数据类型的对象/数组,变量名存在栈中,变量值存储的是对象的地址,并不是对象的实际内容。
前面已经介绍过形参和实参,也介绍了数据类型以及数据在内存中的存储形式,接下来,就是文章的主题:值传递和引用的传递。
值传递:来看个例子:
public static void valueCrossTest(int age,float weight){ System.out.println("传入的age:"+age); System.out.println("传入的weight:"+weight); age=33; weight=89.5f; System.out.println("方法内重新赋值后的age:"+age); System.out.println("方法内重新赋值后的weight:"+weight); } //测试 public static void main(String[] args) { int a=25; float w=77.5f; valueCrossTest(a,w); System.out.println("方法执行后的age:"+a); System.out.println("方法执行后的weight:"+w); }
输出结果:
传入的age:25 传入的weight:77.5 方法内重新赋值后的age:33 方法内重新赋值后的weight:89.5 方法执行后的age:25 方法执行后的weight:77.5
从上面的打印结果可以看到:
a和w作为实参传入valueCrossTest之后,无论在方法内做了什么操作,最终a和w都没变化。
这是什么造型呢?!!
下面我们根据上面学到的知识点,进行详细的分析:
首先程序运行时,调用mian()方法,此时JVM为main()方法往虚拟机栈中压入一个栈帧,即为当前栈帧,用来存放main()中的局部变量表(包括参数)、操作栈、方法出口等信息,如a和w都是mian()方法中的局部变量,因此可以断定,a和w是躺着mian方法所在的栈帧中
如图:
而当执行到valueCrossTest()方法时,JVM也为其往虚拟机栈中压入一个栈,即为当前栈帧,用来存放valueCrossTest()中的局部变量等信息,因此age和weight是躺着valueCrossTest方法所在的栈帧中,而他们的值是从a和w的值copy了一份副本而得,如图:
因而可以a和age、w和weight对应的内容是不一致的,所以当在方法内重新赋值时,实际流程如图:
也就是说,age和weight的改动,只是改变了当前栈帧(valueCrossTest方法所在栈帧)里的内容,当方法执行结束之后,这些局部变量都会被销毁,mian方法所在栈帧重新回到栈顶,成为当前栈帧,再次输出a和w时,依然是初始化时的内容。
因此:
值传递传递的是真实内容的一个副本,对副本的操作不影响原内容,也就是形参怎么变化,不会影响实参对应的内容。
举个栗子:
先定义一个对象:
public class Person { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
我们写个函数测试一下:
public static void PersonCrossTest(Person person){ System.out.println("传入的person的name:"+person.getName()); person.setName("我是张小龙"); System.out.println("方法内重新赋值后的name:"+person.getName()); } //测试 public static void main(String[] args) { Person p=new Person(); p.setName("我是马化腾"); p.setAge(45); PersonCrossTest(p); System.out.println("方法执行后的name:"+p.getName()); }
输出结果:
传入的person的name:我是马化腾 方法内重新赋值后的name:我是张小龙 方法执行后的name:我是张小龙
可以看出,person经过personCrossTest()方法的执行之后,内容发生了改变,这印证了上面所说的“引用传递”,对形参的操作,改变了实际对象的内容。
那么,到这里就结题了吗?
不是的,没那么简单,
能看得到想要的效果
是因为刚好选对了例子而已!!!
下面我们对上面的例子稍作修改,加上一行代码,
public static void PersonCrossTest(Person person){ System.out.println("传入的person的name:"+person.getName()); person=new Person();//加多此行代码 person.setName("我是张小龙"); System.out.println("方法内重新赋值后的name:"+person.getName()); }
输出结果:
传入的person的name:我是马化腾 方法内重新赋值后的name:我是张小龙 方法执行后的name:我是马化腾
为什么这次的输出和上次的不一样了呢?
看出什么问题了吗?
按照上面讲到JVM内存模型可以知道,对象和数组是存储在Java堆区的,而且堆区是共享的,因此程序执行到main()方法中的下列代码时
Person p=new Person(); p.setName("我是马化腾"); p.setAge(45); PersonCrossTest(p);
JVM会在堆内开辟一块内存,用来存储p对象的所有内容,同时在main()方法所在线程的栈区中创建一个引用p存储堆区中p对象的真实地址,如图:
当执行到PersonCrossTest()方法时,因为方法内有这么一行代码:
person=new Person();
JVM需要在堆内另外开辟一块内存来存储new Person(),假如地址为“xo3333”,那此时形参person指向了这个地址,假如真的是引用传递,那么由上面讲到:引用传递中形参实参指向同一个对象,形参的操作会改变实参对象的改变。
可以推出:实参也应该指向了新创建的person对象的地址,所以在执行PersonCrossTest()结束之后,最终输出的应该是后面创建的对象内容。
然而实际上,最终的输出结果却跟我们推测的不一样,最终输出的仍然是一开始创建的对象的内容。
由此可见:引用传递,在Java中并不存在。
但是有人会疑问:为什么第一个例子中,在方法内修改了形参的内容,会导致原始对象的内容发生改变呢?
这是因为:无论是基本类型和是引用类型,在实参传入形参时,都是值传递,也就是说传递的都是一个副本,而不是内容本身。
有图可以看出,方法内的形参person和实参p并无实质关联,它只是由p处copy了一份指向对象的地址,此时:
p和person都是指向同一个对象。
因此在第一个例子中,对形参p的操作,会影响到实参对应的对象内容。而在第二个例子中,当执行到new Person()之后,JVM在堆内开辟一块空间存储新对象,并且把person改成指向新对象的地址,此时:
p依旧是指向旧的对象,person指向新对象的地址。
所以此时对person的操作,实际上是对新对象的操作,于实参p中对应的对象毫无关系。
因此可见:在Java中所有的参数传递,不管基本类型还是引用类型,都是值传递,或者说是副本传递。
只是在传递过程中:
If you operate on basic data type data, since the original content and the copy store actual values and are in different stack areas, the operation of the formal parameters does not affect the original content.
If you are operating on reference type data, there are two situations. One is that the formal parameters and actual parameters keep pointing to the same object address, then the operation of the formal parameters will affect The contents of the object pointed to by the actual parameter. One is that the formal parameter is changed to point to a new object address (such as reassigning a reference), then the operation of the formal parameter will not affect the content of the object pointed to by the actual parameter.
The above is the detailed content of Detailed explanation of value passing and reference passing in Java. For more information, please follow other related articles on the PHP Chinese website!