Java Paso por Valor vs. Paso por Referencia
Por qué Java siempre es paso por valor, incluso al pasar referencias de objetos, y qué significa esto en la práctica.
Java es paso por valor. Siempre. Todo lo que pasas a un método — un int, un String, un objeto personalizado, un array — el método recibe una copia del valor que le entregaste. Ese valor puede ser un número, o puede ser una referencia a un objeto, pero sigue siendo una copia.
Esto confunde a mucha gente porque un método puede cambiar el contenido de un objeto que pasaste, y ese cambio sí es visible para quien lo llama. Así que parece que el objeto fue pasado por referencia. No lo fue. Lo que se pasó fue una copia de la referencia. Este capítulo muestra exactamente qué significa eso.
Esta página explica cómo se comportan los primitivos, objetos, arrays y strings cuando se pasan a un método, el modelo mental que explica cada caso, y las consecuencias prácticas para tus propios parámetros de método.
Los primitivos son sencillos
Pasar un primitivo copia su valor en el parámetro:
public static void doubleIt(int n) {
n = n * 2;
}
int x = 5;
doubleIt(x);
System.out.println(x); // 5El método tiene su propio n, separado de x. Reasignar n no tiene ningún efecto sobre x. Todo el mundo está de acuerdo en que esto es paso por valor.
Argumentos de objeto: la referencia se copia
Para los tipos de objeto, la variable no contiene el objeto — contiene una referencia (una dirección que apunta al objeto en algún lugar de la memoria). Cuando pasas esa variable a un método, Java copia la referencia, no el objeto:
Caller's variable → [ref to Dog A]
|
v
{ Dog A: name="Rex" }
^
|
Method's parameter → [ref to Dog A]Ambas variables ahora apuntan al mismo objeto Dog. Por eso mutar el objeto a través del parámetro es visible en el lugar de la llamada:
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()); // BuddyPero asignar una nueva referencia al parámetro no cambia la variable del llamador:
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 — unchangedEl método actualizó su propia copia de la referencia. La variable rex del llamador sigue apuntando al Dog original.
El modelo mental de las dos flechas
Siempre que un método recibe un parámetro de objeto, visualiza dos flechas al inicio: una desde la variable del llamador, otra desde el parámetro del método, ambas apuntando al mismo objeto.
- Mutar el objeto a través de cualquiera de las flechas cambia lo que la otra flecha ve.
- Reasignar una flecha para que apunte a otro lugar no tiene efecto sobre la otra flecha.
Esa única regla resuelve cada pregunta de "¿Java es paso por referencia?".
Los arrays siguen la misma regla
Los arrays son objetos, por lo que pasar uno a un método copia la referencia, no el contenido:
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]); // 0El método cambió un elemento a través de su copia de la referencia, y el llamador ve el cambio porque ambas referencias apuntan al mismo array.
Pero reasignar el parámetro a un array completamente nuevo no tiene efecto en el llamador:
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 — unchangedStrings: la inmutabilidad oculta el problema
Los strings también son objetos, pero son inmutables — no existe ningún método que cambie el contenido de un String. Por eso el caso de mutación no puede ocurrir:
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() devuelve un nuevo String; asignarlo a s solo actualiza el parámetro. name sigue apuntando al "ada" original. Para "cambiar" un string, devuelve el nuevo y deja que el llamador lo asigne.
Por qué esto importa
Tres consecuencias prácticas:
-
Un método no puede "sacar" un primitivo ni reasignar la referencia del llamador. Si necesitas ese efecto, devuelve el nuevo valor:
x = doubleIt(x);o usa un objeto envolvente que el llamador pueda leer después de la llamada. -
Un método puede mutar un objeto compartido — lo cual a veces es lo que quieres (llenar un array, poblar una lista) y a veces una sorpresa (los llamadores no esperan que su lista cambie).
-
Copia defensiva. Si un método no debería cambiar el objeto del llamador, o bien no mutes el parámetro, o cópialo primero:
Arrays.copyOf(xs, xs.length). Por el contrario, si devuelves un array o lista interno, los llamadores pueden mutarlo a través de la referencia a menos que devuelvas una copia.
Un ejemplo práctico
Qué viene después
Entiendes cómo fluyen los argumentos individuales hacia los métodos. A veces no sabes de antemano cuántos argumentos suministrará el llamador — piensa en String.format, o un max(...) que acepta cualquier cantidad de valores. Para eso sirven los varargs.