Guía de Constructores en Java

Guía de Constructores en Java

1. Introducción

Los constructores son la puerta de entrada al diseño orientado a objetos.

En este tutorial, veremos cómo actúan como un solo lugar desde el cual inicializar el estado interno del objeto que se está creando.

Pasemos al siguiente bloque y creemos un objeto simple que represente una cuenta bancaria.

2. Configurando una Cuenta Bancaria

Imaginemos que necesitamos crear una clase que represente una cuenta bancaria. Contendrá un Nombre, Fecha de Creación y Saldo.

También, vamos a sobreescribir el método toString para imprimir los detalles en la consola:

class CuentaBancaria {
    String nombre;
    LocalDateTime fechaCreacion;
    double saldo;

    // Marcamos el método con @Override para hacer saber que vamos a sobreescribirlo
    @Override
    public String toString() {
        return String.format("%s, %s, %f",
          this.nombre, this.fechaCreacion.toString(), this.saldo);
    }
}

Ahora, esta clase contiene todos los campos necesarios para almacenar información sobre una cuenta bancaria, pero aún no contiene un constructor.

Esto significa que si creamos un nuevo objeto, los valores de los campos no se inicializarán:

CuentaBancaria cuenta = new CuentaBancaria();
cuenta.toString();

Ejecutar el método toString anterior resultará en una excepción porque los campos nombre y fechaCreacion son nulos:

java.lang.NullPointerException
    at com.javamagician.constructors.CuentaBancaria.toString(CuentaBancaria.java:13)
    at com.javamagician.constructors.ConstructorUnitTest
      .cuandoNoPasamosConstructorExplicitamente_cuandoLoUsamos_entoncesFalla(ConstructorUnitTest.java:22)

3. Un Constructor sin Argumentos

Arreglemos eso con un constructor:

class CuentaBancaria {
    public CuentaBancaria() {
        this.nombre = "";
        this.fechaCreacion = LocalDateTime.now();
        this.saldo = 0.0d;
    }
}

Observa algunas cosas sobre el constructor que acabamos de escribir. En primer lugar, es un método, pero no tiene un tipo de retorno. Esto se debe a que un constructor implícitamente devuelve el tipo de objeto que crea. Llamar a new CuentaBancaria() llamará al constructor anterior.

En segundo lugar, no toma argumentos. Este tipo particular de constructor se llama constructor sin argumentos.

¿Por qué no lo necesitamos la primera vez? Es porque cuando no escribimos explícitamente ningún constructor, el compilador agrega un constructor predeterminado sin argumentos.

Es por eso que pudimos construir el objeto la primera vez, aunque no escribimos un constructor explícito. El constructor predeterminado sin argumentos simplemente establecerá todos los campos en sus valores predeterminados.

Para los objetos, ese valor es null, lo que resultó en la excepción que vimos anteriormente.

4. Un Constructor Parametrizado

Ahora, un verdadero beneficio de los constructores es que nos ayudan a mantener la encapsulación cuando inyectamos estado en el objeto. Para hacer algo útil con esta cuenta bancaria, deberemos darle algunos valores iniciales al objeto.

Para hacer eso, escribamos un constructor parametrizado, es decir, un constructor que toma algunos argumentos:

class CuentaBancaria {
    public CuentaBancaria() { ... }
    public CuentaBancaria(String nombre, LocalDateTime fechaCreacion, double saldo) {
        this.nombre = nombre;
        this.fechaCreacion = fechaCreacion;
        this.saldo = saldo;
    }
}

Ahora podemos hacer algo útil con nuestra clase CuentaBancaria:

LocalDateTime fechaCreacion = LocalDateTime.of(2023, Month.SEPTEMBER, 26, 12, 00, 00);
CuentaBancaria cuenta = new CuentaBancaria("Manuel", fechaCreacion, 1000.0f); 
cuenta.toString();

Observa que nuestra clase tiene ahora 2 constructores. Un constructor sin argumentos explícitos y un constructor parametrizado.

Podemos crear tantos constructores como deseemos, pero probablemente no querríamos crear demasiados. Eso sería un poco confuso.

Si encontramos demasiados constructores en nuestro código, algunos patrones de diseño creacional podrían ser útiles. Los veremos en próximas lecciones.

5. Un Constructor de Copia

Los constructores no necesariamente se limitan a la inicialización. También pueden usarse para crear objetos de otras maneras. Imagina que necesitamos crear una nueva cuenta a partir de una existente.

La nueva cuenta debería tener el mismo nombre que la antigua, la fecha de creación de hoy y ningún fondo. Podemos hacerlo usando un constructor de copia:

public CuentaBancaria(CuentaBancaria otra) {
    this.nombre = otra.nombre;
    this.fechaCreacion = LocalDateTime.now();
    this.saldo = 0.0f;
}

Ahora tenemos el siguiente comportamiento:

LocalDateTime fechaCreacion = LocalDateTime.of(2023, Month.SEPTEMBER, 26, 12, 00, 00);
CuentaBancaria cuenta = new CuentaBancaria("Manolo", fechaCreacion, 1000.0f);
CuentaBancaria nuevaCuenta = new CuentaBancaria(cuenta);

assertThat(cuenta.getNombre()).isEqualTo(nuevaCuenta.getNombre());
assertThat(cuenta.getFechaCreacion()).isNotEqualTo(nuevaCuenta.getFechaCreacion());
assertThat(nuevaCuenta.getSaldo()).isEqualTo(0.0f);

6. Un Constructor en Cadena

Por supuesto, podríamos inferir algunos de los parámetros del constructor o darles valores predeterminados.

Por ejemplo, podríamos crear simplemente una nueva cuenta bancaria con solo el nombre.

Entonces, creemos un constructor con un parámetro de nombre y demos valores predeterminados a los demás parámetros:

public CuentaBancaria(String nombre, LocalDateTime fechaCreacion, double saldo) {
    this.nombre = nombre;
    this.fechaCreacion = fechaCreacion;
    this.saldo = saldo;
}
public CuentaBancaria(String nombre) {
    this(nombre, LocalDateTime.now(), 0.0f);
}

Con la palabra clave this, estamos llamando al otro constructor.

Debemos recordar que si queremos encadenar un constructor de una superclase, debemos usar super en lugar de this.

Además, recuerda que la expresión this o super siempre debe ser la primera declaración.

7. Tipos de Valor

Un uso interesante de los constructores en Java es la creación de "Objetos de Valor". Un objeto de valor es un objeto que no cambia su estado interno después de la inicialización.

Es decir, el objeto es inmutable. La inmutabilidad en Java es un poco matizada y se debe tener cuidado al crear objetos. Exploraremos más sobre este tema en futuras lecciones.

Sigamos y creemos una clase inmutable:

class Transaccion {
    final CuentaBancaria cuentaBancaria;
    final LocalDateTime fecha;
    final double cantidad;

    public Transaccion(CuentaBancaria cuenta, LocalDateTime fecha, double cantidad) {
        this.cuentaBancaria = cuenta;
        this.fecha = fecha;
        this.cantidad = cantidad;
    }
}

Observa que ahora usamos la palabra clave final al definir los miembros de la clase. Esto significa que cada uno de esos miembros solo puede inicializarse dentro del constructor de la clase. No pueden ser reasignados más tarde en ningún otro método. Podemos leer esos valores, pero no cambiarlos.

Si creamos múltiples constructores para la clase Transaccion, cada constructor deberá inicializar cada variable final. No hacerlo resultará en un error de compilación.

8. Conclusión

Hemos realizado un recorrido por las diferentes formas en que los constructores construyen objetos. Cuando se usan con prudencia, los constructores forman los bloques básicos del diseño orientado a objetos en Java.