Je viens de découvrir un énorme trou dans mes connaissances Java. Je savais que Java transmet les paramètres par valeur. Je pensais comprendre cela et chaque fois que je devais éditer l'objet champ d'une classe, je crée un champ. Par exemple,
private int mNumber;
void main(){
mNumber = 1;
}
void otherMethod(){
mNumber = 3;
}
Cela changerait l'objet de champ, tout le monde est content.
Je déboguais et je devais revoir les classes à l'intérieur Android SDK et j'ai remarqué ce morceau de code
public static String dumpCursorToString(Cursor cursor) {
StringBuilder sb = new StringBuilder();
dumpCursor(cursor, sb);
return sb.toString();
}
public static void dumpCursor(Cursor cursor, StringBuilder sb) {
sb.append(">>>>> Dumping cursor " + cursor + "\n");
if (cursor != null) {
int startPos = cursor.getPosition();
cursor.moveToPosition(-1);
while (cursor.moveToNext()) {
dumpCurrentRow(cursor, sb);
}
cursor.moveToPosition(startPos);
}
sb.append("<<<<<\n");
}
Ils modifient la variable locale sb
en la passant comme paramètre à une autre méthode.
Dans mon cas, sb
serait un champ et je le changerais d'une autre méthode.
Comment est-ce possible? Une référence (pointeur) est-elle passée en tant que paramètre, nous modifions donc TOUJOURS la valeur source (tout comme nous la transmettons par référence dans d'autres langues)? Mon approche sur le terrain était-elle inutile pendant tout ce temps?
Donc, mon approche pourrait être facilement changée en
void main(){
int number = 1;
otherMethod(number)
}
void otherMethod(int number){
number = 3;
}
Ne vous fâchez pas si cela ressemble à une question de débutant. C'est peut-être le cas, mais je viens de découvrir un énorme trou dans mes connaissances Java.
La déclaration selon laquelle "Java est toujours transmise par valeur" est techniquement correcte, mais elle peut être très trompeuse, car comme vous venez de le constater, lorsque vous passez un objet à une fonction, la fonction peut modifier le contenu de l'objet, il apparaît donc que l'objet a été passé par référence , et - pas par valeur. Alors, qu'est-ce qui se passe?
Pointeurs. C'est ce qui se passe.
L'un des principaux arguments de vente de Java était que "c'est un langage sûr car il est exempt de pointeurs").
Cette affirmation est vraie dans le sens où vous ne pouvez pas faire la plupart des choses dangereuses comme l'arithmétique des pointeurs qui ont historiquement été la cause de nombreux bugs monstrueux dans des langages comme C et C++. Cependant, cette déclaration a également entravé la compréhension de Java pour de nombreux programmeurs débutants.
Vous voyez, Java est en fait plein de pointeurs; les programmeurs débutants ne commencent à donner un sens au langage qu’une fois réalisez qu'en Java tout ce qui n'est pas une primitive est en fait un pointeur; les pointeurs sont partout; vous ne pouvez pas déclarer un objet statiquement, ou sur la pile, ou comme élément d'un tableau comme vous peut faire en C++; la seule façon de déclarer et d'utiliser un objet est par pointeur; même les tableaux d'objets ne sont en fait que des tableaux de pointeurs vers des objets; tout ce qui ressemble à un objet dans Java n'est jamais vraiment un objet, c'est toujours un pointeur vers un objet.
Java établit une distinction claire entre les primitives et les objets. Primitives, (int
, boolean
, float
, etc.,) également appelées valeur types, sont toujours passés par valeur, et ils se comportent exactement comme vous vous y attendez selon la maxime "Java est toujours passe-par-valeur".
Les objets, d'autre part, également appelés types de référence , sont toujours accessibles par référence, c'est-à-dire via un pointeur, de sorte que leur comportement est un peu plus délicat. Lorsque vous appelez une méthode en lui passant un objet, Java ne passe pas vraiment l'objet lui-même; il lui passe un pointeur. Ce pointeur est passé par valeur, donc la maxime "Java est toujours pass-by-value "est toujours valable d'un point de vue strictement technique. Si vous définissez ce pointeur sur null
dans la méthode, le pointeur détenu par l'appelant n'est pas affecté, donc évidemment, ce que vous avez à l'intérieur de la méthode est un mais si vous modifiez l'objet pointé par le pointeur, l'appelant verra toujours ces modifications, ce qui signifie que l'appelant et l'appelé détiennent des références au même objet.
Ainsi, la déclaration "Java est toujours pass-by-value" est vraie d'un point de vue strictement technique, mais finalement pas très utile, selon lequel les objets ne sont jamais vraiment passé n'importe où, seuls les pointeurs vers les objets sont passés.
En réalité, lorsque vous écrivez du code, quand vous pensez à ce que fait le code, il est très utile de penser aux objets comme étant passés à des fonctions, et puisque Java passe des pointeurs (par valeur) sous le capot, les objets eux-mêmes semblent toujours être passés par référence.
En bref, vous confondez la réaffectation d'une variable locale avec la modification d'un objet partagé. sb.append()
ne modifie pas la variable sb
, elle modifie l'objet référencé par la variable. Mais number = 3
modifie la variable number
en lui affectant une nouvelle valeur. Les objets sont partagés entre les appels de méthode, mais pas les variables locales. Par conséquent, l'attribution d'une variable locale à une nouvelle valeur n'aura aucun effet en dehors de la méthode, mais la modification d'un objet le sera. Un peu simplifié: si vous utilisez un point (.
), vous modifiez un objet, pas une variable locale.
Votre confusion est probablement due à une terminologie confuse. "Référence" signifie deux choses différentes dans un contexte différent.
1) Lorsque l'on parle de Java objets et variables, une référence est une "poignée" vers un objet. À proprement parler, une variable ne peut pas contenir un objet, elle contient un référence à un objet. Les références peuvent être copiées, donc deux variables peuvent obtenir une référence au même objet. Par exemple.
StringBuilder a = new StringBuilder();
StringBuilder b = a; // a and b now references the same object
a.writeLine("Hello"); // modify the shared object
b.toString() // -> "Hello"
Lorsqu'un objet est passé dans une méthode en tant que paramètre, c'est également la référence qui est copiée, de sorte que la méthode appelée peut modifier le même objet. Cela s'appelle également "Appeler en partageant".
void main(){
String builder b = new StringBuilder();
otherMethod(b);
b.toString(); // --> "Hello" because the shared object is modified
}
void otherMethod(StringBuilder b){
b.write("Hello"); // modify the shared object
}
Mais l'autre méthode ne peut que modifier l'objet partagé, elle ne peut modifier aucune variable locale dans la méthode appelante. Si la fonction appelée affecte une nouvelle valeur à ses propres paramètres, cela n'affecte pas la fonction appelante.
void main(){
String builder b = new StringBuilder();
b.writeLine("Hello");
otherMethod(b);
b.toString(); // --> "Hello" because the shared object is NOT modified
}
void otherMethod(StringBuilder b){
b = new StringBuilder(); // assign a new object to b
b.write("Goodbye"); // modify the new object
}
Vous devez donc être clair sur la distinction entre modifier un objet partagé (en modifiant des champs ou en appelant une méthode sur l'objet) et attribuer une nouvelle valeur à une variable ou un paramètre local.
2) Lorsque l'on parle de stratégie d'évaluation (comment les paramètres sont passés en fonction) dans la théorie générale du langage informatique, appel par référence et appel par valeur signifie quelque chose de tout à fait différent.
Appel par valeur, qui Java supporte, signifie que les valeurs des arguments sont copiées dans les paramètres lors de l'appel des méthodes. La valeur peut être une référence, c'est pourquoi la fonction appelée obtiendra une référence à l'objet partagé. Confusément Java est censé "passer les références par valeur", ce qui est assez différent de l'appel par référence.
Si un langage prend en charge l'appel par référence, une fonction peut changer la valeur d'une variable locale dans la fonction appelante . Java ne prend pas en charge cela, comme le montre l'exemple ci-dessus. Appel par référence signifie que la fonction appelée obtient une référence à la variable locale utilisée comme argument dans l'appel. Cette notion est complètement étrangère à Java, où nous n'avons tout simplement pas le concept d'une référence à une variable locale. Nous n'avons que des références à des objets, jamais à des variables locales.
Pour montrer comment fonctionne l'appel par référence, nous pouvons regarder C # qui supporte l'appel par référence avec le modificateur ref
:
void main(){
String builder b = new StringBuilder();
b.writeLine("Hello");
otherMethod(ref b);
b.toString(); // --> "Goodbye" because otherMethod changed b to point to the new object
}
void otherMethod(ref StringBuilder b){
b = new StringBuilder(); // assign a new object to b
b.write("Goodbye"); // modify the new object
}
Ici, otherMethod () modifie en fait la variable locale b dans main ().
Qu'en est-il des primitives comme les entiers, les booléens, etc.? Ce sont immuables ce qui signifie qu'ils ne peuvent pas être modifiés comme le peuvent les objets. Puisqu'ils ne peuvent pas être modifiés, la question de savoir s'ils sont partagés ou non est théorique, car il n'y a pas de différence observable qu'ils soient partagés ou copiés.
Remarque: je n'ai pas inventé la terminologie, alors ne blâmez pas le messager. Je préfère de loin les termes appel par partage ou appel par objet qui est plus intuitif pour Java, mais le fait demeure que les termes appel par valeur et l'appel par référence est une terminologie établie et la spécification Java elle-même utilise le terme appel par valeur).