J'ai appris que lorsque vous modifiez une variable dans Java, la variable sur laquelle elle était basée ne change pas
int a = new Integer(5);
int b = a;
b = b + b;
System.out.println(a); // 5 as expected
System.out.println(b); // 10 as expected
J'ai supposé une chose similaire pour les objets. Considérez cette classe.
public class SomeObject {
public String text;
public SomeObject(String text) {
this.setText(text);
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
Après avoir essayé ce code, je suis devenu confus.
SomeObject s1 = new SomeObject("first");
SomeObject s2 = s1;
s2.setText("second");
System.out.println(s1.getText()); // second as UNexpected
System.out.println(s2.getText()); // second as expected
S'il vous plaît, expliquez-moi pourquoi le changement de l'un des objets affecte l'autre. Je comprends que la valeur du texte variable est stockée au même endroit en mémoire pour les deux objets.
Pourquoi les valeurs des variables sont-elles indépendantes mais corrélées pour les objets?
Aussi, comment dupliquer SomeObject, si une simple affectation ne fait pas le travail?
Chaque variable dans Java est un référence. Alors, quand vous le faites
SomeClass s2 = s1;
vous venez de pointer s2
vers le même objet que s1
pointe vers. Vous assignez en fait la valeur de la référence s1 (qui pointe vers une instance de SomeClass
) à s2. Si vous modifiez s1
, s2
sera également modifié (car il pointe sur le même objet).
Il existe une exception, les types primitifs: int, double, float, boolean, char, byte, short, long
. Ils sont stockés par valeur. Donc, lorsque vous utilisez =
, vous n'attribuez que la valeur, mais ils ne peuvent pas pointer sur le même objet (car ils ne sont pas des références). Cela signifie que
int b = a;
définit uniquement la valeur de b
sur la valeur de a
. Si vous changez a
, b
ne changera pas.
À la fin de la journée, tout est assigné par valeur, il s’agit simplement de la valeur de la référence et non de la valeur de l’objet (à l’exception des types primitifs mentionnés ci-dessus).
Donc, dans votre cas, si vous voulez faire une copie de s1
, tu peux le faire comme ça:
SomeClass s1 = new SomeClass("first");
SomeClass s2 = new SomeClass(s1.getText());
Vous pouvez également ajouter à SomeClass
un constructeur de copie qui prend une instance en tant qu'argument et la copie dans sa propre instance.
class SomeClass {
private String text;
// all your fields and methods go here
public SomeClass(SomeClass copyInstance) {
this.text = new String(copyInstance.text);
}
}
Avec cela, vous pouvez copier un objet assez facilement:
SomeClass s2 = new SomeClass(s1);
La réponse de @ brimborium est très bonne (+1 pour lui), mais je voudrais juste en dire davantage à l'aide de chiffres. Prenons d'abord la tâche primitive:
int a = new Integer(5);
int b = a;
b = b + b;
System.out.println(a); // 5 as expected
System.out.println(b); // 10 as expected
int a = new Integer(5);
1- La première instruction crée un objet Integer de valeur 5. Ensuite, lors de l'assignation à la variable a
, l'objet Integer sera déconditionné et stocké dans a
en tant que primitive.
Après avoir créé l'objet Integer et avant l'affectation:
Après la mission:
int b = a;
2- Cela va juste lire la valeur de a
puis la stocker dans b
.
(L'objet Integer est maintenant éligible pour la récupération de place, mais pas encore pour l'instant.)
b = b + b;
3- Ceci lit la valeur de b
deux fois, additionne-les et place la nouvelle valeur dans b
.
D'autre part:
SomeObject s1 = new SomeObject("first");
SomeObject s2 = s1;
s2.setText("second");
System.out.println(s1.getText()); // second as UNexpected
System.out.println(s2.getText()); // second as expected
SomeObject s1 = new SomeObject("first");
1- Crée une nouvelle instance de SomeObject
class et l’assigne à la référence s1
.
SomeObject s2 = s1;
2- Cela fera la référence s2
pointe sur l'objet que s1
pointe vers.
s2.setText("second");
3- Lorsque vous utilisez des setters sur une référence, cela modifiera l'objet sur lequel la référence pointe.
System.out.println(s1.getText());
System.out.println(s2.getText());
4- Les deux doivent imprimer second
, puisque les deux références s1
et s2
font référence au même objet (comme indiqué dans la figure précédente).
Quand tu fais ça
SomeObject s1 = new SomeObject("first");
SomeObject s2 = s1;
vous avez 2 références au même objet. Ce qui signifie que quel que soit l'objet de référence que vous utilisez, les modifications que vous apportez seront visibles lors de l'utilisation de la deuxième référence.
Pensez-y comme ceci: vous avez un téléviseur dans la pièce, mais deux télécommandes: peu importe la télécommande que vous utilisez, vous apporterez toujours des modifications au même objet sous-jacent (la télévision).
Quand tu écris:
SomeObject s1 = new SomeObject("first");
s1
N'est pas un SomeObject
. C'est un référence à l'objet SomeObject
.
Par conséquent, si vous affectez s2 à s1, vous affectez simplement des références/descripteurs et l'objet sous-jacent est identique. Ceci est important en Java. Tout est passe-par-valeur, mais vous ne transmettez jamais d'objets - seulement des références à des objets.
Ainsi, lorsque vous affectez s1 = s2
, Puis appelez une méthode qui modifie un objet sur s2
, L'objet sous-jacent est modifié et il est visible lorsque vous référencez l'objet via s1
.
Ceci est un argument pour immuabilité dans les objets. En rendant les objets immuables, ils ne changeront pas sous vous et se comporteront donc de manière plus prévisible. Si vous souhaitez copier un objet, la méthode la plus simple et la plus pratique consiste à écrire une méthode copy()
qui crée simplement une nouvelle version et la copie sur les champs. Vous pouvez faire des copies intelligentes en utilisant la sérialisation/réflexion, etc., mais c'est évidemment plus complexe.
De Dix erreurs principales Java Les programmeurs font):
6 - Confusion sur le passage par valeur et par renvoi
Cela peut être un problème frustrant à diagnostiquer, car lorsque vous examinez le code, vous pouvez être sûr qu'il passe par référence, mais vous constatez qu'il est réellement transmis par valeur. Java utilise les deux), vous devez donc savoir quand vous passez par valeur et quand vous passez par référence.
Lorsque vous transmettez un type de données primitif, tel que char, int, float ou double, à une fonction, vous passez par valeur. Cela signifie qu'une copie du type de données est dupliquée et transmise à la fonction. Si la fonction choisit de modifier cette valeur, elle ne modifiera que la copie. Une fois que la fonction est terminée et que le contrôle est retourné à la fonction renvoyée, la variable "réelle" reste inchangée et aucune modification n'a été enregistrée. Si vous devez modifier un type de données primitif, faites-en une valeur de retour pour une fonction ou placez-le dans un objet.
Comme int
est un type primitif, int b = a;
est une copie par valeur, ce qui signifie que a
et b
sont deux objets différents, mais avec la même valeur.
SomeObject s2 = s1;
faire s1
et s2
deux références d'un même objet, donc si vous en modifiez une, l'autre sera également modifiée.
Une bonne solution consiste à implémenter un autre constructeur comme celui-ci:
public class SomeObject{
public SomeObject(SomeObject someObject) {
setText(someObject.getText());
}
// your code
}
Ensuite, utilisez-le comme ça:
SomeObject s2 = new SomeObject(s1);
Dans votre code s1
Et s2
Sont l'objet identique (vous n'avez créé qu'un seul objet avec new
) et vous laissez s2
pointez sur le même objet à la ligne suivante. Par conséquent, lorsque vous modifiez text
, il est modifié à la fois si vous référencez la valeur via s1
Et s2
.
L'opérateur +
Sur Integers crée un objet nouvea, il ne modifie pas l'objet existant (l'ajout de 5 + 5 ne donne donc pas à 5 la nouvelle valeur 10 ...).
En effet, la JVM stocke un pointeur sur s1
. Lorsque vous appelez s2 = s1
, Vous dites en gros que le pointeur s2
(C'est-à-dire l'adresse mémoire) a la même valeur que celui de s1
. Puisqu'ils désignent tous deux le même endroit en mémoire, ils représentent exactement la même chose.
L'opérateur =
Assigne des valeurs de pointeur. Il ne copie pas l'objet.
Le clonage d'objets est une tâche compliquée en soi. Chaque objet a une méthode clone () que vous pourriez être tenté d’utiliser, mais il effectue une copie superficielle (ce qui signifie qu’il crée une copie de l’objet de niveau supérieur mais que les objets qu’il contient ne sont pas clonés). Si vous voulez jouer avec des copies d’objets, lisez certainement --- Effective Java de Joshua Bloch.
Commençons par votre deuxième exemple:
Cette première instruction assigne un nouvel objet à s1
SomeObject s1 = new SomeObject("first");
Lorsque vous effectuez l'affectation dans la deuxième instruction (SomeObject s2 = s1
), Vous indiquez à s2
De pointer sur le même objet s1
, Ce qui fait que vous avez deux références à le même objet.
Notez que vous n'avez pas dupliqué le SomeObject
, mais que deux variables pointent simplement sur le même objet. Donc, si vous modifiez s1
Ou s2
, Vous modifiez en fait le même objet (notez que si vous faites quelque chose comme s2 = new SomeObject("second")
, ils se dirigeraient maintenant vers différents objets).
Dans votre premier exemple, a
et b
sont des valeurs primitives. La modification de l'une n'affectera donc pas l'autre.
Sous le capot de Java, tous les objets fonctionnent en utilisant pass par valeur. Pour les objets, vous transmettez la "valeur" que vous transmettez est l'emplacement de l'objet en mémoire (il semble donc que l'effet de transmission par référence soit similaire). Les primitives se comportent différemment et transmettent simplement une copie de la valeur.
int a = new Integer(5)
Dans le cas ci-dessus, un nouvel Integer est créé. Ici, Integer est un type non primitif et la valeur qu'il contient est convertie (en un int) et affectée à un int 'a'.
SomeObject s1 = new SomeObject("first");
SomeObject s2 = s1;
Dans ce cas, s1 et s2 sont tous deux des types de référence. Ils ne sont pas créés pour contenir une valeur similaire aux types primitifs, ils contiennent plutôt la référence d'un objet. Par souci de compréhension, nous pouvons penser à la référence comme à un lien qui indique où je peux trouver l’objet référencé.
Ici, la référence s1 nous indique où nous pouvons trouver la valeur de "first" (qui est en réalité stockée dans la mémoire de l'ordinateur dans une instance de SomeObject). En autres mots, s1 est l'adresse de l'objet de classe "SomeObject". Par la mission suivante -
SomeObject s2 = s1;
nous ne faisons que copier la valeur stockée dans s1 dans s2 et nous savons maintenant que s1 contient l'adresse de la chaîne "first". Après cette vérification, les deux println () produisent le même résultat, car s1 et s2 référençant le même objet.
En plus du constructeur de copie, vous pouvez copier un objet avec la méthode clone () si vous êtes un utilisateur Java. Le clonage peut être utilisé de la manière suivante:
SomeObject s3 = s1.clone();
Pour plus d'informations sur clone (), il s'agit d'un lien utile http://en.wikipedia.org/wiki/Clone_%28Java_method%29
Les réponses ci-dessus expliquent le comportement que vous observez.
En réponse à "Aussi, comment dupliquer SomeObject, si une simple affectation ne fait pas le travail?" - essayez de chercher cloneable
(c'est une interface Java qui fournit un moyen de copier des objets) et 'copy constructors
'(une alternative et peut-être une meilleure approche)
L'attribution d'objet à une référence ne clone pas votre objet. Les références sont comme des pointeurs. Ils pointent sur un objet et lorsque des opérations sont appelées, cela se fait sur l'objet pointé par le pointeur. Dans votre exemple, s1 et s2 pointent sur le même objet et les paramètres modifient l'état du même objet et les modifications sont visibles dans toutes les références.
Modifiez votre classe afin de créer une nouvelle référence au lieu d'utiliser la même:
public class SomeObject{
public String text;
public SomeObject(String text){
this.setText(text);
}
public String getText(){
return text;
}
public void setText(String text){
this.text = new String(text);
}
}
Vous pouvez utiliser quelque chose comme ceci (je ne prétends pas avoir la solution idéale):
public class SomeObject{
private String text;
public SomeObject(String text){
this.text = text;
}
public SomeObject(SomeObject object) {
this.text = new String(object.getText());
}
public String getText(){
return text;
}
public void setText(String text){
this.text = text;
}
}
Usage:
SomeObject s1 = new SomeObject("first");
SomeObject s2 = new SomeObject(s1);
s2.setText("second");
System.out.println(s1.getText()); // first
System.out.println(s2.getText()); // second
Deuxième ligne (SomeObject s2 = s1;
) assigne simplement la seconde variable à la première. Cela conduit à une seconde variable pointant sur la même instance d'objet que la première.
C'est parce que s1
et s2
ne fonctionne que comme référence à vos objets. Lors de l'attribution de s2 = s1
vous n'attribuez que la référence, ce qui signifie que les deux pointeront vers le même objet en mémoire (l'objet qui contient le texte actuel "en premier").
Quand vous faites un setter maintenant, que ce soit sur s1 ou s2, les deux vont modifier le même objet.
Lorsque vous affectez une variable à un objet et que vous vous en objectez, vous affectez réellement la référence à cet objet. Donc, les deux variables s1
et s2
font référence au même objet.
Depuis les objets ont été référencés par une référence. Donc, si vous écrivez s2 = s1, seule la référence sera copiée. C'est comme en C où vous ne gérez que des adresses de mémoire. Et si vous copiez l'adresse d'une mémoire et modifiez la valeur derrière cette adresse, les deux pointeurs (références) modifieront la valeur à ce stade de la mémoire.