Ir al contenido principal

Constructores y destrucción de objetos

Introducción

En la programación orientada a objetos, la creación y eliminación de objetos son procesos fundamentales que debemos controlar adecuadamente. Los constructores son métodos especiales que se ejecutan automáticamente cuando creamos una instancia de una clase, permitiéndonos inicializar el objeto con un estado correcto. Por otro lado, en Java, la destrucción de objetos está relacionada con la gestión automática de memoria mediante el recolector de basura (garbage collector). En este artículo aprenderemos a implementar constructores eficientes y a entender cómo funciona el proceso de destrucción de objetos en Java.

Constructores en Java

¿Qué es un constructor?

Un constructor es un método especial que se utiliza para inicializar objetos. Se ejecuta automáticamente cuando se crea un objeto utilizando la palabra clave new. Los constructores tienen las siguientes características:

  • Tienen el mismo nombre que la clase
  • No tienen tipo de retorno (ni siquiera void)
  • Pueden recibir parámetros
  • Pueden sobrecargarse (tener varias versiones con distintos parámetros)

Constructor por defecto

Si no definimos ningún constructor en nuestra clase, Java proporciona automáticamente un constructor por defecto (sin parámetros) que inicializa los atributos con valores predeterminados. Por ejemplo:

public class Persona {
    private String nombre;
    private int edad;
    
    // Java crea implícitamente este constructor si no escribimos ninguno:
    // public Persona() {
    //     nombre = null;
    //     edad = 0;
    // }
}

// Uso del constructor por defecto
Persona persona = new Persona();

Es importante destacar que si nosotros definimos algún constructor explícitamente, Java ya no proporcionará el constructor por defecto automáticamente. Si queremos seguir usando un constructor sin parámetros, tendremos que definirlo manualmente.

Definiendo constructores personalizados

Podemos crear constructores que reciban parámetros para inicializar los atributos del objeto con valores específicos:

public class Persona {
    private String nombre;
    private int edad;
    
    // Constructor con parámetros
    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }
}

// Uso del constructor con parámetros
Persona persona = new Persona("Laura", 25);

En este ejemplo, this.nombre hace referencia al atributo de la clase, mientras que nombre hace referencia al parámetro del constructor. Usamos this para distinguir entre ambos cuando tienen el mismo nombre.

Sobrecarga de constructores

Podemos definir múltiples constructores para una misma clase, lo que nos permite crear objetos de diferentes maneras:

public class Persona {
    private String nombre;
    private int edad;
    private String direccion;
    
    // Constructor completo
    public Persona(String nombre, int edad, String direccion) {
        this.nombre = nombre;
        this.edad = edad;
        this.direccion = direccion;
    }
    
    // Constructor con nombre y edad
    public Persona(String nombre, int edad) {
        this(nombre, edad, "Dirección desconocida");
    }
    
    // Constructor solo con nombre
    public Persona(String nombre) {
        this(nombre, 0, "Dirección desconocida");
    }
    
    // Constructor sin parámetros
    public Persona() {
        this("Sin nombre", 0, "Dirección desconocida");
    }
}

En este ejemplo, la palabra clave this() se utiliza para llamar a otro constructor de la misma clase, evitando así la duplicación de código. Es importante que la llamada a this() sea la primera instrucción dentro del constructor.

Constructores y herencia

Cuando una clase hereda de otra, el constructor de la clase hija debe llamar al constructor de la clase padre utilizando super(). Si no lo hacemos explícitamente, Java inserta automáticamente una llamada a super() (sin parámetros) al principio del constructor. Por ejemplo:

public class Empleado extends Persona {
    private String puesto;
    private double salario;
    
    public Empleado(String nombre, int edad, String direccion, String puesto, double salario) {
        super(nombre, edad, direccion); // Llamada al constructor de Persona
        this.puesto = puesto;
        this.salario = salario;
    }
}

Destrucción de objetos y el recolector de basura

A diferencia de otros lenguajes como C++, en Java no necesitamos destruir manualmente los objetos. Java utiliza un recolector de basura (garbage collector) que se encarga automáticamente de liberar la memoria ocupada por objetos que ya no son accesibles.

