jueves, 6 de enero de 2022

La chica de enero

 La chica del mes, la mejor para calentar los meses de invierno









Java ¿paso por referencia o paso por valor?

 Poco o mucho se habla de la forma en que se pasan los parámetros u argumentos por las funciones en Java de ésto podemos encontrar multitud de blogs, incluido el mío, e incluso hilos extensos como el de Stackoverflow.


Concluyendo al final la mayoría, por inercia, que se pasa por valor.

Cuando decimos que se pasa por valor se entiende que se realiza una copia del objeto o valor con lo que si luego desde la función o método decidimos alterar ese valor o el propio objeto teóricamente esto surtiría efecto sólo en el ámbito de la función y no a la vuelta al ámbito desde donde se invocó la función.

Pero lejos de las valoraciones o conclusiones de los usuarios leamos qué nos dice la documentación de Oracle:

Passing Primitive Data Type Arguments

Primitive arguments, such as an int or a double, are passed into methods by value. This means that any changes to the values of the parameters exist only within the scope of the method. When the method returns, the parameters are gone and any changes to them are lost. 

Aquí Oracle nos dice claramente que los tipos primitivos soportados por el lenguaje son pasados por valor, esto significa que cualquier cambio de valor en el parámetro sólo tiene efecto en el ámbito del método o función. Cuando el método retorna los cambios se pierden.

¿pero y qué pasa con los tipos que no son primitivos?

Passing Reference Data Type Arguments

Reference data type parameters, such as objects, are also passed into methods by value. This means that when the method returns, the passed-in reference still references the same object as before. However, the values of the object's fields can be changed in the method, if they have the proper access level.

Pues la documentación oficial nos indica que los objetos también se pasan por valor pero nos explica que la referencia sigue siendo la misma y que los valores de los campos, sus propiedades, y según el nivel de acceso sus valores pueden ser alterados... pero espera, a que ha venido eso de la referencia sigue siendo la misma ¿no acabas de indicarme que se pasa por valor?¿qué sentido tiene eso?¿puedes alterar los valores de las propiedades desde una copia del objeto? Y es aquí donde la gente se hace un lío, incluido yo y muchos que nos han enseñado Java, incluso nuestros maestros.

Hace tiempo pensaba que la dirección de memoria era aquello que nos mostraba el objeto cuando lo pasabas por la salida de pantalla de la consola, que consistía en el tipo completo con su paquete una arroba y un número en hexadecimal pero no señores, es un malentendido, eso es el hashCode. Es un código para identificar el contenido de un objeto, de esta forma podemos comparar objetos y saber si por sus valores son iguales, por lo que el hashCode de un objeto será el mismo que el que llega como parámetro a la función, pero esto no puede tomarse como referencia porque al cambiar sus propiedades su hashCode cambiaría porque ya es distinto al original.


La realidad

La realidad es que Java pasa los parámetros por referencia, entendiendo pasar por referencia que el objeto o tipo primitivo que se pasa por parámetro no es copiado, simplemente usa la misma celda de memoria para trabajar con el objeto o los valores desde el ámbito de la función ¿pero qué pasa?¿por qué no persisten los valores al retornar? Pues porque Java no usa punteros, cuando asignas con el igual a un tipo primitivo u objeto estás instanciando, no estás actualizando el contenido de la memoria o puntero, estás sustituyendo un objeto por otro objeto, por lo que al volver simplemente seguirás trabajando con el mismo objeto que tenías sin alterar sólo la instancias de sus propiedades en el caso de los objetos y que las anteriores se mantendrán en memoria temporalmente hasta que el recolector de basura pase a eliminarlas.

A ver, dirás :”¿me estás diciendo que debo creer a un viejo que se sacó un FP de Desarrollo de Aplicaciones Web que no cursó Java sino Php antes que a la mayoría de desarrolladores que muchos serán ingenieros de software, gente con carrera y certificados Java, y que incluso te crea a ti antes que a la documentación oficial de Oracle? Venga hombre y yo soy superman”. Y lo entiendo, realmente el propósito de esta publicación no va encaminada a convencer al lector sino a reflexionar y que saque conclusiones en base a los argumentos expuestos, que pueden ser rebatidos en los comentarios, donde además ahora mostraré una serie de ejemplos que fundamentan dichos argumentos.

Como hablaba antes el hashCode no tiene nada que ver con la memoria, es sólo un identificador. Para consultar la memoria tendríamos que usar alguna herramienta que hurgue en la Máquina Virtual Java (la JVM) para las pruebas yo he usado una sugerencia de Ali Dehghani desde una de sus publicaciones en Baeldung que con pocas palabras y algunos ejemplos indica la diferencia entre hashCode y memoria. La librería usada es la Java Object Layout de Oracle, la versión 0.16.

