Maison  >  Article  >  Java  >  Explication détaillée du passage de valeur et du passage de référence en Java

Explication détaillée du passage de valeur et du passage de référence en Java

不言
不言avant
2018-10-23 15:09:412702parcourir

Cet article vous apporte une explication détaillée du passage de valeurs et du passage de références en Java. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer.

Cet article vise à transmettre les connaissances de base ennuyeuses dans le langage le plus populaire

Quiconque a appris les bases de Java sait que le passage de valeurs et le passage de références sont une difficulté lors du premier contact avec Java. Parfois, je me souviens de la syntaxe mais je ne sais pas comment l'utiliser réellement. Parfois, je peux l'utiliser mais je ne peux pas expliquer le principe. De plus, le sujet abordé sur le marché est plein de controverses : certains messages du forum disent que Java n'a que de la valeur. en passant, et certains blogs disent que c'est les deux ; oui, cela rend les gens un peu confus. Discutons de ce sujet et effectuons des recherches sur les déclarations dans les livres et les blogs du forum pour obtenir une réponse fiable.

En fait, pour la syntaxe et l'application du passage de valeurs et du passage de références, vous pouvez trouver un nombre considérable d'explications et d'exemples sur Baidu. Peut-être que vous le comprendrez en regardant les exemples, mais lorsque vous y participerez. un entretien, faites ceci Lorsque vous répondez aux questions du test écrit sur les points de connaissance, vous sentez que vous savez comment le faire et écrivez les réponses avec maturité, mais vous constatez qu'elles sont fausses, ou vous ne savez pas comment les faire à tous.

Quelle est la raison ?

C’est parce que vous n’avez pas une compréhension approfondie des points de connaissance et que vous ne connaissez que la surface. Il est facile de se familiariser avec une grammaire, et il n'est pas difficile de comprendre une ligne de code, mais il est très difficile d'intégrer et de relier les connaissances acquises pour comprendre. Ici, concernant le transfert de valeur et le transfert de référence, l'éditeur apprendra. du passé À partir des connaissances de base que j'ai acquises, à partir du modèle de mémoire, je présenterai étape par étape les principes essentiels du transfert de valeurs et du transfert de référence. Par conséquent, l'article est plus long et contient plus de points de connaissances, j'espère que les lecteurs le feront. supporte-moi.

1. Paramètres formels

Examinons d'abord un ensemble de syntaxe :

1. Paramètres formels : paramètres qui doivent être transmis lorsque la méthode est appelée, tels que. : Le a in func(int a) n'a de sens que lorsque func est appelé, c'est-à-dire qu'il se verra allouer de l'espace mémoire. Une fois la méthode func exécutée, a sera détruit pour libérer de l'espace, c'est-à-dire qu'il ne le sera plus. exist

2. Paramètres réels : la valeur réelle transmise lorsque la méthode est appelée. Elle a été initialisée avant l'appel de la méthode et est transmise lorsque la méthode est appelée.

Par exemple :

public static void func(int a){
 a=20;
 System.out.println(a);
}
public static void main(String[] args) {
 int a=10;//实参
 func(a);
}

Dans l'exemple, a in
int a=10; a été créé et initialisé avant d'être appelé. Lorsque la méthode func est appelée, elle l'est. Transmis en tant que paramètre, ce a est donc un paramètre réel.
Le cycle de vie de a in func(int a) ne commence que lorsque func est appelé, et une fois l'appel de func terminé, il est également publié par la JVM, donc ceci a est un paramètre formel.

2. Types de données Java

Le soi-disant type de données est une expression abstraite de la mémoire dans un langage de programmation. Nous savons qu'un programme est composé de fichiers de code et de ressources statiques. Le programme est Avant de s'exécuter, ces codes existent sur le disque dur, et lorsque le programme démarre, ces codes seront convertis en contenu que l'ordinateur peut reconnaître et placés dans la mémoire pour exécution.
Donc

les types de données sont essentiellement utilisés pour définir la forme de stockage du même type de données dans un langage de programmation, c'est-à-dire pour déterminer comment les bits représentant ces valeurs sont stockés dans la mémoire de l'ordinateur.

