CodeGym /Cours /JAVA 25 SELF /Création et gestion d’événements personnalisés

Création et gestion d’événements personnalisés

JAVA 25 SELF
Niveau 50 , Leçon 2
Disponible

1. Quand vous avez besoin d’événements personnalisés

Les événements standard de Java comme l’appui sur un bouton, le mouvement de la souris ou la modification d’un texte ne sont que la partie émergée de l’iceberg. Dans les applications réelles, de nombreuses situations ne rentrent pas dans ce cadre. Par exemple, un programme peut charger des données depuis Internet et il faut notifier d’autres parties de l’application lorsque le chargement est terminé. Dans un jeu, un joueur peut obtenir un nouveau succès, et il convient d’en informer plusieurs composants à la fois, par exemple l’interface et le système de journalisation. Dans une application métier, la modification de l’état d’une commande doit déclencher des notifications pour la comptabilité, l’entrepôt et l’utilisateur simultanément.

Dans tous ces cas, il est utile de créer vos propres types d’événements et d’écouteurs. Ils permettent de décrire des scénarios d’interaction uniques entre composants et de rendre le code plus flexible et structuré.

Structure d’un événement personnalisé

Pour créer votre propre événement en Java, on implémente généralement trois éléments :

  1. Classe d’événement — en règle générale, une sous-classe de java.util.EventObject. Elle conserve les informations sur l’événement (quelle est la source, quelles données sont associées à l’événement).
  2. Interface d’écouteur — par exemple MyEventListener, qui étend java.util.EventListener et définit les méthodes de traitement de l’événement.
  3. Mécanisme d’abonnement/désabonnement — des méthodes sur la source de l’événement pour add...Listener/remove...Listener et l’appel des écouteurs lorsque l’événement survient (fire...).

Voyons cela étape par étape avec un exemple d’application où des données sont chargées depuis un fichier ou le réseau, et où nous devons avertir les autres de la fin du chargement.

Classe d’événement

Créons une classe d’événement qui contiendra des informations sur le chargement terminé.

import java.util.EventObject;

// Classe d’événement : on étend EventObject
public class DataLoadedEvent extends EventObject {
    private final String data; // Informations supplémentaires sur l’événement

    public DataLoadedEvent(Object source, String data) {
        super(source); // source — l’objet qui a déclenché l’événement
        this.data = data;
    }

    public String getData() {
        return data;
    }
}

Ici, source est l’objet source de l’événement (par exemple, un chargeur de données), et data est une chaîne contenant les données chargées (cela peut être un chemin de fichier, du JSON, un résultat, etc.).

Interface d’écouteur

Définissons l’interface d’écouteur. En général, elle étend EventListener (interface marqueur pour la typage).

import java.util.EventListener;

// Interface d’écouteur de notre événement
public interface DataLoadedListener extends EventListener {
    void dataLoaded(DataLoadedEvent event);
}

La méthode dataLoaded sera appelée lorsque l’événement se produira.

Source de l’événement

Il nous faut une entité qui conserve la liste des écouteurs, permette de les enregistrer/retirer et les notifie quand l’événement survient.

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

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

    // Enregistrement d’un écouteur
    public void addDataLoadedListener(DataLoadedListener listener) {
        listeners.add(listener);
    }

    // Suppression d’un écouteur
    public void removeDataLoadedListener(DataLoadedListener listener) {
        listeners.remove(listener);
    }

    // Méthode qui déclenche l’événement (par ex. après le chargement des données)
    private void fireDataLoaded(String data) {
        DataLoadedEvent event = new DataLoadedEvent(this, data);
        // Nous notifions tous les écouteurs
        for (DataLoadedListener listener : listeners) {
            listener.dataLoaded(event);
        }
    }

    // Exemple de méthode qui "charge" des données
    public void loadData() {
        // Nous simulons le chargement (par ex. depuis un fichier ou le réseau)
        String loadedData = "Voici les données chargées !";
        System.out.println("Données chargées : " + loadedData);

        // Nous informons tous les écouteurs
        fireDataLoaded(loadedData);
    }
}

Dans la réalité, la méthode loadData() peut être asynchrone, lire un fichier, contacter un serveur, etc., mais pour l’exemple nous simulons simplement le chargement.

2. Utilisation d’un événement personnalisé

Imaginons que nous ayons un composant qui souhaite être informé de la fin du chargement des données.

public class DataLoadedHandler implements DataLoadedListener {
    @Override
    public void dataLoaded(DataLoadedEvent event) {
        System.out.println("Le gestionnaire a reçu l’événement : " + event.getData());
    }
}

Assemblons le tout dans la classe principale de l’application :

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

        // Nous enregistrons l’écouteur
        loader.addDataLoadedListener(handler);

        // Nous lançons le chargement des données
        loader.loadData();
    }
}

Que va-t-il se passer ?

  1. DataLoader charge les données (simulation).
  2. Après le chargement, il appelle fireDataLoaded, qui crée l’objet événement et notifie tous les écouteurs.
  3. Notre gestionnaire (DataLoadedHandler) reçoit l’événement et affiche un message.

Exemple de sortie :

Données chargées : Voici les données chargées !
Le gestionnaire a reçu l’événement : Voici les données chargées !

Utilisation de classes anonymes et d’expressions lambda

Pour éviter de multiplier les classes pour chaque écouteur, on utilise souvent des classes anonymes ou des expressions lambda (à partir de Java 8) :

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

        // Écouteur via une expression lambda
        loader.addDataLoadedListener(event ->
            System.out.println("Gestionnaire lambda : " + event.getData())
        );

        loader.loadData();
    }
}

Enregistrement et suppression des écouteurs

On peut ajouter et supprimer des écouteurs. C’est important pour gérer la mémoire et éviter les fuites (surtout si l’écouteur est un objet « lourd » ou n’est plus nécessaire).

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

// Plus tard, si le gestionnaire n’est plus nécessaire :
loader.removeDataLoadedListener(handler);

Si vous n’enlevez pas l’écouteur alors que la source de l’événement vit longtemps, l’écouteur restera dans la liste et ne sera pas collecté par le garbage collector — cela peut conduire à une fuite de mémoire.

3. Pratique : mini-exemple — compteur de clics

Faisons un petit exemple, que l’on peut développer dans le cadre d’une application d’apprentissage. Supposons que nous ayons une classe compteur, qui incrémente sa valeur et, à chaque incrément, notifie les écouteurs de la nouvelle valeur.

Classe d’événement

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

Interface d’écouteur

import java.util.EventListener;

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

Classe compteur

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

Utilisation

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

        // On enregistre un écouteur via une lambda
        counter.addCounterChangedListener(event ->
            System.out.println("Le compteur a changé : " + event.getNewValue())
        );

        counter.increment(); // Le compteur a changé : 1
        counter.increment(); // Le compteur a changé : 2
    }
}

4. Erreurs typiques lors de la création et du traitement d’événements personnalisés

Erreur n° 1 : vous avez oublié d’appeler la méthode de notification des écouteurs. L’événement est créé, mais la méthode qui doit notifier les écouteurs (fireDataLoaded, fireCounterChanged) n’est pas appelée. Au final, les écouteurs « se taisent ».

Erreur n° 2 : des exceptions dans les gestionnaires d’écouteurs. Si l’un des écouteurs lève une exception, les autres peuvent ne pas recevoir la notification. Une bonne pratique consiste à envelopper les appels aux écouteurs dans des blocs trycatch, afin qu’un écouteur « défaillant » ne gêne pas les autres.

Erreur n° 3 : on n’enlève pas les écouteurs. Si un écouteur n’est plus nécessaire mais n’a pas été retiré, il continue de recevoir des événements et n’est pas libéré de la mémoire. Cela peut conduire à des fuites de mémoire — surtout lorsque la source vit longtemps et que les écouteurs sont nombreux.

Erreur n° 4 : modification de la liste des écouteurs pendant l’itération. Si, dans un gestionnaire d’événement, quelqu’un ajoute ou supprime un écouteur, cela peut conduire à une ConcurrentModificationException. Une approche sûre consiste à copier d’abord la liste dans un tableau séparé, puis à itérer sur la copie.

Erreur n° 5 : opérations longues dans les gestionnaires. Si un gestionnaire effectue une opération longue (chargement d’un fichier, attente réseau), l’interface peut « se figer ». Déplacez les tâches lourdes dans un thread séparé ou utilisez des mécanismes asynchrones.

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