¿Java pasa por referencia o pasa por valor?

La pregunta aparece mucho en Internet, y muchas respuestas se basan en la idea errónea de cómo Java trata los tipos primitivos y de referencia. En este artículo, vamos a desacreditar el concepto erróneo.

Introducción

La pregunta aparece mucho tanto en Internet como cuando alguien desea verificar su conocimiento sobre cómo Java trata las variables:

¿Java "pass-by-reference" o "pass-by-value" al pasar argumentos a métodos?

Parece una pregunta sencilla (lo es), pero mucha gente se equivoca al decir:

Los objetos se pasan por referencia y los tipos primitivos se pasan por valor.

Una afirmación correcta sería:

Las referencias de objetos se pasan por valor, al igual que los tipos primitivos. Así, Java pasa por valor, no por referencia, en todos los casos.

Esto puede sonar poco intuitivo para algunos, ya que es común que las conferencias muestren la diferencia entre un ejemplo como este:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
    int x = 0;
    incrementNumber(x);
    System.out.println(x);
}

public static void incrementNumber(int x) {
    x += 1;
}

y un ejemplo como este:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static void main(String[] args) {
    Number x = new Number(0);
    incrementNumber(x);
    System.out.println(x);
}

public static void incrementNumber(Number x) {
    x.value += 1;
}

public class Number {
    int value;
    // Constructor, getters and setters
}

El primer ejemplo imprimirá:

1
0

Mientras que el segundo ejemplo imprimirá:

1
1

A menudo se entiende que la razón de esta diferencia es "pass-by-value" (primer ejemplo, se pasa el valor copiado de x y cualquier operación en la copia no se reflejará en el original value) y "pass-by-reference" (segundo ejemplo, se pasa una referencia y, cuando se modifica, refleja el objeto original).

En las secciones siguientes, explicaremos por qué esto es incorrecto.

Cómo trata Java las variables

Vamos a repasar cómo Java trata las variables, ya que esa es la clave para comprender el concepto erróneo. El concepto erróneo se basa en hechos reales, pero un poco distorsionado.

Tipos primitivos

Java es un lenguaje escrito estáticamente. Requiere que primero declaremos una variable, luego la inicialicemos, y solo entonces podemos usarla:

1
2
3
4
5
// Declaring a variable and initializing it with the value 5
int i = 5;

// Declaring a variable and initializing it with a value of false
boolean isAbsent = false;

Puede dividir el proceso de declaración e inicialización:

1
2
3
4
5
6
7
// Declaration
int i;
boolean isAbsent;

// Initialization
i = 5;
isAbsent = false;

Pero si intenta usar una variable no inicializada:

1
2
3
4
5
6
public static void printNumber() {
    int i;
    System.out.println(i);
    i = 5;
    System.out.println(i);
}

Recibe un mensaje de error:

1
2
Main.java:10: error: variable i might not have been initialized
System.out.println(i);

No hay valores predeterminados para tipos primitivos locales como i. Sin embargo, si define variables globales como i en este ejemplo:

1
2
3
4
5
6
7
static int i;

public static void printNumber() {
    System.out.println(i);
    i = 5;
    System.out.println(i);
}

Ejecutando esto, verás el siguiente resultado:

1
2
0
5

La variable i salió como 0, aunque todavía no estaba asignada.

Cada tipo primitivo tiene un valor predeterminado, si se define como una variable global, y normalmente será “0” para tipos basados ​​en números y “falso” para valores booleanos.

Hay 8 tipos primitivos en Java:

  • byte: Rangos de -128 a 127 inclusive, entero de 8 bits con signo
  • corto: varía de -32,768 a 32,767 inclusive, entero de 16 bits con signo
  • int: varía de -2,147,483,648 a 2,147,483,647 inclusive, entero de 32 bits con signo
  • largo: Rangos de -2^31^ a 2^31^-1, inclusive, entero de 64 bits con signo
  • float: precisión simple, 32 bits IEEE754 entero de punto flotante con 6-7 dígitos significativos
  • doble: Número entero de punto flotante IEEE 754 de precisión doble de 64 bits, con 15 dígitos significativos
  • booleano: valores binarios, verdadero o falso
  • char: varía de 0 a 65,536 inclusive, entero sin signo de 16 bits que representa un carácter Unicode

Paso de tipos primitivos {#paso de tipos primitivos}

Cuando pasamos tipos primitivos como argumentos de método, se pasan por valor. O más bien, su valor se copia y luego se pasa al método.

Volvamos al primer ejemplo y analicemos:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
    int x = 0;
    incrementNumber(x);
    System.out.println(x);
}

public static void incrementNumber(int x) {
    x += 1;
}

Cuando declaramos e inicializamos int x = 0;, le hemos dicho a Java que mantenga un espacio de 4 bytes en la pila para que se almacene el int. El int no tiene que llenar los 4 bytes (Integer.MAX_VALUE), pero los 4 bytes estarán disponibles.

Luego, el compilador hace referencia a este lugar en la memoria cuando desea usar el entero x. El nombre de la variable x es lo que nosotros usamos para acceder a la ubicación de la memoria en la pila. El compilador tiene sus propias referencias internas a estas ubicaciones.

