Simplifier l'écriture des listeners Java avec EventHandler

Yann D'Isanto

Ce tutoriel a pour objectif de vous présenter la classe java.beans.EventHandler qui permet de simplifier la création de listeners en Java. Commentez

- Titre : Simplifier l'écriture des listeners Java avec EventHandler

- Auteur : Yann D'Isanto

- Parution : 13 juin 2012

- Licence : Copyright ® 2012 Yann D'Isanto. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Appel simple d'une méthode

- III. Appel d'une méthode avec une propriété de l'évènement comme argument

- IV. Choix des méthodes de l'interface

- V. Cas de la méthode cible surchargée

- VI. Conclusion

- VII. Remerciements

I - Introduction

L'écriture d'IHM java mène généralement à l'écriture de plusieurs listeners, souvent sous la forme de classes anonymes. Celles-ci sont fastidieuses à écrire et réduisent la lisibilité du code pour au final ne faire qu'une simple affectation ou un simple appel de méthode.

La classe java.beans.EventHandler permet de s'affranchir de cette écriture en permettant de générer dynamiquement des instances d'interface dont les méthodes exécutent une instruction simple. Son mécanisme est basé sur la création de proxy (cf java.lang.reflect.Proxy, java.lang.reflect.InvocationHandler).

Son utilisation est plus efficace que les classes anonymes, notamment pour les grandes applications dans lesquelles une même interface est implémentée de nombreuses fois. Elle permet de réduire la taille et l'empreinte mémoire de l'application.
La raison de cette faible empreinte est que la classe proxy utilisée par l'EventHandler partage les implémentations pour des interfaces identiques. Par exemple, pour créer tous les ActionListeners d'une application, une seule implémentation sera créée par le proxy. Globalement, une implémentation est créée par interface alors qu'en utilisant des classes anonymes, une implémentation est créée pour chaque listener.
Par exemple pour une application comptant 11 ActionListeners et 3 MouseListeners, 2 implémentations seront créées avec l'EventHandler (une pour les ActionListeners et une pour les MouseListeners) alors que l'utilisation de classes anonymes en aurait créé 14 (une pour chaque listener).

Son inconvénient est que l'utilisation de la réflection rend l'exécution plus coûteuse. Ce coût est négligeable pour des exécutions ponctuelles (action d'un clic bouton par exemple) mais cela peut nuire aux performances dans le cas d'appels successifs (appels dans une boucle).

La classe EventHandler permet globalement de gérer plusieurs cas d'utilisation simples via ses méthodes statiques create(...).

II - Appel simple d'une méthode

Le premier cas d'utilisation est celui d'un simple appel à une méthode donnée (l'action) sur un objet donné (la cible). Il est géré par la méthode statique EventHandler.create(Class, Object, String).

Prenons un exemple simple :

button.addActionListener(new ActionListener() {

@Override
public void actionPerformed(ActionEvent e) {
foo();
}
});

Ce code crée une instance de l'interface ActionListener afin d'effectuer un appel à la méthode foo() de l'objet englobant la classe anonyme.
Dans notre cas d'utilisation, la méthode foo() est l'action et l'objet englobant est la cible.

Avec la classe EventHandler, le code devient :

button.addActionListener(EventHandler.create(ActionListener.class, this, "foo"));

Le premier paramètre est le type de l'instance qui doit être créée.
Le deuxième paramètre est la cible sur laquelle sera effectuée l'action (ici this).
Le troisième paramètre est l'action à effectuer sur la cible (la méthode à exécuter, ici foo()).

Un autre exemple :

button.addActionListener(new ActionListener() {

@Override
public void actionPerformed(ActionEvent e) {
helloworld.sayHello();
}
});

Devient :

button.addActionListener(EventHandler.create(ActionListener.class, helloworld, "sayHello"));

III - Appel d'une méthode avec une propriété de l'évènement comme argument

L'EventHandler permet également de pouvoir spécifier à l'action une des propriétés de l'évènement avec la méthode statique EventHandler.create(Class, Object, String, String).

Soit le code :

new ActionListener() {

@Override
public void actionPerformed(ActionEvent e) {
doAction(e.getSource());
}
};
...

public void doAction(Object object) {
System.out.println("action sur " + object);
}

Qui, avec l'EventHandler devient :

EventHandler.create(ActionListener.class, this, "doAction", "source");
...

public void doAction(Object object) {
System.out.println("action sur " + object);
}

L'utilisation des propriétés nécessite la présence des getters appropriés, la propriété "source" implique la présence de la méthode getSource().

Il est également possible d'indiquer une sous-propriété en utilisant le caractère ".". Ainsi le code suivant utilisant la propriété text de la propriété source de l'évènement :

new ActionListener() {

@Override
public void actionPerformed(ActionEvent e) {
setTitle(((JTextField) e.getSource()).getText() );
}
};

Devient avec l'EventHandler :

EventHandler.create(ActionListener.class, this, "setTitle", "source.text");

Il est possible d'enchaîner les sous-propriétés. Par exemple :

EventHandler.create(ActionListener.class, this, "setAdminMode", "source.user.admin");

est équivalent à :

new ActionListener() {

@Override
public void actionPerformed(ActionEvent e) {
setAdminMode(((UserAuth) e.getSource()).getUser().isAdmin());
}
};

Il est aussi possible de passer l'évènement lui-même en spécifiant une chaîne vide "" comme propriété :

EventHandler.create(ActionListener.class, this, "doAction", "");
&#8230;

public void doAction(ActionEvent e) {
System.out.println("action sur " + e.getSource());
}

IV - Choix des méthodes de l'interface

Par défaut, le code expliqué précédemment s'applique à toutes les méthodes de l'interface. Par exemple le code :

EventHandler.create(FocusListener.class, this, "focusChanged", "");

est équivalent à :

new FocusListener() {

@Override
public void focusGained(FocusEvent e) {
focusChanged(e);
}

@Override
public void focusLost(FocusEvent e) {
focusChanged(e);
}
};

Pour n'appliquer une action qu'à une méthode précise de l'interface, il est possible de spécifier son nom avec la troisième méthode statique EventHandler.create(Class, Object, String, String, String). Les autres méthodes ne feront alors rien.

EventHandler.create(FocusListener.class, this, "focusChanged", "", "focusGained");

est équivalent à :

new FocusListener() {

@Override
public void focusGained(FocusEvent e) {
focusChanged(e);
}

@Override
public void focusLost(FocusEvent e) {
}
};

V - Cas de la méthode cible surchargée

Il est recommandé de ne pas appeler une méthode surchargée comme action. En effet, si par exemple la cible est une instance de la classe suivante :

public class Target {
public void doAction(String);
public void doAction(Object);
}

L'EventHandler appellera la méthode appropriée en fonction de la source. Cependant, si la source vaut null, les deux méthodes sont appropriées et il n'y a aucune garantie de laquelle sera appelée. Pour cette raison il est déconseillé d'appeler une méthode surchargée comme action.

VI - Conclusion

Même si l'EventHandler ne couvre que des cas d'utilisation relativement basiques, ceux-ci représentent une grande majorité des cas rencontrés dans une application. De plus, même si son utilisation est particulièrement adaptée aux listeners, elle peut s'utiliser pour créer une instance de n'importe quelle interface.

EventHandler.create(Runnable.class, "execute");

Ainsi son principal intérêt est donc de grandement réduire le nombre de classes anonymes ce qui apporte une plus grande lisibilité du code en plus d'une plus faible empreinte mémoire.

VII - Remerciements

Je remercie Gueritarish et Erielle pour leurs remarques et corrections.