Java: passaggio per valore vs. per riferimento
Perché Java è sempre pass-by-value, anche quando si passano riferimenti a oggetti, e cosa significa in pratica.
Java è pass-by-value. Sempre. Qualunque cosa tu passi a un metodo — un int, una String, un oggetto personalizzato, un array — il metodo riceve una copia del valore che hai fornito. Quel valore può essere un numero oppure un riferimento a un oggetto, ma è comunque una copia.
Questo confonde molte persone perché un metodo può modificare il contenuto di un oggetto passato, e quella modifica è visibile al chiamante. Sembra quindi che l'oggetto sia stato passato per riferimento. Non è così. Ciò che è stato passato è una copia del riferimento. Questo capitolo mostra esattamente cosa significa.
Questa pagina illustra come si comportano tipi primitivi, oggetti, array e stringhe quando vengono passati a un metodo, il modello mentale che spiega ogni caso e le conseguenze pratiche per i tuoi parametri di metodo.
I primitivi sono semplici
Passare un primitivo copia il suo valore nel parametro:
public static void doubleIt(int n) {
n = n * 2;
}
int x = 5;
doubleIt(x);
System.out.println(x); // 5Il metodo ha il proprio n, separato da x. Riassegnare n non ha alcun effetto su x. Tutti concordano che questo è pass-by-value.
Argomenti oggetto: il riferimento viene copiato
Per i tipi oggetto, la variabile non contiene l'oggetto — contiene un riferimento (un indirizzo che punta all'oggetto da qualche parte in memoria). Quando passi quella variabile a un metodo, Java copia il riferimento, non l'oggetto:
Caller's variable → [ref to Dog A]
|
v
{ Dog A: name="Rex" }
^
|
Method's parameter → [ref to Dog A]Entrambe le variabili puntano ora allo stesso oggetto Dog. Ecco perché modificare l'oggetto tramite il parametro è visibile nel punto di chiamata:
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()); // BuddyMa assegnare un nuovo riferimento al parametro non cambia la variabile del chiamante:
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 — unchangedIl metodo ha aggiornato la sua copia del riferimento. Il rex del chiamante punta ancora al Dog originale.
Il modello mentale dei due puntatori
Ogni volta che un metodo riceve un parametro oggetto, visualizza due frecce all'inizio: una dalla variabile del chiamante, una dal parametro del metodo, entrambe che puntano allo stesso oggetto.
- Modificare l'oggetto tramite una delle frecce cambia ciò che vede l'altra freccia.
- Riassegnare una freccia per puntare altrove non ha alcun effetto sull'altra freccia.
Questa singola regola risolve ogni domanda del tipo "Java è pass-by-reference?".
Gli array seguono la stessa regola
Gli array sono oggetti, quindi passarne uno a un metodo copia il riferimento, non il contenuto:
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]); // 0Il metodo ha modificato un elemento tramite la sua copia del riferimento, e il chiamante vede la modifica perché entrambi i riferimenti puntano allo stesso array.
Ma riassegnare il parametro a un array completamente nuovo non ha effetto sul chiamante:
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 — unchangedStringhe: l'immutabilità nasconde il problema
Anche le stringhe sono oggetti, ma sono immutabili — non esiste un metodo che ne cambia il contenuto. Quindi il caso di mutazione non può verificarsi:
public static void uppercase(String s) {
s = s.toUpperCase(); // creates a new String
}
String name = "ada";
uppercase(name);
System.out.println(name); // ada — unchangeds.toUpperCase() restituisce una nuova String; assegnarla a s aggiorna solo il parametro. name punta ancora alla "ada" originale. Per "cambiare" una stringa, restituisci quella nuova e lascia che il chiamante la assegni.
Perché questo è importante
Tre conseguenze pratiche:
-
Un metodo non può modificare un primitivo né riassegnare il riferimento del chiamante. Se hai bisogno di quell'effetto, restituisci il nuovo valore:
x = doubleIt(x);oppure usa un oggetto wrapper che il chiamante può leggere dopo la chiamata. -
Un metodo può mutare un oggetto condiviso — il che a volte è desiderato (riempire un array, popolare una lista) e a volte sorprende (i chiamanti non si aspettano che la loro lista cambi).
-
Copia difensiva. Se un metodo non dovrebbe modificare l'oggetto del chiamante, o non mutare il parametro oppure copiarlo prima:
Arrays.copyOf(xs, xs.length). Al contrario, se restituisci un array o una lista interna, i chiamanti possono mutarli tramite il riferimento a meno che tu non restituisca una copia.
Un esempio pratico
Cosa c'è dopo
Hai capito come i singoli argomenti fluiscono nei metodi. A volte non sai in anticipo quanti argomenti fornirà il chiamante — pensa a String.format, o a un max(...) che accetta qualsiasi numero di valori. Ecco a cosa servono i varargs.