Java: el paso de parámetros es por valor

El post de hoy es para resolver una duda sobre Java básico que a veces surge incluso entre gente con mucha experiencia: ¿Cómo es el paso de parámetros en Java? ¿Es por valor o por referencia?

Dependiendo del lenguaje de programación, el paso de parámetros puede ser por valor o por referencia:

  • El paso por valor es cuando al pasar una variable como parámetro a un método, el valor de ese parámetro se copia en una variable «temporal» que solo estará vigente durante la ejecución de ese método. De manera que al salir del método, la variable inicial tendrá el mismo valor que al inicio, ya que dentro del método no puede modificarse.
  • En el paso por referencia, en cambio, al pasar una variable como parámetro a un método, se pasa una referencia a esa variable. Con lo que si dentro del método es modificada, al salir del método el valor de la variable será distinto al inicial.

En Java el paso de parámetros es siempre por valor. Siempre. Varias veces he oído a gente decir que el paso en Java es por referencia, o, más concrétamente, que el paso es por valor si son datos primitivos y por referencia si son objetos. Esto último lo cree mucha gente, y no es cierto realmente, aunque lo pueda parecer.

Vamos a explicarlo con un pequeño ejemplo, veamos el siguiente código:

    public void primitiveExample() {
        int years = 10;
        System.out.println("Years before method: " + years);
        methodWithPrimitive(years);
        System.out.println("Years after method: " + years);
    }

    private void methodWithPrimitive(int arg) {
        arg = arg + 3;
        System.out.println("Parameter modified: " + arg);
    }

En el código pasamos un int ‘years’ como parámetro a un método y dentro del método le asignamos un nuevo valor.
Si ejecutamos ese código, la salida sería la siguiente:

Years before method: 10
Parameter modified: 13
Years after method: 10

Como vemos, al salir del método la variable ‘years’ tiene el mismo valor que tenia antes de entrar en el método, aún habiéndose modificado en él. Así funciona el paso por valor.

Veamos ahora el siguiente ejemplo, con un objeto en lugar de un tipo primitivo:

    class User {
        private String name;

        public User(String name) {
            this.name = name;
        }

        //getter and setter...

        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    ...
    public void objectExample() {
        User user = new User("edwise");
        System.out.println("User before method: " + user);
        methodWithObjectSet(user);
        System.out.println("User after method: " + user);
    }

    private void methodWithObjectSet(User arg) {
        arg.setName("newUser");
        System.out.println("Parameter modified: " + arg);
    }

Aquí, creamos un ‘User’ (clase muy simple con un atributo String), se lo pasamos como parámetro a un método y dentro de este llamamos a un método set para modificar un atributo de nuestro objeto.
La salida por consola sería la siguiente:

User before method: User{name='edwise'}
Parameter modified: User{name='newUser'}
User after method: User{name='newUser'}

Si, ahora, como vemos, nuestro parámetro ha sido modificado dentro del método, y esa modificación perdura en nuestro objeto. ¿Es entonces esto paso por referencia? No… realmente es paso por valor, como siempre. ¿Qué ha pasado entonces?

Veamos un último ejemplo, y lo explicamos:

    public void otherObjectExample() {
        User user = new User("edwise");
        System.out.println("User before method: " + user);
        methodWithObjectNew(user);
        System.out.println("User after method: " + user);
    }

    private void methodWithObjectNew(User arg) {
        arg = new User("newUser");
        System.out.println("Parameter modified: " + arg);
    }

En este caso hacemos casi lo mismo que en el código anterior. Dentro del código estamos modificando nuestro objeto, pero ahora creando un nuevo y asignandoselo. El resultado debería ser el mismo.
Salida:

User before method: User{name='edwise'}
Parameter modified: User{name='newUser'}
User after method: User{name='edwise'}

No, en este caso nuestro objeto no se ha modificado. Aquí si parece claro que el paso es por valor.

Para entender el porque de nuestro segundo ejemplo, la clave es recordar que, en Java, todas las variables que «son» objetos realmente son referencias a objetos, no son el objeto en si. De manera que, tanto en nuestro segundo y tercer ejemplo al método le pasamos una referencia a un objeto. Y como el paso es por valor, se crea una variable temporal para el método en la que se copia el valor de la variable que le pasamos, que es una referencia. Luego entonces, dentro del método tenemos una nueva referencia que apunta al mismo objeto.

En el segundo ejemplo (método ‘objectExample‘), llamamos desde nuestra nueva referencia (‘arg’) al método set del objeto. Como el objeto sobre el que llamamos a ese método es el mismo que para la referencia de fuera, nuestro objeto inicial es modificado.

En el tercer ejemplo (método ‘otherObjectExample‘), lo que realmente estamos haciendo es crear un nuevo objeto, y que la referencia ‘arg’ apunte/referencie a ese objeto. Al salir del método la referencia ‘arg’ desaparece, y la ‘user’ sigue apuntando al primer objeto, el cual no ha sido modificado, claro. (Además, el objeto creado dentro del método será destruido por el Gargabe Collector al no tener ninguna referencia que apunte a él, pero eso ya es otra historia).

Conclusiones

El paso de parámetros en Java es siempre por valor. Pero al ser referencias a objetos las «variables objetos», desde dentro de un método podemos acceder y modificar el objeto original. Y hay que tener mucho cuidado cuando asignemos un nuevo objeto a un parámetro de un método, ya que el resultado no va a ser el esperado…

De todas maneras, asignar nuevos valores / objetos a un parámetro de un método no suele considerarse una buena práctica, con lo que si hacemos un código medianamente limpio no deberíamos tener estos problemas…

Tenéis en mi github (proyecto passbyvalue-example) el código completo de este post, para poder ejecutarlo y comprobar que es cierto todo esto 😉

Deja un comentario