이 기사는 Java의 값 전달 및 참조 전달에 대한 자세한 설명을 제공합니다. 필요한 참조 값이 있으면 도움이 될 것입니다.
이 글은 가장 대중적인 언어의 지루한 기본 지식을 알려주는 것을 목표로 합니다
Java의 기본을 배운 사람이라면 누구나 Java를 처음 접할 때 값 전달과 참조 전달이 어렵다는 것을 알고 있지만 가끔 구문이 기억납니다. 실제 응용에서는 어떻게 사용할 수 있지만 원리를 설명할 수 없는 경우도 있고, 시장에서 논의되는 주제는 논란으로 가득 차 있습니다. 일부 포럼 게시물에서는 Java에는 값 전달만 있다고 말하고 일부 블로그에서는 다음과 같이 말합니다. 둘 다 있습니다. 조금 혼란스럽습니다. 이 주제에 대해 조사하고 신뢰할 수 있는 답변을 얻기 위해 책과 포럼 블로그에서 말하는 내용을 조사해 보겠습니다.
사실 값에 의한 전달과 참조에 의한 전달의 구문과 적용에 대해서는 바이두만 검색해도 꽤 많은 설명과 예시를 얻을 수 있는데, 아마 예제를 보시면 이해가 되실 텐데요. 면접, 이 지식 포인트에 대한 필기 시험을 치르십시오. 질문을 할 때 어떻게 해야 하는지 알 것 같은 느낌이 들고, 답변을 성숙하게 작성했지만 그것이 틀렸다는 것을 발견하거나 어떻게 해야 할지 모른다. 전혀.
이유는 무엇인가요?
그건 지식 포인트에 대한 철저한 이해가 없고 표면만 알고 있기 때문이에요. 문법을 익히는 것은 쉽고, 코드 한 줄을 이해하는 것은 어렵지 않지만, 학습한 지식을 통합하고 이를 직렬로 연결하여 이해하는 것은 매우 어렵습니다. 편집자는 과거로부터 배울 것입니다. 배운 기본 지식부터 기억 모델을 시작으로 가치 전달과 참조 전달의 필수 원리를 단계별로 소개하므로, 글이 비교적 길고 독자들이 많은 지식을 담고 있기를 바랍니다. 나와 함께 견딜 것입니다.
먼저 구문 집합을 검토해 보겠습니다.
1 형식 매개변수: 메서드가 호출될 때 전달되어야 하는 매개변수입니다. 예: a in func(int a) 이는 func가 호출될 때만 A가 의미가 있는 경우에만 사용됩니다. 즉, func 메서드의 실행이 완료된 후 a가 소멸되어 공간이 해제됩니다. 즉, 더 이상 존재하지 않습니다.
2. 실제 매개변수: 메소드 호출 시 전달됩니다. 입력된 실제 값은 메소드 호출 전에 초기화되어 메소드 호출 시 전달됩니다. 예:public static void func(int a){ a=20; System.out.println(a); } public static void main(String[] args) { int a=10;//实参 func(a); }
int a=10;에서는 func 메서드가 호출되기 전에
int a=10;이 생성되고 초기화되었습니다. 따라서 이것은 실제 매개변수입니다.
in func(int a)의 라이프 사이클은 func가 호출될 때만 시작되고 func 호출이 끝난 후에는 JVM에서도 해제되므로 이 a는 형식 매개변수입니다.
소위 데이터 유형은 프로그래밍 언어에서 메모리의 추상 표현입니다. 프로그램이 실행되기 전에 이러한 코드가 존재한다는 것을 알고 있습니다. 하드디스크 여기에서 프로그램이 실행되기 시작하고, 이 코드들은 컴퓨터가 인식할 수 있는 내용으로 변환되어 실행을 위해 메모리에 배치됩니다.
So
그래서 메모리 내 데이터의 저장 형태와 저장 위치는 데이터 유형에 따라 정의됩니다.
그래서
Java의 데이터 유형은 무엇인가요?
기본 유형: 프로그래밍 언어에 내장된 가장 작은 세부 데이터 유형입니다. 여기에는 4개의 주요 범주와 8가지 유형이 포함됩니다.
4개의 정수 유형: byte, short, int, long
2개의 부동 소수점 유형: float, double
1개의 문자 유형: char
1 부울 유형: boolean
인용 유형: 참조는 다음과 같습니다. 핸들이라고도 합니다. 참조 유형은 핸들에 있는 실제 내용의 주소 값을 저장하는 프로그래밍 언어로 정의된 데이터 형식입니다. 그것은 주로 다음을 포함합니다:
Class
Interface
Array
JVM의 프로그램 데이터 관리는 다양한 데이터 유형에 따라 저장 형식과 위치가 다릅니다. 다양한 유형의 데이터를 저장하려면, 먼저 JVM의 메모리 구분과 각 부분의 기능을 이해해야 합니다.
Java 언어 자체는 메모리를 작동할 수 없습니다. 따라서 모든 것을 JVM에서 관리하고 제어합니다. 따라서 Java 메모리 영역 분할은 메모리 분할 이전에도 마찬가지입니다. JVM의 경우 먼저 Java 프로그램의 실행 과정을 아래와 같이 살펴보겠습니다.
그림에서 볼 수 있듯이: Java 코드가 컴파일러에 의해 바이트코드로 컴파일된 후 , JVM은 프로그램 실행 중에 필요한 데이터 및 관련 정보를 저장하기 위해 클래스 로더를 통해 런타임 데이터 영역에 추가되는 메모리 공간(런타임 데이터 영역이라고도 함)을 엽니다.
1. 가상머신 스택
2. 힙
4. 메소드 영역
5. 로컬 메소드 스택
구체적으로 어떻게 사용되는지 살펴보겠습니다. 프로그램 실행 프로세스 어떤 데이터.
虚拟机栈是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中所有的参数传递,不管基本类型还是引用类型,都是值传递,或者说是副本传递。
只是在传递过程中:
기본 데이터 형식의 데이터를 연산하는 경우 원본 콘텐츠와 복사본이 실제 값을 저장하고 서로 다른 스택 영역에 있으므로 형식 매개변수의 연산이 원본 콘텐츠에 영향을 미치지 않습니다.
참조 유형 데이터에 대해 작업하는 경우 두 가지 상황이 있습니다. 하나는 형식 매개변수와 실제 매개변수가 동일한 개체 주소를 계속 가리키는 경우 형식 매개변수의 작업이 가리키는 개체의 내용에 영향을 미칩니다. 실제 매개변수에 의해. 하나는 형식 매개변수가 새 개체 주소를 가리키도록 변경된 경우(예: 참조 재할당) 형식 매개변수의 작업이 실제 매개변수가 가리키는 개체의 내용에 영향을 미치지 않는다는 것입니다.
위 내용은 Java의 값 전달 및 참조 전달에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!