Afin d'améliorer la performance et la réactivité de vos programmes Java, nous allons parler de l'EDT, pour « Event Dispatch Thread ». Comme son nom l'indique, il s'agit d'un thread, d'une pile d'appel. Cependant celui-ci a une particularité, il s'occupe de gérer toutes les modifications portant sur un composant graphique :
- le redimensionnement ;
- le changement de couleur ;
- le changement de valeur ;
- …
Vos applications graphiques seront plus performantes et plus sûres lorsque vous utiliserez ce thread pour effectuer tous les changements qui pourraient intervenir sur votre IHM.
Présentation des protagonistes
Vous savez déjà que, lorsque vous lancez un programme Java en mode console, un thread principal est démarré pour empiler les instructions de votre programme jusqu'à la fin. Ce que vous ignorez peut-être, c'est qu'un autre thread est lancé : celui qui s'occupe de toutes les tâches de fond (lancement de nouveaux threads…).
Or depuis un certain temps, nous ne travaillons plus en mode console mais en mode graphique. Et, je vous le donne en mille, un troisième thread est lancé qui se nomme l'EDT (Event Dispatch Thread). Comme je vous le disais, c'est dans celui-ci que tous les changements portant sur des composants sont exécutés. Voici un petit schéma illustrant mes dires (figure suivante).
La philosophie de Java est que toute modification apportée à un composant se fait obligatoirement dans l'EDT : lorsque vous utilisez une méthode actionPerformed
, celle-ci, son contenu compris, est exécutée dans l'EDT (c'est aussi le cas pour les autres intercepteurs d'événements). La politique de Java est simple : toute action modifiant l'état d'un composant graphique doit se faire dans un seul et unique thread, l'EDT. Vous vous demandez sûrement pourquoi. C'est simple, les composants graphiques ne sont pas « thread-safe » : ils ne peuvent pas être utilisés par plusieurs threads simultanément et assurer un fonctionnement sans erreurs ! Alors, pour s'assurer que les composants sont utilisés au bon endroit, on doit placer toutes les interactions dans l'EDT.
Par contre, cela signifie que si dans une méthode actionPerformed
nous avons un traitement assez long, c'est toute notre interface graphique qui sera figée !
Vous vous souvenez de la première fois que nous avons tenté de contrôler notre animation ? Lorsque nous cliquions sur le bouton pour la lancer, notre interface était bloquée étant donné que la méthode contenant une boucle infinie n'était pas dépilée du thread dans lequel elle était lancée. D'ailleurs, si vous vous souvenez bien, le bouton s'affichait comme si on n'avait pas relâché le clic ; c'était dû au fait que l'exécution de notre méthode se faisait dans l'EDT, bloquant ainsi toutes les actions sur nos composants.
Voici un schéma, en figure suivante, résumant la situation.
Imaginez la ligne comme une tête de lecture. Il y a déjà quelques événements à faire dans l'EDT :
- la création de la fenêtre ;
- la création et mise à jour de composants ;
- …
Seulement voilà, nous cliquons sur un bouton engendrant un long, un très long traitement dans l'EDT (dernier bloc) : du coup, toute notre IHM est figée ! Non pas parce que Java est lent, mais parce que nous avons exécuté un traitement au mauvais endroit. Il existe toutefois quelques méthodes thread-safe :
paint()
etrepaint()
;validate()
,invalidate()
etrevalidate()
.
Celles-ci peuvent être appelées depuis n'importe quel thread.
À ce stade, une question se pose : comment exécuter une action dans l'EDT ? C'est exactement ce que nous allons voir.
Utiliser l'EDT
Java vous fournit la classe SwingUtilities
qui offre plusieurs méthodes statiques permettant d'insérer du code dans l'EDT :
invokeLater(Runnable doRun)
: exécute le thread en paramètre dans l'EDT et rend immédiatement la main au thread principal ;invokeAndWait(Runnable doRun)
: exécute le thread en paramètre dans l'EDT et attend la fin de celui-ci pour rendre la main au thread principal ;isEventDispatchThread()
: retourne vrai si le thread dans lequel se trouve l'instruction est dans l'EDT.
Maintenant que vous savez comment exécuter des instructions dans l'EDT, il nous faut un cas concret :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //CTRL + SHIFT + O pour générer les imports public class Test1 { static int count = 0, count2 = 0; static JButton bouton = new JButton("Pause"); public static void main(String[] args){ JFrame fen = new JFrame("EDT"); fen.getContentPane().add(bouton); fen.setSize(200, 100); fen.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); fen.setLocationRelativeTo(null); fen.setVisible(true); updateBouton(); System.out.println("Reprise du thread principal"); } public static void updateBouton(){ for(int i = 0; i < 5; i++){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } bouton.setText("Pause " + ++count); } } } |
Au lancement de ce test, vous constatez que le thread principal ne reprend la main qu'après la fin de la méthode updateBouton()
, comme le montre la figure suivante.
La solution pour rendre la main au thread principal avant la fin de la méthode, vous la connaissez : créez un nouveau thread, mais cette fois vous allez également exécuter la mise à jour du bouton dans l'EDT. Voilà donc ce que nous obtenons :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | //CTRL + SHIFT + O pour générer les imports public class Test1 { static int count = 0; static JButton bouton = new JButton("Pause"); public static void main(String[] args){ JFrame fen = new JFrame("EDT"); fen.getContentPane().add(bouton); fen.setSize(200, 100); fen.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); fen.setLocationRelativeTo(null); fen.setVisible(true); updateBouton(); System.out.println("Reprise du thread principal"); } public static void updateBouton(){ //Le second thread new Thread(new Runnable(){ public void run(){ for(int i = 0; i < 5; i++){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //Modification de notre composant dans l'EDT Thread t = new Thread(new Runnable(){ public void run(){ bouton.setText("Pause " + ++count); } }); if(SwingUtilities.isEventDispatchThread()) t.start(); else{ System.out.println("Lancement dans l' EDT"); SwingUtilities.invokeLater(t); } } } }).start(); } } |
Le rendu correspond à la figure suivante.
Ce code est rudimentaire, mais il a l'avantage de vous montrer comment utiliser les méthodes présentées. Cependant, pour bien faire, j'aurais aussi dû inclure la création de la fenêtre dans l'EDT, car tout ce qui touche aux composants graphiques doit être mis dans celui-ci.
Pour finir notre tour du sujet, il manque encore la méthode invokeAndWait()
. Celle-ci fait la même chose que sa cousine, mais comme je vous le disais, elle bloque le thread courant jusqu'à la fin de son exécution. De plus, elle peut lever deux exceptions : InterruptedException
et InvocationTargetException
.
Depuis la version 6 de Java, une classe est mise à disposition pour effectuer des traitements lourds et interagir avec l'EDT.
La classe SwingWorker<T, V>
Cette dernière est une classe abstraite permettant de réaliser des traitements en tâche de fond tout en dialoguant avec les composants graphiques via l'EDT, aussi bien en cours de traitement qu'en fin de traitement. Dès que vous aurez un traitement prenant pas mal de temps et devant interagir avec votre IHM, pensez aux SwingWorker
.
Vu que cette classe est abstraite, vous allez devoir redéfinir une méthode : doInBackground()
. Elle permet de redéfinir ce que doit faire l'objet en tâche de fond. Une fois cette tâche effectuée, la méthode doInBackground()
prend fin. Vous avez la possibilité de redéfinir la méthode done()
, qui a pour rôle d'interagir avec votre IHM tout en s'assurant que ce sera fait dans l'EDT. Implémenter la méthode done()
est optionnel, vous n'êtes nullement tenus de le faire.
Voici un exemple d'utilisation :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | //CTRL + SHIFT + O pour générer les imports public class Test1 { static int count = 0; static JButton bouton = new JButton("Pause"); public static void main(String[] args){ JFrame fen = new JFrame("EDT"); fen.getContentPane().add(bouton); fen.setSize(200, 100); fen.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); fen.setLocationRelativeTo(null); fen.setVisible(true); updateBouton(); System.out.println("Reprise du thread principal"); } public static void updateBouton(){ //On crée le SwingWorker SwingWorker sw = new SwingWorker(){ protected Object doInBackground() throws Exception { for(int i = 0; i < 5; i++){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } return null; } public void done(){ if(SwingUtilities.isEventDispatchThread()) System.out.println("Dans l'EDT ! "); bouton.setText("Traitement terminé"); } }; //On lance le SwingWorker sw.execute(); } } |
Vous constatez que le traitement se fait bien en tâche de fond, et que votre composant est mis à jour dans l'EDT. La preuve à la figure suivante.
Je vous disais plus haut que vous pouviez interagir avec l'EDT pendant le traitement. Pour ce faire, il suffit d'utiliser la méthode setProgress(int progress)
combinée avec l'événement PropertyChangeListener
, qui sera informé du changement d'état de la propriété progress
.
Voici un code d'exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | //CTRL + SHIFT + O pour générer les imports public class Test1 { static int count = 0; static JButton bouton = new JButton("Pause"); public static void main(String[] args){ JFrame fen = new JFrame("EDT"); fen.getContentPane().add(bouton); fen.setSize(200, 100); fen.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); fen.setLocationRelativeTo(null); fen.setVisible(true); updateBouton(); System.out.println("Reprise du thread principal"); } public static void updateBouton(){ SwingWorker sw = new SwingWorker(){ protected Object doInBackground() throws Exception { for(int i = 0; i < 5; i++){ try { //On change la propriété d'état setProgress(i); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } return null; } public void done(){ if(SwingUtilities.isEventDispatchThread()) System.out.println("Dans l'EDT ! "); bouton.setText("Traitement terminé"); } }; //On écoute le changement de valeur pour la propriété sw.addPropertyChangeListener(new PropertyChangeListener(){ //Méthode de l'interface public void propertyChange(PropertyChangeEvent event) { //On vérifie tout de même le nom de la propriété if("progress".equals(event.getPropertyName())){ if(SwingUtilities.isEventDispatchThread()) System.out.println("Dans le listener donc dans l'EDT ! "); //On récupère sa nouvelle valeur bouton.setText("Pause " + (Integer) event.getNewValue()); } } }); //On lance le SwingWorker sw.execute(); } } |
La figure suivante présente le résultat du code.
Les méthodes que vous avez vues jusqu'ici sont issues de la classe SwingWorker
, qui implémente l'interface java.util.concurrent.Future
, offrant les méthodes suivantes :
get()
: permet à la méthodedoInBackground()
de renvoyer son résultat à d'autres threads ;cancel()
: essaie d'interrompre la tâche dedoInBackground()
en cours ;isCancelled()
: retourne vrai si l'action a été interrompue ;isDone()
: retourne vrai si l'action est terminée
Nous pouvons donc utiliser ces méthodes dans notre objet SwingWorker
afin de récupérer le résultat d'un traitement. Pour le moment, nous n'avons pas utilisé la généricité de cette classe. Or, comme l'indique le titre de cette section, SwingWorker
peut prendre deux types génériques. Le premier correspond au type de renvoi de la méthode doInBackground()
et, par extension, au type de renvoi de la méthode get()
. Le deuxième est utilisé comme type de retour intermédiaire pendant l'exécution de la méthode doInBackground()
.
Afin de gérer les résultats intermédiaires, vous pouvez utiliser les méthodes suivantes :
publish(V value)
: publie le résultat intermédiaire pour la méthodeprogress(List<V> list)
;progress(List<V> list)
: permet d'utiliser le résultat intermédiaire pour un traitement spécifique.
Voici l'exemple utilisé jusqu'ici avec les compléments :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | //CTRL + SHIFT + O pour générer les imports public class Test1 { static int count = 0; static JButton bouton = new JButton("Pause"); public static void main(String[] args){ JFrame fen = new JFrame("EDT"); fen.getContentPane().add(bouton); fen.setSize(200, 100); fen.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); fen.setLocationRelativeTo(null); fen.setVisible(true); updateBouton(); System.out.println("Reprise du thread principal"); } public static void updateBouton(){ //On crée un Worker générique, cette fois SwingWorker sw = new SwingWorker<Integer, String>(){ protected Integer doInBackground() throws Exception { int i; for(i = 0; i < 5; i++){ try { //On change la propriété d'état setProgress(i); //On publie un résultat intermédiaire publish("Tour de boucle N° " + (i+1)); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } return i; } public void done(){ if(SwingUtilities.isEventDispatchThread()) System.out.println("Dans l'EDT ! "); try { //On utilise la méthode get() pour récupérer le résultat //de la méthode doInBackground() bouton.setText("Traitement terminé au bout de "+get()+" fois !"); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } //La méthode gérant les résultats intermédiaires public void process(List<String> list){ for(String str : list) System.out.println(str); } }; //On écoute le changement de valeur pour la propriété sw.addPropertyChangeListener(new PropertyChangeListener(){ //Méthode de l'interface public void propertyChange(PropertyChangeEvent event) { //On vérifie tout de même le nom de la propriété if("progress".equals(event.getPropertyName())){ if(SwingUtilities.isEventDispatchThread()) System.out.println("Dans le listener donc dans l'EDT ! "); //On récupère sa nouvelle valeur bouton.setText("Pause " + (Integer) event.getNewValue()); } } }); //On lance le SwingWorker sw.execute(); } } |
Et le résultat, à la figure suivante, parle de lui-même.
Voilà : vous savez maintenant comment utiliser l'EDT et les SwingWorker
. Vos applications n'en seront que plus réactives !
- Au lancement d'un programme Java, trois threads se lancent : le thread principal, celui gérant les tâches de fond et l'EDT.
- Java préconise que toute modification des composants graphiques se fasse dans l'EDT.
- Si vos IHM se figent, c'est peut-être parce que vous avez lancé un traitement long dans l'EDT.
- Afin d'améliorer la réactivité de vos applications, vous devez choisir au mieux dans quel thread vous allez traiter vos données.
- Java offre la classe
SwingUtilities
, qui permet de lancer des actions dans l'EDT depuis n'importe quel thread. - Depuis Java 6, la classe
SwingWorker(<T, V>)
vous offre la possibilité de lancer des traitements dans un thread en vous assurant que les mises à jour des composants se feront dans l'EDT.