Par conséquent, la forme de stockage et l'emplacement de stockage des données en mémoire sont définis en fonction du type de données.
Alors
Quels sont les types de données de Java ?

Types de base : les plus petits types de données granulaires intégrés à un langage de programmation. Il comprend quatre catégories et huit types :

4 types entiers : byte, short, int, long
2 types à virgule flottante : float, double
1 type de caractère : char
1 type booléen : booléen

Type de référence : la référence est également appelée handle. Le type de référence est un formulaire de données défini dans un langage de programmation qui stocke la valeur d'adresse de l'adresse du contenu réel dans le handle. Il comprend principalement :

Classe
Interface
Array

Avec les types de données, la gestion des données du programme par JVM est standardisée. Différents types de données, leurs formes de stockage et leurs emplacements sont différents. Si vous souhaitez savoir comment la JVM stocke différents types de données, vous devez d'abord comprendre la division de la mémoire de la JVM et les fonctions de chaque partie.

3. La division et les fonctions de la mémoire JVM

Le langage Java lui-même ne peut pas faire fonctionner la mémoire. Tout ce qu'il contient est géré et contrôlé par la JVM, donc la division des zones de mémoire Java l'est également. est la division en zones de la JVM Avant de parler de la division mémoire de la JVM, jetons d'abord un coup d'œil au processus d'exécution du programme Java, comme indiqué ci-dessous :

Explication détaillée du passage de valeur et du passage de référence en Java

Comme le montre l'image : une fois le code Java compilé en bytecode par le compilateur, la JVM ouvre un espace mémoire (également appelé zone de données d'exécution) et l'ajoute à la zone de données d'exécution via le chargeur de classe pour le stockage. Données et informations associées nécessaires à l'exécution du programme. Dans cette zone de données, il se compose des parties suivantes :

1. 🎜>2. Tas

3. Compteur de programme

4. Zone de méthode

5. chaque partie et comment elle est utilisée pour stocker le processus d'exécution du programme. Quelles données.


1. 虚拟机栈

虚拟机栈是Java方法执行的内存模型,栈中存放着栈帧,每个栈帧分别对应一个被调用的方法,方法的调用过程对应栈帧在虚拟机中入栈到出栈的过程。

栈是线程私有的,也就是线程之间的栈是隔离的;当程序中某个线程开始执行一个方法时就会相应的创建一个栈帧并且入栈(位于栈顶),在方法结束后,栈帧出栈。

下图表示了一个Java栈的模型以及栈帧的组成:

Explication détaillée du passage de valeur et du passage de référence en Java

栈帧:是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。

每个栈帧中包括:

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

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

  3. 指向运行时常量池的引用:存储程序执行时可能用到常量的引用。

  4. 方法返回地址:存储方法执行完成后的返回地址。

2. 堆:

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

3. 方法区:

方法区是一块所有线程共享的内存逻辑区域,在JVM中只有一个方法区,用来存储一些线程可共享的内容,它是线程安全的,多个线程同时访问方法区中同一个内容时,只能有一个线程装载该数据,其它线程只能等待。

方法区可存储的内容有:类的全路径名、类的直接超类的权全限定名、类的访问修饰符、类的类型(类或接口)、类的直接接口全限定名的有序列表、常量池(字段,方法信息,静态变量,类型引用(class))等

4. 本地方法栈:

本地方法栈的功能和虚拟机栈是基本一致的,并且也是线程私有的,它们的区别在于虚拟机栈是为执行Java方法服务的,而本地方法栈是为执行本地方法服务的。

有人会疑惑:什么是本地方法?为什么Java还要调用本地方法?

5. 程序计数器:

线程私有的。
记录着当前线程所执行的字节码的行号指示器,在程序运行过程中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。

4. 数据如何在内存中存储?

从上面程序运行图我们可以看到,JVM在程序运行时的内存分配有三个地方:

  • 静态方法区

  • 常量区

相应地,每个存储区域都有自己的内存分配策略:

  • 堆式:

  • 栈式

  • 静态

我们已经知道:Java中的数据类型有基本数据类型和引用数据类型,那么这些数据的存储都使用哪一种策略呢?
这里要分以下的情况进行探究:

1. 基本数据类型的存储:
  • A. 基本数据类型的局部变量

  • B. 基本数据类型的成员变量

  • C. 基本数据类型的静态变量