Una vez que hemos pasado x al método incrementNumber() y el compilador alcanza la firma del método con el parámetro int x, crea una nueva ubicación/espacio de memoria en la pila.

El nombre de la variable que usamos, x, tiene poco significado para el compilador. Incluso podemos llegar a decir que el int x que hemos declarado en el método main() es x_1 y el int x que hemos declarado en la firma del método es x_2 .

Luego aumentamos el valor del entero x_2 en el método y luego imprimimos x_1. Naturalmente, el valor almacenado en la ubicación de memoria para x_1 se imprime y vemos lo siguiente:

1
0

Aquí hay una visualización del código:

java stack and primitive variables

En conclusión, el compilador hace referencia a la ubicación de memoria de las variables primitivas.

Existe una pila para cada subproceso que estamos ejecutando y se usa para la asignación de memoria estática de variables simples, así como referencias a los objetos en el montón (más información sobre el montón en secciones posteriores).

Esto es probablemente lo que ya sabía, y lo que saben todos los que respondieron con la declaración inicial incorrecta. Donde radica el mayor error de concepto es en el siguiente tipo de datos.

Tipos de referencia

El tipo utilizado para pasar datos es el tipo de referencia.

Cuando declaramos e instanciamos/inicializamos objetos (similares a los tipos primitivos), se crea una referencia para ellos, de nuevo, muy similar a los tipos primitivos:

1
2
// Declaration and Instantiation/initialization
Object obj = new Object();

Nuevamente, también podemos dividir este proceso:

1
2
3
4
5
// Declaration
Object obj;

// Instantiation/initialization
obj = new Object();

Nota: Hay una diferencia entre instanciación e inicialización. Instanciación se refiere a la creación del objeto y la asignación de una ubicación en la memoria. Inicialización se refiere a la población de los campos de este objeto a través del constructor, una vez que se crea.

Una vez que hayamos terminado con la declaración, la variable obj es una referencia al nuevo objeto en la memoria. Este objeto se almacena en el montón, a diferencia de los tipos primitivos que se almacenan en la pila.

Cada vez que se crea un objeto, se coloca en el montón. El recolector de basura barre este montón en busca de objetos que hayan perdido sus referencias y los elimina porque ya no podemos alcanzarlos.

El valor predeterminado para los objetos después de la declaración es null. No hay ningún tipo del que null sea una instancia de y no pertenezca a ningún tipo o conjunto. Si no se asigna ningún valor a una referencia, como obj, la referencia apuntará a null.

Digamos que tenemos una clase como Empleado:

1
2
3
4
public class Employee {
    String name;
    String surname;
}

Y crea una instancia de la clase como:

1
2
3
Employee emp = new Employee();
emp.name = new String("David");
emp.surname = new String("Landup");

Esto es lo que sucede en segundo plano:

java heap memory object creation

La referencia emp apunta a un objeto en el espacio del montón. Este objeto contiene referencias a dos objetos String que contienen los valores David y Landup.

Cada vez que se usa la palabra clave nuevo, se crea un nuevo objeto.

Transferencia de referencias de objetos

Veamos qué sucede cuando pasamos un objeto como argumento del método:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public static void main(String[] args) {
    Employee emp = new Employee();
    emp.salary = 1000;
    incrementSalary(emp);
    System.out.println(emp.salary);
}

public static void incrementSalary(Employee emp) {
    emp.salary += 100;
}

Hemos pasado nuestra referencia emp al método incrementSalary(). El método accede al campo int salario del objeto y lo incrementa en 100. Al final, somos recibidos con:

1
1100

Esto seguramente significa que la referencia ha sido pasada entre la llamada al método y el método mismo, ya que el objeto al que queríamos acceder sí ha sido cambiado.

Equivocado. Lo mismo que con los tipos primitivos, podemos seguir adelante y decir que hay dos variables emp una vez que se ha llamado al método: emp_1 y emp_2, a los ojos del compilador.

La diferencia entre la primitiva x que hemos usado antes y la referencia emp que estamos usando ahora es que tanto emp_1 como emp_2 apuntan al mismo objeto en la memoria.

Usando cualquiera de estas dos referencias, se accede al mismo objeto y se cambia la misma información.

java heap memory multiple references

Dicho esto, esto nos lleva a la pregunta inicial.

¿Java "pasa por referencia" o "pasa por valor"?

Java pasa por valor. Los tipos primitivos se pasan por valor, las referencias a objetos se pasan por valor.

Java no pasa objetos. Pasa referencias de objetos, por lo que si alguien pregunta cómo pasa objetos Java, la respuesta es: "no\pasa".1

En el caso de los tipos primitivos, una vez que se pasan, se les asigna un nuevo espacio en la pila y, por lo tanto, todas las operaciones posteriores en esa referencia se vinculan a la nueva ubicación de memoria.

En el caso de las referencias a objetos, una vez pasadas, se hace una nueva referencia, pero apuntando a la misma ubicación de memoria.

[1. Según Brian Goetz, el arquitecto del lenguaje Java que trabaja en los proyectos Valhalla y Amber. Puedes leer más sobre esto aquí.]{.small}

Licensed under CC BY-NC-SA 4.0