¿Cuándo un objeto es elegible para la recolección de basura?

Un objeto se convierte en candidato para la recolección de basura cuando:

  1. No hay referencias apuntando al objeto
  2. Todas las referencias al objeto están fuera de alcance
  3. Las referencias al objeto se han establecido a null

Por ejemplo:

public void ejemploGarbageCollection() {
    Persona persona = new Persona("Carlos", 30); // Se crea el objeto
    
    persona = null; // El objeto ya no es accesible y es candidato para ser eliminado
    
    // Otra situación:
    if (true) {
        Persona temporal = new Persona("Ana", 25);
        // Al salir de este bloque, 'temporal' queda fuera de alcance
        // y el objeto es candidato para ser eliminado
    }
}

Método finalize()

Aunque no se recomienda su uso en código moderno, Java proporciona el método finalize() que se ejecuta antes de que el objeto sea eliminado por el recolector de basura:

public class Recurso {
    private String nombre;
    
    public Recurso(String nombre) {
        this.nombre = nombre;
        System.out.println("Recurso " + nombre + " creado");
    }
    
    @Override
    protected void finalize() throws Throwable {
        try {
            System.out.println("Finalizando recurso " + nombre);
            // Código para liberar recursos si es necesario
        } finally {
            super.finalize(); // Llamada al método finalize de la superclase
        }
    }
}

Importante: El método finalize() está obsoleto desde Java 9 y se desaconseja su uso, ya que:

  • No hay garantía de cuándo se ejecutará exactamente
  • Puede causar problemas de rendimiento
  • Puede no ejecutarse en absoluto si la aplicación finaliza antes

Alternativas modernas a finalize()

En lugar de utilizar finalize(), las prácticas modernas recomiendan:

  1. Patrón try-with-resources (desde Java 7) para recursos que implementen AutoCloseable:
public class ConexionBD implements AutoCloseable {
    public ConexionBD() {
        System.out.println("Conexión abierta");
    }
    
    @Override
    public void close() {
        System.out.println("Conexión cerrada");
    }
}

// Uso:
try (ConexionBD conexion = new ConexionBD()) {
    // Usar la conexión
} // La conexión se cierra automáticamente al salir del bloque
  1. Método explícito para liberar recursos:
public class Archivo {
    private boolean cerrado = false;
    
    public void procesar() {
        if (cerrado) {
            throw new IllegalStateException("Archivo ya cerrado");
        }
        System.out.println("Procesando archivo...");
    }
    
    public void cerrar() {
        if (!cerrado) {
            System.out.println("Cerrando archivo...");
            cerrado = true;
        }
    }
}

// Uso:
Archivo archivo = new Archivo();
try {
    archivo.procesar();
} finally {
    archivo.cerrar();
}

Referencias débiles y fantasmas

Java ofrece mecanismos avanzados para controlar la recolección de basura mediante diferentes tipos de referencias:

  • Referencias fuertes: las referencias normales como Persona p = new Persona()
  • Referencias débiles (WeakReference): permiten que el objeto sea recolectado
  • Referencias blandas (SoftReference): objetos recolectados solo cuando la memoria está baja
  • Referencias fantasma (PhantomReference): permiten realizar acciones antes de que el objeto sea finalmente eliminado

Estos mecanismos avanzados son útiles en situaciones específicas como caché, manejo de recursos o monitoreo de memoria.

Resumen

Los constructores son métodos especiales que nos permiten inicializar correctamente los objetos cuando se crean. Podemos definir múltiples constructores mediante la sobrecarga para proporcionar diferentes formas de crear objetos. En cuanto a la destrucción de objetos, Java se encarga automáticamente mediante su recolector de basura, eliminando los objetos que ya no son accesibles. Aunque existe el método finalize(), las prácticas modernas recomiendan el uso de patrones como try-with-resources o métodos explícitos para liberar recursos. Dominar estos conceptos te permitirá crear objetos correctamente inicializados y gestionar eficientemente los recursos en tus aplicaciones Java.