2. 引用数据类型的存储

1. 基本数据类型的存储

我们分别来研究一下:

A.基本数据类型的局部变量

  1. 定义基本数据类型的局部变量以及数据都是直接存储在内存中的栈上,也就是前面说到的“虚拟机栈”,数据本身的值就是存储在栈空间里面。

    Explication détaillée du passage de valeur et du passage de référence en Java


如上图,在方法内定义的变量直接存储在栈中,如

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. 基本数据类型的成员变量

成员变量:顾名思义,就是在类体中定义的变量。
看下图:

Explication détaillée du passage de valeur et du passage de référence en Java

我们看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变量。因此可见:
对于引用数据类型的对象/数组,变量名存在栈中,变量值存储的是对象的地址,并不是对象的实际内容。

6. 值传递和引用传递

前面已经介绍过形参和实参,也介绍了数据类型以及数据在内存中的存储形式,接下来,就是文章的主题:值传递和引用的传递。

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

来看个例子:

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方法所在的栈帧中
如图:

Explication détaillée du passage de valeur et du passage de référence en Java

而当执行到valueCrossTest()方法时,JVM也为其往虚拟机栈中压入一个栈,即为当前栈帧,用来存放valueCrossTest()中的局部变量等信息,因此age和weight是躺着valueCrossTest方法所在的栈帧中,而他们的值是从a和w的值copy了一份副本而得,如图:

Explication détaillée du passage de valeur et du passage de référence en Java

因而可以a和age、w和weight对应的内容是不一致的,所以当在方法内重新赋值时,实际流程如图:

Explication détaillée du passage de valeur et du passage de référence en Java

也就是说,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对象的真实地址,如图:

Explication détaillée du passage de valeur et du passage de référence en Java

当执行到PersonCrossTest()方法时,因为方法内有这么一行代码:

person=new Person();

JVM需要在堆内另外开辟一块内存来存储new Person(),假如地址为“xo3333”,那此时形参person指向了这个地址,假如真的是引用传递,那么由上面讲到:引用传递中形参实参指向同一个对象,形参的操作会改变实参对象的改变

可以推出:实参也应该指向了新创建的person对象的地址,所以在执行PersonCrossTest()结束之后,最终输出的应该是后面创建的对象内容。

然而实际上,最终的输出结果却跟我们推测的不一样,最终输出的仍然是一开始创建的对象的内容。

由此可见:引用传递,在Java中并不存在。

但是有人会疑问:为什么第一个例子中,在方法内修改了形参的内容,会导致原始对象的内容发生改变呢?

这是因为:无论是基本类型和是引用类型,在实参传入形参时,都是值传递,也就是说传递的都是一个副本,而不是内容本身。

Explication détaillée du passage de valeur et du passage de référence en Java

有图可以看出,方法内的形参person和实参p并无实质关联,它只是由p处copy了一份指向对象的地址,此时:

p和person都是指向同一个对象

因此在第一个例子中,对形参p的操作,会影响到实参对应的对象内容。而在第二个例子中,当执行到new Person()之后,JVM在堆内开辟一块空间存储新对象,并且把person改成指向新对象的地址,此时:

p依旧是指向旧的对象,person指向新对象的地址。

所以此时对person的操作,实际上是对新对象的操作,于实参p中对应的对象毫无关系

结语

因此可见:在Java中所有的参数传递,不管基本类型还是引用类型,都是值传递,或者说是副本传递。
只是在传递过程中:

Si vous opérez sur des données de type de données de base, puisque le contenu original et la copie stockent des valeurs réelles et se trouvent dans des zones de pile différentes, le fonctionnement des paramètres formels n'affecte pas le contenu original.

Si vous opérez sur des données de type référence, il existe deux situations. La première est que les paramètres formels et les paramètres réels continuent de pointer vers la même adresse d'objet, alors le fonctionnement des paramètres formels le fera. affecter Le contenu de l'objet pointé par le paramètre réel. La première est que le paramètre formel est modifié pour pointer vers une nouvelle adresse d'objet (comme la réaffectation d'une référence), alors le fonctionnement du paramètre formel n'affectera pas le contenu de l'objet pointé par le paramètre réel.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer