W3docs

Java : passage par valeur vs. passage par référence

Pourquoi Java est toujours passage par valeur, même avec des références d'objets, et ce que cela implique en pratique.

Java est passage par valeur. Toujours. Quoi que vous passiez à une méthode — un int, un String, un objet personnalisé, un tableau — la méthode reçoit une copie de la valeur que vous lui avez transmise. Cette valeur peut être un nombre, ou une référence vers un objet, mais c'est quand même une copie.

Cela déroute beaucoup de gens parce qu'une méthode peut modifier le contenu d'un objet passé en paramètre, et ce changement est visible par l'appelant. On a donc l'impression que l'objet a été passé par référence. Ce n'est pas le cas. Ce qui a été passé, c'est une copie de la référence. Ce chapitre explique précisément ce que cela signifie.

Cette page explique comment les primitives, les objets, les tableaux et les chaînes se comportent chacun quand ils sont transmis à une méthode, le modèle mental qui explique chaque cas, ainsi que les conséquences pratiques pour vos propres paramètres de méthode.

Les primitives, c'est simple

Passer une primitive copie sa valeur dans le paramètre :

public static void doubleIt(int n) {
  n = n * 2;
}

int x = 5;
doubleIt(x);
System.out.println(x);    // 5

La méthode possède son propre n, distinct de x. Réassigner n n'a aucun effet sur x. Tout le monde s'accorde à dire que c'est du passage par valeur.

Arguments objets : la référence est copiée

Pour les types objets, la variable ne contient pas l'objet — elle contient une référence (une adresse pointant vers l'objet quelque part en mémoire). Quand vous passez cette variable à une méthode, Java copie la référence, pas l'objet :

Caller's variable        →   [ref to Dog A]
                                  |
                                  v
                              { Dog A: name="Rex" }
                                  ^
                                  |
Method's parameter       →   [ref to Dog A]

Les deux variables pointent maintenant vers le même objet Dog. C'est pourquoi muter l'objet via le paramètre est visible au niveau de l'appelant :

public static void rename(Dog d) {
  d.setName("Buddy");           // mutates the shared object
}

Dog rex = new Dog("Rex");
rename(rex);
System.out.println(rex.getName());   // Buddy

Mais assigner une nouvelle référence au paramètre ne modifie pas la variable de l'appelant :

public static void replace(Dog d) {
  d = new Dog("Buddy");         // parameter now points at a new Dog
}

Dog rex = new Dog("Rex");
replace(rex);
System.out.println(rex.getName());   // Rex — unchanged

La méthode a mis à jour sa propre copie de la référence. La variable rex de l'appelant pointe toujours vers le Dog original.

Le modèle mental des deux pointeurs

Chaque fois qu'une méthode reçoit un paramètre objet, visualisez deux flèches au départ : une depuis la variable de l'appelant, une depuis le paramètre de la méthode, toutes deux pointant vers le même objet.

  • Muter l'objet via l'une ou l'autre flèche modifie ce que l'autre flèche voit.
  • Réassigner une flèche pour qu'elle pointe ailleurs n'a aucun effet sur l'autre flèche.

Cette règle unique résout toutes les questions du type « Java est-il passage par référence ? ».

Les tableaux suivent la même règle

Les tableaux sont des objets, donc passer un tableau à une méthode copie la référence, pas le contenu :

public static void zeroFirst(int[] xs) {
  xs[0] = 0;                    // mutates the shared array
}

int[] data = {1, 2, 3};
zeroFirst(data);
System.out.println(data[0]);    // 0

La méthode a modifié un élément via sa copie de la référence, et l'appelant voit le changement parce que les deux références pointent vers le même tableau.

Mais réassigner le paramètre à un tout nouveau tableau n'a aucun effet sur l'appelant :

public static void resetArray(int[] xs) {
  xs = new int[]{0, 0, 0};      // parameter only
}

int[] data = {1, 2, 3};
resetArray(data);
System.out.println(data[0]);    // 1 — unchanged

Les chaînes : l'immuabilité masque le problème

Les chaînes sont aussi des objets, mais elles sont immuables — il n'existe aucune méthode qui modifie le contenu d'un String. La mutation est donc impossible :

public static void uppercase(String s) {
  s = s.toUpperCase();          // creates a new String
}

String name = "ada";
uppercase(name);
System.out.println(name);       // ada — unchanged

s.toUpperCase() retourne un nouveau String ; l'assigner à s ne met à jour que le paramètre. name pointe toujours vers l'"ada" original. Pour « modifier » une chaîne, retournez la nouvelle valeur et laissez l'appelant l'assigner.

Pourquoi cela est important

Trois conséquences pratiques :

  1. Une méthode ne peut pas « sortir » une primitive ni réassigner la référence de l'appelant. Si vous avez besoin de cet effet, retournez la nouvelle valeur : x = doubleIt(x); ou utilisez un objet enveloppeur que l'appelant peut lire après l'appel.

  2. Une méthode peut muter un objet partagé — ce qui est parfois voulu (remplir un tableau, alimenter une liste) et parfois surprenant (les appelants ne s'attendent pas à ce que leur liste change).

  3. La copie défensive. Si une méthode ne doit pas modifier l'objet de l'appelant, soit ne mutez pas le paramètre, soit copiez-le d'abord : Arrays.copyOf(xs, xs.length). Inversement, si vous retournez un tableau ou une liste interne, les appelants peuvent le muter via la référence, sauf si vous retournez une copie.

Un exemple complet

java— editable, runs on the server

Et ensuite

Vous comprenez maintenant comment les arguments individuels sont transmis aux méthodes. Parfois, vous ne savez pas à l'avance combien d'arguments l'appelant fournira — pensez à String.format, ou à un max(...) qui accepte n'importe quel nombre de valeurs. C'est là qu'interviennent les varargs.

Pratique

Pratique
Une méthode reçoit un paramètre objet et le réassigne : param = new Thing(). Que voit l'appelant ?
Une méthode reçoit un paramètre objet et le réassigne : param = new Thing(). Que voit l'appelant ?
Was this page helpful?