Abajo la prueba que muestra que cualquier cosa se pasa por referencia y no por copia como nos han vendido toda la vida.


import java.util.Date;
import org.openjdk.jol.vm.VM;
public class Test1 {
    public static void main(String[] args) throws Exception {
        int entero = 45;
        String string = "prueba";
        Date date = new Date();
        System.out.println("Entero: "+entero);
        System.out.println("String: "+string);
        System.out.println("Date: "+date);
        System.out.println("Dirección de 'entero' en el ámbito main: "+VM.current().addressOf(entero));
        System.out.println("Dirección de 'string' en el ámbito main: "+VM.current().addressOf(string));
        System.out.println("Dirección de 'date' en el ámbito main: "+VM.current().addressOf(date));
        updateInt(entero);
        updateString(string);
        updateDate(date);
    }
    public static void updateDate(Date date){
        System.out.println("Dirección de 'date' al pasarla a una funcion: "+VM.current().addressOf(date));
    }
    public static void updateString(String string){
        System.out.println("Dirección de 'string' al pasarla a una funcion: "+VM.current().addressOf(string));
    }
    public static void updateInt(int entero){
        System.out.println("Dirección de 'entero' al pasarla a una funcion: "+VM.current().addressOf(entero));
    }
}

Abajo segunda y última prueba en la que instanciamos nuevos valores, tanto en sus propiedades como en los mismos objetos. Como observarán al retornar los objetos mantienen sus nuevas instancias en las propiedades, porque se vincula la referencia pasa lo mismo con los arrays/listas, y los otros tipos como la cadena o el entero mantienen su valor original ya que la instancia que usamos dentro de la función se queda ahí, dentro del ámbito de la función.


import java.util.Date;
import org.openjdk.jol.vm.VM;
public class App {
    public static void main(String[] args) throws Exception {
        int entero = 45;
        String string = "prueba";
        Date date = new Date();
        Date dateNull = null;
        System.out.println("Entero: "+entero);
        System.out.println("String: "+string);
        System.out.println("Date: "+date);
        System.out.println("Dirección de 'entero' en el ámbito main: "+VM.current().addressOf(entero));
        System.out.println("Dirección de 'string' en el ámbito main: "+VM.current().addressOf(string));
        System.out.println("Dirección de 'date' en el ámbito main: "+VM.current().addressOf(date));
        System.out.println("Dirección de 'dateNull' en el ámbito main: "+VM.current().addressOf(dateNull));
        updateInt(entero);
        updateString(string);
        updateDate(date);
        updateDateNull(dateNull);
        System.out.println("Dirección de 'dateNull' al salir de la funcion: "+VM.current().addressOf(dateNull));
        System.out.println("Entero actualizado: "+entero);
        System.out.println("String actualizado: "+string);
        System.out.println("Date actualizado: "+date);
    }
    public static void updateDateNull(Date dateNull){
        System.out.println("Dirección de 'dateNull' al pasarla a una funcion: "+VM.current().addressOf(dateNull));
        dateNull = new Date(); // nueva instancia
        System.out.println("Dirección de 'dateNull' al instanciarla en una funcion: "+VM.current().addressOf(dateNull));
        dateNull.setTime(0);
    }
    public static void updateDate(Date date){
        System.out.println("Dirección de 'date' al pasarla a una funcion: "+VM.current().addressOf(date));
        date.setTime(0); // nueva instancia para la propiedad "time", vincula la referencia y la anterior se mantiene en memoria hasta que pasa el GC
    }
    public static void updateString(String string){
        System.out.println("Dirección de 'string' al pasarla a una funcion: "+VM.current().addressOf(string));
        string = "cadena"; // nueva instancia
        System.out.println("Dirección de 'string' al pasarla a una funcion: "+VM.current().addressOf(string));
    }
    public static void updateInt(int entero){
        System.out.println("Dirección de 'entero' al pasarla a una funcion: "+VM.current().addressOf(entero));
        entero = 666; // nueva instancia
        System.out.println("Dirección de 'entero' al pasarla a una funcion: "+VM.current().addressOf(entero));
    }
}


Esta publicación ha surgido por un pequeño debate entre mi querido compañero de trabajo Ricardo a raíz de una mala praxis en un desarrollo Java donde me corrigió un problema sobre instancias en Java y que en ese momento yo estaba convencido que se pasaban por referencia, por mi pobre experiencia en Java, pero no supe probarlo y argumentarlo correctamente. A él con cariño se lo dedico.


Fuentes: