CodeGym /Cursos /JAVA 25 SELF /Creación y gestión de eventos personalizados

Creación y gestión de eventos personalizados

JAVA 25 SELF
Nivel 50 , Lección 2
Disponible

1. Cuando necesitas eventos personalizados

Los eventos estándar de Java, como la pulsación de un botón, el movimiento del ratón o el cambio de texto, son solo la punta del iceberg. En las aplicaciones reales surgen multitud de situaciones que no encajan en esos marcos. Por ejemplo, un programa puede estar cargando datos de Internet y es necesario notificar a otras partes de la aplicación cuando la carga termina. En un juego, el jugador puede obtener un nuevo logro y conviene comunicarlo a varios componentes a la vez, como a la interfaz de usuario y al sistema de registro. En una aplicación empresarial, un cambio en el estado de un pedido debe disparar notificaciones para contabilidad, almacén y el usuario simultáneamente.

En todos esos casos es útil crear tus propios tipos de eventos y escuchadores. Permiten describir escenarios únicos de interacción entre componentes y hacer el código más flexible y estructurado.

Estructura de un evento personalizado

Para crear tu propio evento en Java, normalmente se implementan tres partes:

  1. Clase de evento — por regla general, una subclase de java.util.EventObject. Almacena la información del evento (quién es la fuente, qué datos están asociados al evento).
  2. Interfaz del escuchador — por ejemplo, MyEventListener, que amplía java.util.EventListener y define los métodos de manejo del evento.
  3. Mecanismo de suscripción/cancelación — métodos en la fuente del evento para add...Listener/remove...Listener y la llamada a los escuchadores cuando se produce el evento (fire...).

Veámoslo paso a paso con un ejemplo de una aplicación en la que los datos se cargan desde un archivo o la red, y necesitamos notificar a otros cuando la carga termina.

Clase de evento

Crearemos una clase de evento que contendrá información sobre la carga finalizada.

import java.util.EventObject;

// Clase de evento: heredamos de EventObject
public class DataLoadedEvent extends EventObject {
    private final String data; // Información adicional del evento

    public DataLoadedEvent(Object source, String data) {
        super(source); // source: objeto que disparó el evento
        this.data = data;
    }

    public String getData() {
        return data;
    }
}

Aquí, source es el objeto fuente del evento (por ejemplo, el cargador de datos), y data es una cadena con los datos cargados (puede ser una ruta de archivo, JSON, un resultado, etc.).

Interfaz del escuchador

Definimos la interfaz del escuchador. Suele ampliar EventListener (interfaz marcador para tipado).

import java.util.EventListener;

// Interfaz del escuchador de nuestro evento
public interface DataLoadedListener extends EventListener {
    void dataLoaded(DataLoadedEvent event);
}

El método dataLoaded se llamará cuando se produzca el evento.

Fuente del evento

Necesitamos una entidad que almacene la lista de escuchadores, permita registrarlos/eliminarlos y notifique cuando ocurra el evento.

import java.util.ArrayList;
import java.util.List;

public class DataLoader {
    private final List<DataLoadedListener> listeners = new ArrayList<>();

    // Registro de escuchador
    public void addDataLoadedListener(DataLoadedListener listener) {
        listeners.add(listener);
    }

    // Eliminación de escuchador
    public void removeDataLoadedListener(DataLoadedListener listener) {
        listeners.remove(listener);
    }

    // Método que inicia el evento (por ejemplo, tras cargar los datos)
    private void fireDataLoaded(String data) {
        DataLoadedEvent event = new DataLoadedEvent(this, data);
        // Notificamos a todos los escuchadores
        for (DataLoadedListener listener : listeners) {
            listener.dataLoaded(event);
        }
    }

    // Ejemplo de método que "carga" datos
    public void loadData() {
        // Simulamos la carga (p. ej., desde un archivo o la red)
        String loadedData = "¡Estos son los datos cargados!";
        System.out.println("Datos cargados: " + loadedData);

        // Informamos a todos los escuchadores
        fireDataLoaded(loadedData);
    }
}

En la vida real, el método loadData() puede ser asíncrono, leer un archivo, acceder a un servidor, etc., pero para el ejemplo simplemente simulamos la carga.

2. Uso de un evento personalizado

Imaginemos que tenemos un componente que quiere saber cuándo termina la carga de datos.

public class DataLoadedHandler implements DataLoadedListener {
    @Override
    public void dataLoaded(DataLoadedEvent event) {
        System.out.println("El manejador recibió el evento: " + event.getData());
    }
}

Vinculemos todo en la clase principal de la aplicación:

public class Main {
    public static void main(String[] args) {
        DataLoader loader = new DataLoader();
        DataLoadedHandler handler = new DataLoadedHandler();

        // Registramos el escuchador
        loader.addDataLoadedListener(handler);

        // Iniciamos la carga de datos
        loader.loadData();
    }
}

¿Qué ocurrirá?

  1. DataLoader carga los datos (simulación).
  2. Tras la carga, llama a fireDataLoaded, que crea el objeto de evento y notifica a todos los escuchadores.
  3. Nuestro manejador (DataLoadedHandler) recibe el evento y muestra un mensaje.

Ejemplo de salida:

Datos cargados: ¡Estos son los datos cargados!
El manejador recibió el evento: ¡Estos son los datos cargados!

Uso de clases anónimas y expresiones lambda

Para no crear clases separadas para cada escuchador, a menudo se usan clases anónimas o expresiones lambda (desde Java 8):

public class Main {
    public static void main(String[] args) {
        DataLoader loader = new DataLoader();

        // Escuchador mediante una expresión lambda
        loader.addDataLoadedListener(event ->
            System.out.println("Manejador lambda: " + event.getData())
        );

        loader.loadData();
    }
}

Registro y eliminación de escuchadores

Se pueden añadir y eliminar escuchadores. Esto es importante para gestionar la memoria y evitar fugas (especialmente si el escuchador es un objeto «pesado» o ya no es necesario).

DataLoadedHandler handler = new DataLoadedHandler();
loader.addDataLoadedListener(handler);

// Más tarde, si el manejador ya no es necesario:
loader.removeDataLoadedListener(handler);

Si no eliminas el escuchador y la propia fuente del evento vive mucho tiempo, el escuchador permanecerá en la lista y no será eliminado por el recolector de basura — puede provocar una fuga de memoria.

3. Práctica: mini‑ejemplo — contador de clics

Hagamos un ejemplo pequeño que se puede ampliar en una aplicación de aprendizaje. Supongamos que tenemos una clase contador que incrementa su valor y, en cada incremento, notifica a los escuchadores del nuevo valor.

Clase de evento

import java.util.EventObject;

public class CounterChangedEvent extends EventObject {
    private final int newValue;

    public CounterChangedEvent(Object source, int newValue) {
        super(source);
        this.newValue = newValue;
    }

    public int getNewValue() {
        return newValue;
    }
}

Interfaz del escuchador

import java.util.EventListener;

public interface CounterChangedListener extends EventListener {
    void counterChanged(CounterChangedEvent event);
}

Clase contador

import java.util.ArrayList;
import java.util.List;

public class Counter {
    private int value = 0;
    private final List<CounterChangedListener> listeners = new ArrayList<>();

    public void addCounterChangedListener(CounterChangedListener listener) {
        listeners.add(listener);
    }

    public void removeCounterChangedListener(CounterChangedListener listener) {
        listeners.remove(listener);
    }

    public void increment() {
        value++;
        fireCounterChanged();
    }

    private void fireCounterChanged() {
        CounterChangedEvent event = new CounterChangedEvent(this, value);
        for (CounterChangedListener listener : listeners) {
            listener.counterChanged(event);
        }
    }
}

Uso

public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();

        // Registramos un escuchador mediante lambda
        counter.addCounterChangedListener(event ->
            System.out.println("El contador ha cambiado: " + event.getNewValue())
        );

        counter.increment(); // El contador ha cambiado: 1
        counter.increment(); // El contador ha cambiado: 2
    }
}

4. Errores típicos al crear y manejar eventos personalizados

Error n.º 1: olvidar llamar al método de notificación de escuchadores. Se crea el evento, pero no se invoca el método que debe notificar a los escuchadores (fireDataLoaded, fireCounterChanged). Como resultado, los escuchadores «no dicen nada».

Error n.º 2: excepciones en los manejadores de los escuchadores. Si uno de los escuchadores lanza una excepción, es posible que los demás no reciban la notificación. Una buena práctica es envolver las llamadas a los escuchadores en trycatch, para que un escuchador «problemático» no afecte a los demás.

Error n.º 3: no se eliminan los escuchadores. Si un escuchador ya no es necesario pero no se eliminó, seguirá recibiendo eventos y no será eliminado de la memoria. Esto puede provocar fugas de memoria — sobre todo cuando la fuente vive mucho tiempo y hay muchos escuchadores.

Error n.º 4: modificar la lista de escuchadores durante la iteración. Si, en el manejador del evento, alguien añade o elimina un escuchador, puede producirse ConcurrentModificationException. Un enfoque seguro es copiar primero la lista a un array independiente y después iterar sobre la copia.

Error n.º 5: operaciones largas en los manejadores. Si un manejador hace algo que lleva mucho tiempo (cargar un archivo, esperar a la red), la interfaz puede «congelarse». Traslada las tareas pesadas a un hilo aparte o utiliza mecanismos asíncronos.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION