¿Java «pasa por referencia» o «pasa por valor»?

    Introducción

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

    ¿Java «pasa por referencia» o «pasa por valor» al pasar argumentos a métodos?

    Parece una pregunta simple (lo es), pero muchas personas se equivocan al decir:

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

    Una afirmación correcta sería:

    Referencias de objeto se pasan por valor, como son tipos primitivos. Por lo tanto, Java pasa por valor, no por referencia, en todos los casos.

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

    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:

    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 se imprimirá:

    0
    

    Mientras que el segundo ejemplo se imprimirá:

    1
    

    A menudo se entiende que el motivo de esta diferencia se debe al «paso por valor» (primer ejemplo, el valor copiado de x se pasa y cualquier operación en la copia no se reflejará en el valor original) y «pasar por referencia» (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

    Repasemos cómo trata Java las variables, ya que esa es la clave para comprender el concepto erróneo. La idea errónea se basa en hechos reales, pero un poco deformada.

    Tipos primitivos

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

    // 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:

    // Declaration
    int i;
    boolean isAbsent;
    
    // Initialization
    i = 5;
    isAbsent = false;
    

    Pero si intenta utilizar una variable no inicializada:

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

    Recibirá un error:

    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:

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

    Al ejecutar esto, verá el siguiente resultado:

    0
    5
    

    La variable i se produjo como 0, aunque aún no estaba asignado.

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

    Hay 8 tipos primitivos en Java:

    • byte: Rangos desde -128 a 127 inclusive, entero de 8 bits con signo
    • short: Rangos desde -32,768 a 32,767 inclusive, entero de 16 bits con signo
    • int: Rangos desde -2,147,483,648 a 2,147,483,647 inclusive, entero de 32 bits con signo
    • long: Va desde -231 a 231-1, inclusive, entero de 64 bits con signo
    • float: Precisión simple, 32 bits IEEE 754 entero de punto flotante con 6-7 dígitos significativos
    • double: Entero de coma flotante IEEE 754 de doble precisión de 64 bits, con 15 dígitos significativos
    • boolean: Valores binarios, true o false
    • char: Rangos desde 0 a 65,536 entero sin signo de 16 bits inclusive que representa un carácter Unicode

    Pasando 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 desglosémoslo:

    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 int para ser almacenado. 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. los x El nombre de la variable es lo que 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 incrementNumber() método y el compilador alcanza la firma del método con el int x parámetro: 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 ir tan lejos como para decir que int x hemos declarado en el main() el método es x_1 y el int x que hemos declarado en la firma del método es x_2.

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

    0
    

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

    En conclusión, el compilador hace referencia a la ubicación de la 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 para referencias a los objetos en el montón (más 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 reside el mayor error es en el siguiente tipo de datos.

    Tipos de referencia

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

    Cuando declaramos y instanciamos / inicializamos objetos (similar a los tipos primitivos), se crea una referencia a ellos, nuevamente, muy similar a los tipos primitivos:

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

    Nuevamente, también podemos dividir este proceso:

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

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

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

    Siempre que se crea un objeto, se coloca en el montón. El recolector de basura barre este montón en busca de objetos que han 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 un tipo que null es un instanceof y no pertenece 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 Employee:

    public class Employee {
        String name;
        String surname;
    }
    

    E instancia la clase como:

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

    Esto es lo que sucede en segundo plano:

    los emp puntos de referencia a un objeto en el espacio dinámico. Este objeto contiene referencias a dos String objetos que tienen los valores David y Landup.

    Cada vez que el new se utiliza la palabra clave, se crea un nuevo objeto.

    Pasar referencias a objetos

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

    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 nuestro emp referencia al método incrementSalary(). El método accede al int salary campo del objeto y lo incrementa en 100. Al final, somos recibidos con:

    1100
    

    Esto seguramente significa que la referencia se ha pasado entre la llamada al método y el método en sí, ya que el objeto al que queríamos acceder efectivamente se ha cambiado.

    Incorrecto. Al igual que con los tipos primitivos, podemos seguir adelante y decir que hay dos emp variables una vez que se ha llamado al método – emp_1 y emp_2, a los ojos del compilador.

    La diferencia entre lo primitivo x que hemos usado antes y el emp La referencia que estamos usando ahora es que tanto emp_1 y emp_2 apuntar al mismo objeto en la memoria.

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

    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 a objetos, por lo que si alguien pregunta cómo pasa Java, la respuesta es: «no lo hace» .1

    En el caso de los tipos primitivos, una vez pasados, se les asigna un nuevo espacio en la pila y, por lo tanto, todas las operaciones adicionales en esa referencia están vinculadas a la nueva ubicación de memoria.

    En el caso de referencias a objetos, una vez pasadas, se realiza 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í.

    5/5 - (1 voto)
    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *