CodeGym /Corsi /JAVA 25 SELF /Creazione e gestione di eventi personalizzati

Creazione e gestione di eventi personalizzati

JAVA 25 SELF
Livello 50 , Lezione 2
Disponibile

1. Quando servono eventi personalizzati

Gli eventi standard di Java come il clic di un pulsante, il movimento del mouse o la modifica di un testo sono solo la punta dell’iceberg. Nelle applicazioni reali emergono moltissime situazioni che non rientrano in questi schemi. Per esempio, un programma può caricare dati da Internet e bisogna avvisare altre parti dell’applicazione quando il caricamento è terminato. In un gioco il giocatore può ottenere un nuovo obiettivo; conviene notificarlo a più componenti, ad esempio all’interfaccia e al sistema di logging. In un’applicazione business la modifica dello stato di un ordine dovrebbe innescare notifiche per la contabilità, il magazzino e l’utente contemporaneamente.

In tutti questi casi è utile creare tipi di eventi e listener personalizzati. Consentono di descrivere scenari di interazione unici tra i componenti e di rendere il codice più flessibile e strutturato.

Struttura di un evento personalizzato

Per creare un proprio evento in Java, di solito si implementano tre parti:

  1. Classe dell’evento — di norma un sottotipo di java.util.EventObject. Contiene le informazioni sull’evento (chi è la fonte, quali dati sono associati all’evento).
  2. Interfaccia del listener — ad esempio, MyEventListener, che estende java.util.EventListener e definisce i metodi per gestire l’evento.
  3. Meccanismo di registrazione/rimozione — metodi nella sorgente dell’evento per add...Listener/remove...Listener e chiamata dei listener al verificarsi dell’evento (fire...).

Vediamolo passo dopo passo con un esempio di applicazione in cui i dati vengono caricati da un file o dalla rete e dobbiamo notificare gli altri quando il caricamento è terminato.

Classe dell’evento

Creiamo una classe evento che contenga le informazioni sul caricamento completato.

import java.util.EventObject;

// Classe dell'evento: estende EventObject
public class DataLoadedEvent extends EventObject {
    private final String data; // Informazioni aggiuntive sull'evento

    public DataLoadedEvent(Object source, String data) {
        super(source); // source — l'oggetto che ha generato l'evento
        this.data = data;
    }

    public String getData() {
        return data;
    }
}

Qui source è l’oggetto sorgente dell’evento (ad esempio, il loader dei dati), mentre data è una stringa con i dati caricati (può essere un percorso file, un JSON, un risultato, ecc.).

Interfaccia del listener

Definiamo l’interfaccia del listener. Di solito estende EventListener (interfaccia marker per la tipizzazione).

import java.util.EventListener;

// Interfaccia listener per il nostro evento
public interface DataLoadedListener extends EventListener {
    void dataLoaded(DataLoadedEvent event);
}

Il metodo dataLoaded verrà chiamato quando l’evento si verifica.

Sorgente dell’evento

Serve un’entità che mantenga l’elenco dei listener, consenta di registrarli/rimuoverli e li notifichi quando si verifica l’evento.

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

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

    // Registrazione di un listener
    public void addDataLoadedListener(DataLoadedListener listener) {
        listeners.add(listener);
    }

    // Rimozione di un listener
    public void removeDataLoadedListener(DataLoadedListener listener) {
        listeners.remove(listener);
    }

    // Metodo che innesca l'evento (ad esempio dopo aver caricato i dati)
    private void fireDataLoaded(String data) {
        DataLoadedEvent event = new DataLoadedEvent(this, data);
        // Notifichiamo tutti i listener
        for (DataLoadedListener listener : listeners) {
            listener.dataLoaded(event);
        }
    }

    // Esempio di metodo che "carica" i dati
    public void loadData() {
        // Simuliamo il caricamento (ad esempio da file o dalla rete)
        String loadedData = "Questi sono i dati caricati!";
        System.out.println("Dati caricati: " + loadedData);

        // Notifichiamo tutti i listener
        fireDataLoaded(loadedData);
    }
}

Nella realtà il metodo loadData() può essere asincrono, leggere un file, contattare un server, ecc., ma per l’esempio simuliamo semplicemente il caricamento.

2. Utilizzo di un evento personalizzato

Immaginiamo di avere un componente che vuole essere informato sul completamento del caricamento dei dati.

public class DataLoadedHandler implements DataLoadedListener {
    @Override
    public void dataLoaded(DataLoadedEvent event) {
        System.out.println("Il gestore ha ricevuto l'evento: " + event.getData());
    }
}

Colleghiamo tutto nel main dell’applicazione:

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

        // Registriamo il listener
        loader.addDataLoadedListener(handler);

        // Avviamo il caricamento dei dati
        loader.loadData();
    }
}

Cosa succederà?

  1. DataLoader carica i dati (simulazione).
  2. Dopo il caricamento chiama fireDataLoaded, che crea l’oggetto evento e notifica tutti i listener.
  3. Il nostro gestore (DataLoadedHandler) riceve l’evento e stampa un messaggio.

Esempio di output:

Dati caricati: Questi sono i dati caricati!
Il gestore ha ricevuto l'evento: Questi sono i dati caricati!

Uso di classi anonime ed espressioni lambda

Per non creare classi separate per ogni listener, spesso si usano classi anonime o espressioni lambda (a partire da Java 8):

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

        // Listener tramite espressione lambda
        loader.addDataLoadedListener(event ->
            System.out.println("Gestore lambda: " + event.getData())
        );

        loader.loadData();
    }
}

Registrazione e rimozione dei listener

I listener possono essere aggiunti e rimossi. Questo è importante per la gestione della memoria e per prevenire leak (soprattutto se il listener è un oggetto «pesante» o non serve più).

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

// Più tardi, se il gestore non serve più:
loader.removeDataLoadedListener(handler);

Se non si rimuove il listener e la sorgente dell’evento vive a lungo, il listener rimarrà nell’elenco e non verrà raccolto dal garbage collector — questo può provocare una perdita di memoria.

3. Pratica: mini-esempio — contatore dei clic

Facciamo un piccolo esempio, che si può sviluppare in un’applicazione didattica. Supponiamo di avere una classe contatore che aumenta il proprio valore e, a ogni incremento, notifica i listener del nuovo valore.

Classe dell’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;
    }
}

Interfaccia del listener

import java.util.EventListener;

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

Classe contatore

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);
        }
    }
}

Utilizzo

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

        // Registriamo un listener tramite lambda
        counter.addCounterChangedListener(event ->
            System.out.println("Il contatore è cambiato: " + event.getNewValue())
        );

        counter.increment(); // Il contatore è cambiato: 1
        counter.increment(); // Il contatore è cambiato: 2
    }
}

4. Errori tipici nella creazione e gestione di eventi personalizzati

Errore n. 1: ci si dimentica di chiamare il metodo che notifica i listener. L’evento viene creato, ma il metodo che dovrebbe notificare i listener (fireDataLoaded, fireCounterChanged) non viene chiamato. Di conseguenza i listener «tacciono».

Errore n. 2: eccezioni nei gestori dei listener. Se uno dei listener genera un’eccezione, gli altri potrebbero non ricevere le notifiche. Una buona pratica è incapsulare le chiamate ai listener in trycatch, in modo che un listener “problematico” non impedisca agli altri di essere eseguiti.

Errore n. 3: non si rimuovono i listener. Se un listener non serve più ma non è stato rimosso, continuerà a ricevere eventi e non verrà eliminato dalla memoria. Questo può portare a perdite di memoria — soprattutto quando la sorgente vive a lungo e i listener sono molti.

Errore n. 4: modifica dell’elenco dei listener durante l’iterazione. Se nel gestore dell’evento qualcuno aggiunge o rimuove un listener, ciò può portare a ConcurrentModificationException. Un approccio sicuro è copiare prima l’elenco in un array separato e poi iterare sulla copia.

Errore n. 5: operazioni lunghe nei gestori. Se un gestore esegue qualcosa di lungo (caricamento di un file, attesa della rete), l’interfaccia può “bloccarsi”. Spostate i compiti pesanti in un thread separato o usate meccanismi asincroni.

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