Introduction▲
Note de la rédaction : ce tutoriel avait été publié à l'origine dans le défunt magazine Login. Les fichiers du CD-Rom de cet article sont disponibles en téléchargement en archive ZIP (cdrom.zip).
Swing repose entièrement sur son prédécesseur, l'Abstract Windowing Toolkit (AWT). Ces deux technologies diffèrent non seulement par leurs API et leurs fonctionnalités, mais surtout par leur nature. Les composants AWT sont en effet liés à des composants natifs dont ils se servent pour l'affichage. C'est pourquoi AWT est dit heavyweight, ou lourd. Swing, au contraire, prend complètement en charge la gestion des composants, qui sont dessinés par du code en pur Java. Swing est donc dit lightweight.
Malgré cette importante abstraction par rapport au système d'exploitation, Swing a besoin d'AWT pour fonctionner. Outre le fait que Swing dessine ses composants dans un canevas AWT, il utilise également son système d'acheminement des événements, source d'une majeure partie des problèmes de « performances » rencontrés. Lorsque vous lancez une application Swing, trois threads sont automatiquement créés. Le premier est le « main application thread » qui se charge d'exécuter la méthode main() de l'application. Le deuxième thread est le « toolkit thread » dont le rôle est de recevoir les événements du système d'exploitation, par exemple un clic de souris, et de les transmettre au dernier thread, appelé « event dispatching thread » ou EDT. Ce dernier est extrêmement important, car il répartit les événements reçus vers les composants concernés et se charge d'invoquer les méthodes d'affichage.
Lorsque vous appuyez sur une touche dans un champ texte, l'événement « appui sur une touche » est transmis à l'instance de JTextField qui met alors à jour son affichage (par l'entremise de son délégué d'UI). Toutes ces opérations se déroulent au sein de l'EDT et c'est pour cela que les interfaces Swing semblent réagir lentement. Pour vous en convaincre, compilez et exécutez le code du listing 1.
public
class
FreezeEDT extends
JFrame implements
ActionListener {
public
FreezeEDT
(
) {
super
(
"Freeze"
);
JButton freezer =
new
JButton
(
"Freeze"
);
freezer.addActionListener
(
this
);
add
(
freezer);
pack
(
);
}
public
void
actionPerformed
(
ActionEvent e) {
try
{
Thread.sleep
(
4000
);
}
catch
(
InterruptedException e) {
}
}
public
static
void
main
(
String... args) {
FreezeEDT edt =
new
FreezeEDT
(
);
edt.setVisible
(
true
);
}
}
Cliquez sur le bouton « Freeze » dans la fenêtre de l'application pour constater qu'il reste enfoncé. En effet, lorsque l'utilisateur déclenche une action en appuyant sur le bouton, la méthode actionPerformed() est exécutée pour mettre en pause le thread courant pendant 4 secondes.
Malheureusement, tous les événements sont exécutés dans l'EDT, qui gère également le dessin. En attendant 4 secondes nous bloquons tous les événements et les rafraîchissements de l'interface graphique. Puisque l'EDT fonctionne comme une file d'attente, toute opération un peu longue retarde les autres. Cela prouve que Swing ne souffre pas de mauvaises performances, mais que la méconnaissance de son modèle de gestion des threads empêche les développeurs de réaliser des interfaces réactives.
I. Le modèle de gestion des threads▲
Le toolkit Swing a été créé en partant du principe que toutes les opérations affectant l'état des composants seraient réalisées dans l'EDT. Cela est également vrai pour la création des composants graphiques. Ainsi, contrairement à ce que l'on a cru pendant des années, une méthode main() telle que celle décrite dans le listing 1 n'est pas valide et peut entraîner des problèmes d'inter blocage. La création de la fenêtre et de son contenu devrait avoir lieu dans l'EDT. Swing n'est donc pas une API « thread safe » et ne doit être manipulée que depuis un seul et unique thread, l'EDT. Les concepteurs de Swing ont fait ce choix pour garantir la prédictibilité d'exécution des événements et des rafraîchissements, mais également pour en simplifier l'utilisation et le débogage.
Swing n'est d'ailleurs pas la seule API à fonctionner ainsi : SWT, QT ou encore les WinForms de .NET fonctionnent sur un modèle de thread unique. Nous savons à présent que nous devons absolument éviter d'exécuter des opérations longues dans les gestionnaires d'événement. La solution évidente consiste à placer le code dans un autre thread, comme dans le listing 2.
public
void
actionPerformed
(
ActionEvent e) {
new
Thread
(
new
Runnable
(
) {
public
void
run
(
) {
String text =
readHugeFile
(
);
textArea.setText
(
text);
}
}
).start
(
);
}
Dans cet exemple, un nouveau thread est exécuté pour lire un fichier de plusieurs mégaoctets et en placer le contenu dans une JTextArea à l'écran. À première vue, cet exemple répond à notre problème en débloquant l'EDT. Malheureusement, il viole la règle du thread unique, puisque nous accédons à un composant Swing depuis un thread qui n'est pas l'EDT. L'API de Swing offre une solution sous la forme de trois méthodes de la classe SwingUtilities. La première s'intitule invokeLater() et permet de poster une tâche dans l'EDT. Le listing 3 corrige le listing 2 pour s'assurer que la mise à jour de la JTextArea a lieu dans l'EDT.
public
void
actionPerformed
(
ActionEvent e) {
new
Thread
(
new
Runnable
(
) {
public
void
run
(
) {
final
String text =
readHugeFile
(
);
SwingUtilities.invokeLater
(
new
Runnable
(
) {
public
void
run
(
) {
textArea.setText
(
text);
}
}
}
}
).start
(
);
}
Lorsque l'action est déclenchée, l'EDT exécute la méthode actionPerformed(). Cette dernière, pour ne pas bloquer l'EDT, crée un nouveau thread et le démarre. Dans ce thread, le programme lit un fichier et stocke le résultat dans une chaîne de caractères. Enfin, il crée une nouvelle tâche, une instance de Runnable, qui est placée à la fin de la file de l'EDT par invokeLater(). Cette technique doit être employée même lorsque l'opération réalisée par le gestionnaire d'événement doit avoir lieu dans l'EDT, comme dans l'exemple 4.
public
void
actionPerformed
(
ActionEvent e) {
JFrame f =
new
JFrame
(
);
f.getContentPane
(
).setLayout
(
new
FlowLayout
(
));
for
(
int
i =
0
; i <
5000
; i++
) {
f.add
(
new
JLabel
(
"Label "
+
i));
}
f.pack
(
);
f.setVisible
(
true
);
}
Ce code respecte la règle du thread unique, mais bloque l'EDT, le bouton qui a déclenché l'action reste donc enfoncé. Vous devez donc employer invokeLater() pour corriger ce problème. Nous avons vu précédemment que la méthode main() du listing 1 est invalide puisqu'elle ne crée pas l'interface graphique dans l'EDT. Essayez à titre d'exercice de la corriger pour garantir que les composants Swing sont créés dans l'EDT. La deuxième méthode que vous pouvez utiliser pour gérer convenablement les threads avec Swing s'appelle isEventDispatchThread(). Elle renvoie vrai lorsque le code s'exécute dans l'EDT. Vous pouvez ainsi créer des méthodes utilisables depuis l'EDT et depuis un thread quelconque ainsi que le montre l'exemple 5.
private
void
incrementLabel
(
) {
tickCounter++
;
Runnable code =
new
Runnable
(
) {
public
void
run
(
) {
counter.setText
(
String.valueOf
(
tickCounter));
}
}
;
if
(
SwingUtilities.isEventDispatchThread
(
)) {
code.run
(
);
}
else
{
SwingUtilities.invokeLater
(
code);
}
}
Dans ce code, tickCounter est un entier qui est utilisé pour changer le texte du JLabel counter. Si incrementLabel() est invoquée dans l'EDT, le code est directement exécuté. Dans le cas contraire, invokeLater() est appelé pour éviter tout problème. Cette méthode est donc parfaitement « thread safe » est peut être invoquée depuis un gestionnaire d'événements comme actionPerformed() ou depuis un thread que vous avez créé. Le code complet de cet exemple se trouve sur le CD-Rom dans le fichier SwingThreading.java.
La troisième et dernière méthode indispensable est également la moins utilisée. Il s'agit d'invokeAndWait() dont le comportement est similaire à invokeLater(). Cette méthode permet d'exécuter du code dans l'EDT et de bloquer le thread courant pendant ce temps. Prenons le cas d'une application intelligente capable de détecter une latence importante lors de l'ouverture d'un fichier. Cette application lit le fichier dans un thread et si au bout de 10 secondes l'opération n'est pas terminée, une boîte de dialogue apparaît pour demander à l'utilisateur s'il souhaite l'annuler ou la continuer. Pour implémenter cette fonctionnalité, vous devez normalement initialiser un verrou pour bloquer le thread de lecture puis poster l'affichage de la boîte de dialogue dans l'EDT. Vous risquez évidemment d'introduire un bug susceptible de provoquer un inter blocage. Le listing 6 montre comment utiliser invokeAndWait() pour simplifier votre code.
try
{
final
int
[] answer =
new
int
[1
];
SwingUtilities.invokeAndWait
(
new
Runnable
(
) {
public
void
run
(
) {
answer[0
] =
JOptionPane.showConfirmDialog
(
SwingThreadingWait.this
,
"Abort long operation?"
,
"Abort?"
,
JOptionPane.YES_NO_OPTION);
}
}
);
if
(
answer[0
] ==
JOptionPane.YES_OPTION) {
return
;
}
}
catch
(
InterruptedException ie) {
}
catch
(
InvocationTargetException ite) {
}
Vous serez sans doute surpris de l'utilisation d'un tableau de 1 entier pour conserver le résultat de la boîte de dialogue. Souvenez-vous que les classes anonymes ne peuvent accéder qu'à des membres de classe, des membres d'instance ou à des variables locales finales. Nous ne pouvons donc pas utiliser un final int puisqu'il ne pourrait alors pas être modifié lors de l'affection du résultat de showConfirmDialog(). En déclarant un tableau d'entiers (de taille 1) nous pouvons en revanche modifier l'entier puisque nous ne modifions pas l'objet lui-même, à savoir le tableau. L'exemple complet se trouve sur le CD-Rom dans le fichier SwingThreadingWait.java.
Outre ces méthodes utilitaires, tous les composants Swing possèdent des méthodes parfaitement thread safe : repaint(), invalidate() et revalidate(). Les deux dernières servent à forcer un composant à réorganiser ses enfants en fonction du layout choisi. La première méthode, repaint(), force le rafraîchissement de l'affichage d'un composant. Le listing 7 présente un extrait de l'application SafeRepaint.java présente sur le CD-Rom :
public
class
SafeComponent extends
JLabel {
public
SafeComponent
(
) {
super
(
"Safe Repaint"
);
}
public
void
paintComponent
(
Graphics g) {
super
.paintComponent
(
g);
System.out.println
(
SwingUtilities.isEventDispatchThread
(
));
}
}
SafeComponent est un composant dérivé de JLabel qui affiche « true » dans la console si sa méthode d'affichage est exécutée depuis l'EDT. Le programme SafeRepaint crée un thread qui appelle safeComponent.repaint() toutes les secondes. En exécutant l'application, nous pouvons constater que l'affichage est toujours réalisé depuis l'EDT.
Sachez enfin que les javax.swing.Timer permettent d'exécuter des actions dans l'EDT à intervalle régulier.
II. Gestion avancée des tâches▲
Bien que la classe SwingUtilities permette d'améliorer considérablement les performances perçues des applications Swing, le code engendré par son utilisation s'avère difficile à lire et à maintenir. Conscients de cet écueil, les concepteurs de Swing ont créé un nouvel outil pour gérer facilement les longues opérations, la classe SwingWorker. Celle-ci est à l'heure actuelle disponible sur Internet, plus particulièrement dans le projet JDNC. La prochaine version 1.6 de Java SE devrait enfin l'intégrer à l'API officielle.
Note de la rédaction : La classe SwingWorker sera bien présente dans Java SE 6. Mais, en comparaison de la classe indiquée dans cette page, elle a été grandement remaniée afin de profiter des nouveautés du langage apportées par Java 5.0 (notamment les Generics).
Toutefois le principe général reste le même…
Pour plus d'informations, veuillez consulter la documentation officielle de la classe :
http://java.sun.com/javase/6/docs/api/javax/swing/SwingWorker.html
SwingWorker est une classe abstraite exposant les méthodes construct(), finished(), get(), interrupt() et start(). Pour l'utiliser, nous devons la surcharger et implémenter la méthode abstraite construct(). Une fois le worker créé, vous pouvez lancer le traitement en invoquant la méthode start() qui se chargera de créer un thread qui exécutera construct(). C'est dans cette dernière que vous devez placer les tâches susceptibles de bloquer l'EDT. Souvenez-vous que construct() ne s'exécute pas dans l'EDT et que toute mise à jour de l'interface graphique devra être effectuée à l'aide d'invokeLater() ou d'invokeAndWait(). En examinant la signature de construct(), vous constaterez que vous pouvez retourner une valeur sous forme d'un Object. Cette valeur peut être récupérée après exécution en invoquant la méthode get(). La plupart du temps vous utiliserez cette valeur dans finished(), appelée après exécution de construct(). Vous pouvez enfin interrompre l'exécution à tout moment à l'aide d'interrupt(). Le listing 8 présente le SwingWorker utilisé par l'application FileFinder.java, présente sur le CD-Rom.
public
class
FileFinderWorker extends
SwingWorker {
private
JList filesList;
private
DefaultListModel filesListModel;
public
FileFinderWorker
(
JList filesList) {
this
.filesList =
filesList;
this
.filesListModel =
new
DefaultListModel
(
);
this
.filesList.setModel
(
this
.filesListModel);
}
public
void
start
(
) {
filesListModel.removeAllElements
(
);
filesListModel.addElement
(
"Searching *.java..."
);
super
.start
(
);
}
public
void
finished
(
) {
filesListModel.removeAllElements
(
);
String[] files =
(
String[]) get
(
);
for
(
String file: files) {
filesListModel.addElement
(
file);
}
}
public
Object construct
(
) {
FileFilter filter =
new
FileFilter
(
) {
public
boolean
accept
(
File pathname) {
return
pathname.getName
(
).endsWith
(
".java"
);
}
}
;
final
List list =
new
ArrayList
(
);
final
int
[] counter =
new
int
[1
];
try
{
FileTreeWalker walker =
new
FileTreeWalker
(
"."
, filter);
walker.walk
(
new
FileTreeWalk
(
) {
public
void
walk
(
File path) {
list.add
(
path.toString
(
));
counter[0
]++
;
SwingUtilities.invokeLater
(
new
Runnable
(
) {
public
void
run
(
) {
filesListModel.setElementAt
(
"Searching *.java... ("
+
counter[0
] +
")"
, 0
);
}
}
);
}
}
);
}
catch
(
IOException e) {
}
return
list.toArray
(
new
String[list.size
(
)]);
}
}
Ce worker recherche tous les fichiers portant l'extension .java dans le répertoire courant d'exécution. Lorsque le worker démarre, la liste contenant les résultats est vidée et un message d'attente y est inséré. Nous modifions la liste dans start() puisque cette méthode est appelée par le gestionnaire d'événements actionPerformed(), exécuté dans l'EDT. La méthode construct() génère un tableau contenant la liste de tous les fichiers répondant à notre critère. À chaque fichier trouvé, le message d'attente est modifié pour indiquer la progression. Pour cela, nous modifions la liste dans l'EDT. La méthode finished(), exécutée dans l'EDT, se charge enfin de récupérer les résultats du travail de construct() et de les ajouter à la liste pour les afficher.
En exploitant intelligemment l'EDT vous pouvez dès maintenant réaliser des interfaces Swing aux performances excellentes. Pour vous en convaincre, essayez d'exécuter FileFinder à la racine de votre disque dur.