Livre Java .book Page I Jeudi, 25. novembre...

publicité
Livre Java .book Page I Jeudi, 25. novembre 2004 3:04 15
Livre Java .book Page I Jeudi, 25. novembre 2004 3:04 15
Au cœur de Java 2
volume 1
Notions fondamentales
Cay S. Horstmann
et Gary Cornell
Livre Java .book Page II Jeudi, 25. novembre 2004 3:04 15
CampusPress a apporté le plus grand soin à la réalisation de ce livre afin de vous fournir une information complète et fiable. Cependant, CampusPress n’assume de responsabilités, ni pour son utilisation, ni pour les contrefaçons de brevets ou atteintes aux droits de tierces personnes qui pourraient
résulter de cette utilisation.
Les exemples ou les programmes présents dans cet ouvrage sont fournis pour illustrer les descriptions théoriques. Ils ne sont en aucun cas destinés à une utilisation commerciale ou professionnelle.
CampusPress ne pourra en aucun cas être tenu pour responsable des préjudices ou dommages de
quelque nature que ce soit pouvant résulter de l’utilisation de ces exemples ou programmes.
Tous les noms de produits ou marques cités dans ce livre sont des marques déposées par leurs
propriétaires respectifs.
Publié par CampusPress
47 bis, rue des Vinaigriers
75010 PARIS
Tél. : 01 72 74 90 00
Titre original : Core Java 2, volume 1 Fundamentals
Traduit de l’américain par :
Christiane Silhol et Nathalie Le Guillou de Penanros
Mise en pages : TyPAO
ISBN : 2-7440-1833-3
Copyright © 2004 CampusPress
Tous droits réservés
CampusPress est une marque
de Pearson Education France
ISBN original : 0-13-148202-5
Copyright © 2005 Sun Microsystems, Inc.
Tous droits réservés
Sun Microsystems Inc.
901 San Antonio Road, Palo Alto, California
94303 USA
Toute reproduction, même partielle, par quelque procédé que ce soit, est interdite sans autorisation préalable. Une copie par
xérographie, photographie, film, support magnétique ou autre, constitue une contrefaçon passible des peines prévues par la
loi, du 11 mars 1957 et du 3 juillet 1995, sur la protection des droits d’auteur.
Livre Java .book Page III Jeudi, 25. novembre 2004 3:04 15
Table des matières
Introduction .................................................................................................................................
1
Avertissement au lecteur ..........................................................................................................
A propos de ce livre .................................................................................................................
Conventions .............................................................................................................................
Exemples de code ....................................................................................................................
1
3
5
5
Chapitre 1. Une introduction à Java .........................................................................................
7
Java, plate-forme de programmation .......................................................................................
Les termes clés du livre blanc de Java .....................................................................................
Simplicité ...........................................................................................................................
Orienté objet .......................................................................................................................
Distribué .............................................................................................................................
Fiabilité ..............................................................................................................................
Sécurité ...............................................................................................................................
Architecture neutre .............................................................................................................
Portabilité ...........................................................................................................................
Interprété ............................................................................................................................
Performances élevées .........................................................................................................
Multithread .........................................................................................................................
Java, langage dynamique ....................................................................................................
Java et Internet .........................................................................................................................
Bref historique de Java .............................................................................................................
Les idées fausses les plus répandues concernant Java .............................................................
7
8
8
9
10
10
10
11
12
12
12
13
13
14
15
18
Chapitre 2. L’environnement de programmation de Java .....................................................
23
Installation du kit de développement Java ...............................................................................
Télécharger le JDK ............................................................................................................
23
24
Livre Java .book Page IV Jeudi, 25. novembre 2004 3:04 15
IV
Table des matières
Configurer le chemin d’exécution ......................................................................................
Installer la bibliothèque et la documentation .....................................................................
Installer les exemples de programmes ...............................................................................
Explorer les répertoires de Java .........................................................................................
Choix de l’environnement de développement .........................................................................
Utilisation des outils de ligne de commande ...........................................................................
Conseils pour la recherche d’erreurs ..................................................................................
Utilisation d’un environnement de développement intégré .....................................................
Localiser les erreurs de compilation ..................................................................................
Compilation et exécution de programmes à partir d’un éditeur de texte .................................
Exécution d’une application graphique ...................................................................................
Elaboration et exécution d’applets ...........................................................................................
25
26
26
27
28
29
30
32
34
35
37
39
Chapitre 3. Structures fondamentales de la programmation Java .......................................
Un exemple simple de programme Java ..................................................................................
Commentaires ..........................................................................................................................
Types de données .....................................................................................................................
Entiers ................................................................................................................................
Types à virgule flottante .....................................................................................................
Le type char .......................................................................................................................
Type booléen ......................................................................................................................
Variables ..................................................................................................................................
Initialisation des variables ......................................................................................................
Constantes ..........................................................................................................................
Opérateurs ................................................................................................................................
Opérateurs d’incrémentation et de décrémentation ...........................................................
Opérateurs relationnels et booléens ...................................................................................
Opérateurs binaires ............................................................................................................
Fonctions mathématiques et constantes .............................................................................
Conversions de types numériques ......................................................................................
Transtypages ......................................................................................................................
Parenthèses et hiérarchie des opérateurs ............................................................................
Types énumérés ..................................................................................................................
Chaînes ....................................................................................................................................
Points et unités de code ......................................................................................................
Sous-chaînes .......................................................................................................................
Modification de chaînes .....................................................................................................
Concaténation .....................................................................................................................
Test d’égalité des chaînes ...................................................................................................
Lire la documentation API en ligne ...................................................................................
43
44
47
47
48
49
50
52
52
53
54
54
56
56
57
58
59
60
60
61
62
62
63
63
65
65
68
Livre Java .book Page V Jeudi, 25. novembre 2004 3:04 15
Table des matières
V
Entrées et sorties ......................................................................................................................
Lire les caractères entrés ....................................................................................................
Mise en forme de l’affichage ..............................................................................................
Flux d’exécution ......................................................................................................................
Portée d’un bloc .................................................................................................................
Instructions conditionnelles ...............................................................................................
Boucles ..............................................................................................................................
Boucles déterminées ..........................................................................................................
Sélections multiples — l’instruction switch .......................................................................
Interrompre le flux d’exécution ..........................................................................................
Grands nombres .......................................................................................................................
Tableaux ...................................................................................................................................
La boucle "for each" ...........................................................................................................
Initialiseurs de tableaux et tableaux anonymes ..................................................................
Copie des tableaux .............................................................................................................
Paramètres de ligne de commande .....................................................................................
Tri d’un tableau ..................................................................................................................
Tableaux multidimensionnels .............................................................................................
Tableaux irréguliers ............................................................................................................
70
70
73
78
78
79
82
85
88
90
92
94
95
96
97
98
99
102
104
Chapitre 4. Objets et classes ......................................................................................................
Introduction à la programmation orientée objet ......................................................................
Le vocabulaire de la POO ..................................................................................................
Les objets ...........................................................................................................................
Relations entre les classes ..................................................................................................
Comparaison entre POO et programmation procédurale traditionnelle .............................
Utilisation des classes existantes .............................................................................................
Objets et variables objet .....................................................................................................
La classe GregorianCalendar de la bibliothèque Java .......................................................
Les méthodes d’altération et les méthodes d’accès ...........................................................
Construction de vos propres classes ........................................................................................
Une classe Employee ..........................................................................................................
Travailler avec plusieurs fichiers source ............................................................................
Analyser la classe Employee ..............................................................................................
Premiers pas avec les constructeurs ...................................................................................
Paramètres implicites et explicites .....................................................................................
Avantages de l’encapsulation .............................................................................................
Privilèges d’accès fondés sur les classes ............................................................................
Méthodes privées ...............................................................................................................
Champs d’instance final .....................................................................................................
109
110
111
112
113
115
116
117
120
121
127
127
130
130
131
132
133
136
136
136
Livre Java .book Page VI Jeudi, 25. novembre 2004 3:04 15
VI
Table des matières
Champs et méthodes statiques .................................................................................................
Champs statiques ................................................................................................................
Constantes ..........................................................................................................................
Méthodes statiques .............................................................................................................
Méthodes "factory" ............................................................................................................
La méthode main ................................................................................................................
Paramètres des méthodes .........................................................................................................
Construction d’un objet ..........................................................................................................
Surcharge ............................................................................................................................
Initialisation des champs par défaut ...................................................................................
Constructeurs par défaut ....................................................................................................
Initialisation explicite de champ ........................................................................................
Noms de paramètres ...........................................................................................................
Appel d’un autre constructeur ............................................................................................
Blocs d’initialisation ..........................................................................................................
Destruction des objets et méthode finalize .........................................................................
Packages ..................................................................................................................................
Importation des classes ......................................................................................................
Imports statiques ................................................................................................................
Ajout d’une classe dans un package ..................................................................................
Comment la machine virtuelle localise les classes ............................................................
Visibilité dans un package ..................................................................................................
Commentaires pour la documentation .....................................................................................
Insertion des commentaires ................................................................................................
Commentaires de classe .....................................................................................................
Commentaires de méthode .................................................................................................
Commentaires de champ ....................................................................................................
Commentaires généraux .....................................................................................................
Commentaires de package et d’ensemble ..........................................................................
Extraction des commentaires .............................................................................................
Conseils pour la conception de classes ....................................................................................
137
137
138
139
140
140
143
149
149
149
150
151
152
152
153
157
157
158
159
160
163
166
167
168
168
169
170
170
171
171
172
Chapitre 5. L’héritage ................................................................................................................
Classes, superclasses et sous-classes .......................................................................................
Hiérarchie d’héritage ..........................................................................................................
Polymorphisme ..................................................................................................................
Liaison dynamique .............................................................................................................
Empêcher l’héritage : les classes et les méthodes final ......................................................
Transtypage ........................................................................................................................
Classes abstraites ................................................................................................................
Accès protégé .....................................................................................................................
175
176
182
183
184
187
188
190
195
Livre Java .book Page VII Jeudi, 25. novembre 2004 3:04 15
Table des matières
VII
Object : la superclasse cosmique .............................................................................................
La méthode equals .............................................................................................................
Test d’égalité et héritage ....................................................................................................
La méthode hashCode .......................................................................................................
La méthode toString ...........................................................................................................
Listes de tableaux génériques ..................................................................................................
Accéder aux éléments d’une liste de tableaux ...................................................................
Compatibilité entre les listes de tableaux brutes et tapées .................................................
Enveloppes d’objets et autoboxing ..........................................................................................
Méthodes ayant un nombre variable de paramètres ...........................................................
Réflexion ..................................................................................................................................
La classe Class ...................................................................................................................
La réflexion pour analyser les caractéristiques d’une classe ..............................................
La réflexion pour l’analyse des objets à l’exécution ..........................................................
La réflexion pour créer un tableau générique .....................................................................
Les pointeurs de méthodes .................................................................................................
Classes d’énumération .............................................................................................................
Conseils pour l’utilisation de l’héritage ...................................................................................
196
197
198
200
202
207
210
214
215
218
219
220
223
228
232
236
239
241
Chapitre 6. Interfaces et classes internes .................................................................................
Interfaces ..................................................................................................................................
Propriétés des interfaces .....................................................................................................
Interfaces et classes abstraites ............................................................................................
Clonage d’objets ......................................................................................................................
Interfaces et callbacks ..............................................................................................................
Classes internes ........................................................................................................................
Accéder à l’état d’un objet à l’aide d’une classe interne ...................................................
Règles particulières de syntaxe pour les classes internes ...................................................
Utilité, nécessité et sécurité des classes internes ................................................................
Classes internes locales ......................................................................................................
Classes internes anonymes .................................................................................................
Classes internes statiques ...................................................................................................
Proxies .....................................................................................................................................
Propriétés des classes proxy ...............................................................................................
243
244
249
250
251
257
260
261
264
265
267
270
272
275
279
Chapitre 7. Programmation graphique ....................................................................................
Introduction à Swing ................................................................................................................
Création d’un cadre ..................................................................................................................
Positionnement d’un cadre ......................................................................................................
Affichage des informations dans un panneau ..........................................................................
Formes 2D ...............................................................................................................................
281
282
285
288
294
298
Livre Java .book Page VIII Jeudi, 25. novembre 2004 3:04 15
VIII
Table des matières
Couleurs ...................................................................................................................................
Remplir des formes ............................................................................................................
Texte et polices ........................................................................................................................
Images ......................................................................................................................................
306
309
311
319
Chapitre 8. Gestion des événements .........................................................................................
Introduction à la gestion des événements ................................................................................
Exemple : gestion d’un clic de bouton ...............................................................................
Etre confortable avec les classes internes ..........................................................................
Transformer des composants en écouteurs d’événement ...................................................
Exemple : modification du "look and feel" ........................................................................
Exemple : capture des événements de fenêtre ....................................................................
Hiérarchie des événements AWT .............................................................................................
Evénements sémantiques et de bas niveau ...............................................................................
Résumé de la gestion des événements ...............................................................................
Types d’événements de bas niveau ..........................................................................................
Evénements du clavier .......................................................................................................
Evénements de la souris .....................................................................................................
Evénements de focalisation ................................................................................................
Actions .....................................................................................................................................
Multidiffusion ..........................................................................................................................
Implémenter des sources d’événements ..................................................................................
325
326
328
333
336
338
341
345
347
349
350
350
356
365
368
377
380
Chapitre 9. Swing et les composants d’interface utilisateur ..................................................
L’architecture Modèle-Vue-Contrôleur ...................................................................................
Une analyse Modèle-Vue-Contrôleur des boutons Swing .................................................
Introduction à la gestion de mise en forme ..............................................................................
Gestionnaire BorderLayout ................................................................................................
Panneaux ............................................................................................................................
Disposition des grilles ........................................................................................................
Entrée de texte .........................................................................................................................
Champs de texte .................................................................................................................
Etiquettes et composants d’étiquetage ...............................................................................
Suivi des modifications dans les champs de texte ..............................................................
Champs de mot de passe ....................................................................................................
Champs de saisie mis en forme ..........................................................................................
Zones de texte ....................................................................................................................
Composants du choix ...............................................................................................................
Cases à cocher ....................................................................................................................
Boutons radio .....................................................................................................................
Bordures .............................................................................................................................
385
386
390
392
394
396
397
401
402
403
405
410
410
426
431
431
434
437
Livre Java .book Page IX Jeudi, 25. novembre 2004 3:04 15
Table des matières
IX
Listes déroulantes ...............................................................................................................
Curseurs .............................................................................................................................
Le composant JSpinner ......................................................................................................
Menus ......................................................................................................................................
Création d’un menu ............................................................................................................
Icônes et options de menu ..................................................................................................
Options de menu avec cases à cocher et boutons radio .....................................................
Menus contextuels ..............................................................................................................
Caractères mnémoniques et raccourcis clavier ..................................................................
Activation et désactivation des options de menu ...............................................................
Barres d’outils ....................................................................................................................
Bulles d’aide ......................................................................................................................
Mise en forme sophistiquée .....................................................................................................
Gestionnaire BoxLayout .....................................................................................................
Gestionnaire GridBagLayout .............................................................................................
SpringLayout ......................................................................................................................
Création sans gestionnaire de mise en forme .....................................................................
Gestionnaires de mise en forme personnalisés ..................................................................
Séquence de tabulation .......................................................................................................
Boîtes de dialogue ....................................................................................................................
Boîtes de dialogue d’options ..............................................................................................
Création de boîtes de dialogue ...........................................................................................
Echange de données ...........................................................................................................
Boîtes de dialogue Fichier ..................................................................................................
Sélecteurs de couleur .........................................................................................................
442
445
451
459
460
463
464
465
467
469
473
475
478
481
486
496
506
507
511
512
513
524
528
534
547
Chapitre 10. Déployer des applets et des applications ...........................................................
Introduction aux applets ..........................................................................................................
Un petit applet ....................................................................................................................
Affichage des applets .........................................................................................................
Conversion d’une application en applet .............................................................................
Le cycle de vie d’un applet ................................................................................................
Premières règles de sécurité ...............................................................................................
Fenêtres pop-up dans un applet ..........................................................................................
Balises HTML et attributs pour applets ...................................................................................
Les attributs de positionnement d’un applet ......................................................................
Les attributs d’applet pour la partie Code ..........................................................................
Les attributs d’un applet pour les visualisateurs acceptant Java ........................................
La balise object ..................................................................................................................
Passer des informations à un applet avec des paramètres ..................................................
553
554
556
557
559
561
562
564
566
567
568
570
571
571
Livre Java .book Page X Jeudi, 25. novembre 2004 3:04 15
X
Table des matières
Le multimédia ..........................................................................................................................
Encapsuler les URL ...........................................................................................................
Récupérer des fichiers multimédias ...................................................................................
Le contexte d’applet .................................................................................................................
La communication interapplets ..........................................................................................
Faire afficher des informations par le navigateur ...............................................................
Un applet signet .................................................................................................................
C’est un applet et c’est aussi une application ! ..................................................................
Les fichiers JAR .......................................................................................................................
Packaging des applications ......................................................................................................
Le manifeste .......................................................................................................................
Fichiers JAR auto-extractibles ...........................................................................................
Les ressources ....................................................................................................................
Verrouillage ........................................................................................................................
Java Web Start ..........................................................................................................................
L’API JNLP .......................................................................................................................
Stockage des préférences d’applications .................................................................................
Concordances de propriétés ...............................................................................................
Informations système .........................................................................................................
L’API Preferences .............................................................................................................
576
576
577
578
578
579
581
583
589
591
591
592
593
597
597
600
611
611
615
617
Chapitre 11. Exceptions et mise au point .................................................................................
625
Le traitement des erreurs .........................................................................................................
Le classement des exceptions .............................................................................................
Signaler les exceptions sous contrôle .................................................................................
Comment lancer une exception ..........................................................................................
Créer des classes d’exception .............................................................................................
Capturer les exceptions ............................................................................................................
Capturer des exceptions multiples .....................................................................................
Relancer et enchaîner les exceptions ..................................................................................
La clause finally .................................................................................................................
Analyser les traces de piles ................................................................................................
Un dernier mot sur la gestion des erreurs et des exceptions de Java .................................
Quelques conseils sur l’utilisation des exceptions ...................................................................
La consignation ........................................................................................................................
Consignation de base .........................................................................................................
Consignation avancée .........................................................................................................
Modifier la configuration du gestionnaire de journaux ......................................................
La localisation ....................................................................................................................
626
627
629
631
632
633
635
635
636
639
642
646
649
650
650
652
653
Livre Java .book Page XI Jeudi, 25. novembre 2004 3:04 15
Table des matières
XI
Les gestionnaires ................................................................................................................
654
Les filtres ............................................................................................................................
658
Les formateurs ....................................................................................................................
658
Les assertions ...........................................................................................................................
666
Activation et désactivation des assertions ..........................................................................
667
Conseils d’utilisation des assertions ..................................................................................
668
Les techniques de mise au point ..............................................................................................
670
Quelques tours de main pour le débogage .........................................................................
670
Utiliser une fenêtre de console ...........................................................................................
676
Tracer les événements AWT ...............................................................................................
678
Le robot awt .......................................................................................................................
681
Utiliser un débogueur ..............................................................................................................
685
Le débogueur JDB .............................................................................................................
685
Le débogueur Eclipse .........................................................................................................
691
Chapitre 12. Les flux et les fichiers ...........................................................................................
693
Les flux ....................................................................................................................................
693
Lire et écrire des octets ......................................................................................................
694
La faune des flux ......................................................................................................................
696
Empilements de flux filtrés ................................................................................................
700
Flux de données .................................................................................................................
704
Flux de fichiers en accès direct ..........................................................................................
707
Les flux de texte .................................................................................................................
708
Jeux de caractères ..............................................................................................................
709
La sortie du texte ................................................................................................................
718
L’entrée de texte .................................................................................................................
720
Les flux de fichiers ZIP ............................................................................................................
721
L’utilisation des flux ................................................................................................................
729
Ecrire en format fixe ...........................................................................................................
729
Analyseurs lexicaux pour les textes délimités ...................................................................
730
Lecture en format fixe ........................................................................................................
731
La classe StringBuilder ......................................................................................................
735
Les flux en accès direct ......................................................................................................
736
Les flux d’objets ......................................................................................................................
742
Ecrire des objets de types variables ...................................................................................
742
La sérialisation des objets ..................................................................................................
746
Résoudre le problème de l’écriture des références d’objets ..............................................
750
Livre Java .book Page XII Jeudi, 25. novembre 2004 3:04 15
XII
Table des matières
Comprendre le format de sortie des références d’objets ....................................................
Modifier le mécanisme de sérialisation par défaut .............................................................
Sérialisation des singletons et énumérations sûres ............................................................
La gestion des versions ......................................................................................................
La sérialisation comme outil de clonage ............................................................................
La gestion des fichiers .............................................................................................................
Nouvelles E/S ..........................................................................................................................
Fichiers à concordance de mémoire ...................................................................................
La structure des données du tampon ..................................................................................
Verrouillage des fichiers .....................................................................................................
Expressions ordinaires .............................................................................................................
756
758
760
761
763
766
771
772
778
780
782
Chapitre 13. Programmation générique ...................................................................................
793
Pourquoi la programmation générique ? ..................................................................................
Y a-t-il un programmeur générique dans la salle ? ............................................................
Définition d’une classe générique simple ................................................................................
Méthodes génériques ...............................................................................................................
Limites pour variables de type .................................................................................................
Code générique et machine virtuelle .......................................................................................
Traduire les expressions génériques ...................................................................................
Traduire les méthodes génériques ......................................................................................
Appeler un code existant ....................................................................................................
Restrictions et limites ..............................................................................................................
Types primitifs ...................................................................................................................
Informations sur le type d’exécution ..................................................................................
Exceptions ..........................................................................................................................
Tableaux .............................................................................................................................
Instanciation de types génériques ......................................................................................
Contextes statiques .............................................................................................................
Conflits après un effacement ..............................................................................................
Règles d’héritage pour les types génériques ............................................................................
Types joker ...............................................................................................................................
Limites de supertypes pour les jokers ................................................................................
Jokers sans limites ..............................................................................................................
Capture de caractères joker ................................................................................................
Réflexion et générique .............................................................................................................
Utilisation des paramètres Class<T> pour la concordance de type ..................................
Informations de type générique dans la machine virtuelle ................................................
794
795
796
797
798
800
802
802
804
805
805
806
806
807
807
808
808
809
810
812
814
815
818
819
819
Livre Java .book Page XIII Jeudi, 25. novembre 2004 3:04 15
Table des matières
XIII
Annexe A. Les mots clés de Java ...............................................................................................
825
Annexe B. Adaptation en amont du code du JDK 5.0 ............................................................
Amélioration de la boucle for ..................................................................................................
Listes de tableaux génériques ..................................................................................................
Autoboxing ..............................................................................................................................
Listes de paramètres variables .................................................................................................
Types de retour covariants .......................................................................................................
Importation statique .................................................................................................................
Saisie à la console ....................................................................................................................
Sortie mise en forme ................................................................................................................
Délégation du volet conteneur .................................................................................................
Points de code Unicode ...........................................................................................................
Construction des chaînes .........................................................................................................
829
829
830
830
830
831
831
831
832
832
832
833
Index .............................................................................................................................................
835
Livre Java .book Page XIV Jeudi, 25. novembre 2004 3:04 15
Livre Java .book Page 1 Jeudi, 25. novembre 2004 3:04 15
Introduction
Avertissement au lecteur
Vers la fin de 1995, le langage de programmation Java surgit sur la grande scène d’Internet et obtint
immédiatement un énorme succès. La prétention de Java est de constituer la colle universelle capable de connecter les utilisateurs aux informations, que celles-ci proviennent de serveurs Web, de
bases de données, de fournisseurs d’informations ou de toute autre source imaginable. Et Java se
trouve en bonne position pour relever ce défi. Il s’agit d’un langage de conception très robuste qui a
été adopté par la majorité des principaux fournisseurs, à l’exception de Microsoft. Ses caractéristiques intégrées de sécurité offrent un sentiment de confiance aux programmeurs comme aux utilisateurs des applications. De plus, Java intègre des fonctionnalités qui facilitent grandement certaines
tâches de programmation avancées, comme la gestion réseaux, la connectivité bases de données ou
le développement d’applications multitâches.
Depuis le lancement de Java, Sun Microsystems a émis six révisions majeures du kit de développement Java. Au cours des neuf dernières années, l’API (interface de programmation d’application)
est passée de 2 000 à plus de 3 000 classes. Elle traite maintenant des domaines aussi divers que
la construction de l’interface utilisateur, la gestion des bases de données, l’internationalisation, la
sécurité et le traitement du code XML. Le JDK 5.0, sorti en 2004, constitue la mise à jour la plus importante du langage Java depuis sa première sortie.
L’ouvrage que vous tenez entre les mains est le premier volume de la septième édition de Au cœur de
Java 2. Chaque édition a suivi la sortie du kit de développement d’aussi près que possible et, à
chaque fois, nous avons réécrit le livre pour y inclure les toutes dernières fonctionnalités. Dans cette
édition, nous nous passionnons pour les collections génériques, l’amélioration de la boucle for et
d’autres caractéristiques passionnantes du JDK 5.0.
Ce livre, comme les éditions précédentes, s’adresse essentiellement aux programmeurs professionnels désireux d’utiliser Java pour développer de véritables projets. Nous considérons que le lecteur
possède déjà une solide habitude de la programmation, mais il n’a pas nécessairement besoin de
connaître le langage C++ ou la programmation orientée objet. Les programmeurs expérimentés qui
utilisent Visual Basic, C ou COBOL n’éprouveront pas de difficultés à comprendre le contenu de cet
ouvrage (il n’est pas même nécessaire que vous ayez une expérience de la programmation des interfaces graphiques sous Windows, UNIX ou Macintosh).
Nous supposons donc, a priori, que :
m
Vous souhaitez écrire de vrais programmes permettant de résoudre de vrais problèmes.
m
Vous n’aimez pas les livres qui fourmillent d’exemples dépourvus d’utilité pratique.
Vous trouverez de nombreux programmes de démonstration qui abordent la plupart des sujets traités
dans cet ouvrage. Ces programmes sont volontairement simples afin que le lecteur puisse se concentrer
Livre Java .book Page 2 Jeudi, 25. novembre 2004 3:04 15
2
Au cœur de Java 2 - Notions fondamentales
sur les points importants. Néanmoins, dans la plupart des cas, il s’agit d’applications utiles qui pourront
vous servir de base pour le développement de vos propres projets.
Nous supposons également que vous souhaitez apprendre les caractéristiques avancées de Java ;
c’est pourquoi nous étudierons en détail :
m
la programmation orientée objet ;
m
le mécanisme de réflexion de Java et de proxy ;
m
les interfaces et classes internes ;
m
le modèle d’écouteur d’événement ;
m
la conception d’interfaces graphiques avec la boîte à outils Swing ;
m
la gestion des exceptions ;
m
les flux d’E/S et la sérialisation d’objet ;
m
la programmation générique.
Enfin, compte tenu de l’explosion de la bibliothèque de classes de Java, nous avons dû répartir
l’étude de toutes les fonctionnalités sur deux volumes. Le premier, que vous avez en mains, se
concentre sur les concepts fondamentaux du langage Java, ainsi que sur les bases de la programmation d’une interface graphique. Le second volume traite plus exhaustivement des fonctionnalités
d’entreprise et de la programmation avancée des interfaces utilisateur. Il aborde les sujets suivants :
m
les multithreads ;
m
la programmation réseaux ;
m
les objets distribués ;
m
les classes de collections ;
m
les bases de données ;
m
les concepts graphiques avancés ;
m
les composants GUI avancés ;
m
l’internationalisation ;
m
les méthodes natives ;
m
JavaBeans ;
m
le traitement XML.
Lors de la rédaction d’un ouvrage comme celui-ci, il est inévitable de commettre des erreurs et des
inexactitudes. Nous avons donc préparé sur le site Web http://www.horstmann.com/corejava.html
une liste de questions courantes, de corrections et d’explications. Placé stratégiquement à la fin de
page des corrections (pour vous encourager à la lire), vous trouverez un formulaire permettant de
signaler des bogues et de suggérer des améliorations. Ne soyez pas déçus si nous ne répondons pas à
chaque requête ou si nous ne vous écrivons pas rapidement. Nous lisons tous les e-mails et apprécions
vos commentaires, qui nous permettent d’améliorer les futures versions de cet ouvrage.
Nous espérons que vous prendrez plaisir à lire ce livre et qu’il vous aidera dans votre programmation.
Livre Java .book Page 3 Jeudi, 25. novembre 2004 3:04 15
Introduction
3
A propos de ce livre
Le Chapitre 1 présentera les caractéristiques de Java qui le distinguent des autres langages de
programmation. Nous expliquerons les intentions des concepteurs du langage et nous montrerons
dans quelle mesure ils sont parvenus à leurs fins. Nous terminerons par un historique de Java et nous
préciserons la manière dont il a évolué.
Le Chapitre 2 vous indiquera comment télécharger et installer le JDK et les exemples de programme
du livre. Nous vous guiderons ensuite dans la compilation et l’exécution de trois programmes Java
typiques : une application console, une application graphique et un applet, grâce à du JDK brut, un
éditeur de texte activé pour Java et un IDE Java.
Nous entamerons dans le Chapitre 3 une étude approfondie du langage, en commençant par les
éléments de base : les variables, les boucles et les fonctions simples. Si vous êtes un programmeur C
ou C++, tout cela ne vous posera pas de problème, car la syntaxe employée est comparable à celle de
C. Si vous avez une autre formation, par exemple en Visual Basic, nous vous conseillons de lire
attentivement ce chapitre.
Le programmation orientée objet (POO) est maintenant au cœur des méthodes modernes de
programmation, et Java est un langage orienté objet. Le Chapitre 4 présentera l’encapsulation — la
première des deux notions fondamentales de l’orientation objet — et le mécanisme du langage Java
qui permet de l’implémenter, c’est-à-dire les classes et les méthodes. En plus des règles de Java, nous
vous proposerons des conseils pour une bonne conception orientée objet. Nous aborderons ensuite le
merveilleux outil javadoc qui permet de transformer les commentaires de votre code en une
documentation au format HTML. Si vous êtes un habitué du C++, vous pourrez vous contenter de
parcourir rapidement ce chapitre. Les programmeurs qui ne sont pas familiarisés avec la programmation orientée objet doivent se donner le temps d’étudier ces concepts avant de poursuivre leur
exploration de Java.
Les classes et l’encapsulation ne constituent qu’une partie du concept de POO, et le Chapitre 5 introduira l’autre élément essentiel : l’héritage. Celui-ci permet de récupérer une classe existante et de la
modifier selon vos besoins. Il s’agit là d’une technique fondamentale de la programmation Java. Le
mécanisme d’héritage de Java est comparable à celui de C++. Ici encore, les programmeurs C++
pourront se concentrer uniquement sur les différences entre les deux langages.
Le Chapitre 6 vous montrera comment utiliser la notion d’interface. Les interfaces permettent de
dépasser le modèle d’héritage simple vu au Chapitre 5. En maîtrisant les interfaces, vous pourrez
profiter pleinement de l’approche orientée objet de la programmation Java. Nous traiterons
également dans ce chapitre une caractéristique technique très utile de Java, appelée classe interne.
Les classes internes permettent d’obtenir des programmes plus propres et plus concis.
Dans le Chapitre 7, nous commencerons véritablement la programmation d’applications. Nous vous
montrerons comment créer des fenêtres, y dessiner et y tracer des figures géométriques, formater du
texte avec différentes fontes et afficher des images.
Le Chapitre 8 sera consacré à une étude détaillée du modèle d’événement AWT. Nous verrons
comment écrire le code permettant de répondre à des événements tels que des clics de la souris ou
des frappes de touches. Vous verrez par la même occasion comment gérer des éléments de l’interface
utilisateur graphique comme les boutons ou les panneaux.
Livre Java .book Page 4 Jeudi, 25. novembre 2004 3:04 15
4
Au cœur de Java 2 - Notions fondamentales
Le Chapitre 9 examinera de manière approfondie l’outil Swing, qui permet de créer des interfaces
graphiques multi-plates-formes. Vous apprendrez tout ce qu’il faut savoir sur les différents types de
boutons, les composants de saisie, les bordures, les barres de défilement, les zones de listes, les
menus et les boîtes de dialogue. Certains de ces composants, plus avancés, seront étudiés au
Volume 2.
Lorsque vous en aurez terminé avec le Chapitre 9, vous connaîtrez tous les mécanismes permettant
d’écrire des applets, ces mini-programmes qui peuvent s’exécuter dans une page Web. Nous leur
consacrerons le Chapitre 10. Nous vous proposerons un certain nombre d’applets utiles et amusants,
mais nous chercherons surtout à vous les présenter comme une méthode de déploiement de programmes. Nous vous indiquerons comment packager des applications dans des fichiers JAR et délivrer
des applications sur Internet avec le mécanisme Java Web Start. Enfin, nous vous expliquerons
comment les programmes Java peuvent stocker et récupérer des informations de configuration
lorsqu’elles ont été déployées.
Le Chapitre 11 traitera de la gestion des exceptions, un mécanisme robuste qui s’appuie sur le fait
que même les bons programmes peuvent subir des avanies. Par exemple, une connexion réseau peut
devenir indisponible au beau milieu d’un téléchargement, ou un disque peut être saturé, etc. Les
exceptions fournissent un moyen efficace de séparer le code normal de traitement et la gestion
d’erreurs. Bien entendu, même si vous avez sécurisé votre programme en gérant toutes les conditions exceptionnelles, il n’est pas certain qu’il fonctionne parfaitement. La deuxième partie de ce
chapitre vous donnera quelques astuces de débogage. Pour terminer, nous vous accompagnerons
dans une session de débogage avec différents outils : le débogueur JDB, le débogueur d’un environnement de développement intégré, un analyseur de performance (profiler), un outil de test de couverture du code et le robot AWT.
Le Chapitre 12 traitera de la gestion des entrées/sorties. En Java, toutes les E/S sont gérées par ce
qu’on appelle des flux. Ceux-ci vous permettent de travailler de manière uniforme avec toutes les
sources de données, qu’il s’agisse des fichiers, des connexions réseau ou des blocs de mémoire.
Nous étudierons en détail les classes de lecture et d’écriture, qui facilitent le traitement d’Unicode ;
nous vous montrerons également ce qui se passe sous le capot lorsque vous employez le mécanisme
de sérialisation, qui facilite et accélère la sauvegarde et le chargement des objets. Enfin, nous aborderons plusieurs bibliothèques qui ont été ajoutées au JDK 1.4 : les classes "new I/O" (nouvelles E/S)
qui assurent la prise en charge des opérations de fichier avancées et plus efficaces, et la bibliothèque
d’expressions ordinaires.
Nous terminerons cet ouvrage par une vue d’ensemble de la programmation générique, une avancée
majeure du JDK 5.0. Elle facilite la lecture de vos programmes, tout en les sécurisant. Nous vous
montrerons comment utiliser des types forts avec les collections et comment supprimer des transtypages peu sûrs.
L’Annexe A est consacrée aux mots clés du langage Java.
L’Annexe B vous montre comment modifier les exemples de code, de sorte qu’ils se compilent sur
une version plus ancienne du compilateur (JDK 1.4).
Livre Java .book Page 5 Jeudi, 25. novembre 2004 3:04 15
Introduction
5
Conventions
Comme c’est l’usage dans la plupart des ouvrages d’informatique, nous employons la police courrier
pour le code des programmes.
INFO C++
De nombreuses notes d’info C++ vous précisent les différences entre Java et C++. Vous pouvez ignorer ces notes si
vous n’êtes pas familiarisé avec ce langage.
INFO
Ces informations sont signalées par une icône de bloc-notes qui ressemble à ceci.
ASTUCE
Les infos et les astuces sont repérées par une de ces deux icônes.
ATTENTION
Une icône "Attention" vous prévient s’il y a du danger à l’horizon.
API Java
Java est accompagné d’une importante bibliothèque de programmation (API, Application
Programming Interface).
Lorsque nous utilisons pour la première fois un appel à l’API, nous proposons également une
brève description dans une note "API" située à la fin de la section. Ces descriptions sont un peu
plus informelles que celles de la documentation officielle en ligne, mais nous espérons qu’elles
sont plus instructives.
Les programmes dont le code source se trouve sur le Web sont fournis sous forme d’exemples,
comme ceci :
Exemple 2.4 : WelcomeApplet.java
... Le code ici.
Exemples de code
Le site Web de cet ouvrage http://www.phptr.com/corejava contient tous les exemples du livre,
sous forme compressée. Ils peuvent être décompressés avec un des outils courants du marché, ou
avec l’utilitaire jar du kit JDK.
Reportez-vous au Chapitre 2 pour en savoir plus sur l’installation du JDK et des codes d’exemple.
Livre Java .book Page 6 Jeudi, 25. novembre 2004 3:04 15
Livre Java .book Page 7 Jeudi, 25. novembre 2004 3:04 15
1
Une introduction à Java
Au sommaire de ce chapitre
✔ Java, plate-forme de programmation
✔ Les termes clés du livre blanc de Java
✔ Java et Internet
✔ Bref historique de Java
✔ Les idées fausses les plus répandues concernant Java
La première version de Java, sortie en 1996, a fait naître beaucoup de passions, pas seulement au
niveau de la presse informatique, mais également dans la presse plus généraliste comme The New
York Times, The Washington Post et Business Week. Java présente l’avantage d’être le premier et le
seul langage de programmation dont l’histoire est contée à la radio. Il est plutôt amusant de revisiter
cette époque pionnière, nous vous présenterons donc un bref historique de Java dans ce chapitre.
Java, plate-forme de programmation
Dans la première édition de cet ouvrage, nous avons dû écrire ceci :
"La réputation de Java en tant que langage informatique est exagérée : Java est assurément un bon
langage de programmation. Il s’agit, sans aucun doute, de l’un des meilleurs disponibles pour un
programmeur sérieux. Java aurait, potentiellement, pu être un grand langage de programmation,
mais il est probablement trop tard pour cela. Lorsqu’un langage commence à être exploité, se pose le
problème de la compatibilité avec le code existant."
Un haut responsable de Sun, que nous ne nommerons pas, a adressé à notre éditeur de nombreux
commentaires sur ce paragraphe. Mais, avec le temps, notre pronostic semble s’avérer. Java présente
de très bonnes fonctionnalités (nous les verrons en détail plus loin dans ce chapitre). Il a pourtant sa
part d’inconvénients, et les derniers ajouts ne sont pas aussi agréables que les premiers, et ce pour
des raisons de compatibilité.
Toutefois, comme nous le disions dans la première édition, Java n’a jamais été qu’un langage. Même
s’il en existe à foison, peu font beaucoup d’éclats. Java est une plate-forme complète, disposant
Livre Java .book Page 8 Jeudi, 25. novembre 2004 3:04 15
8
Au cœur de Java 2 - Notions fondamentales
d’une importante bibliothèque, d’une grande quantité de code réutilisable et d’un environnement
d’exécution qui propose des services tels que la sécurité, la portabilité sur les systèmes d’exploitation
et le ramasse-miettes automatique.
En tant que programmeur, vous voulez un langage à la syntaxe agréable et à la sémantique compréhensible (donc, pas de C++). Java répond à ces critères, comme des dizaines d’autres langages.
Certains vous proposent la portabilité, le ramasse-miettes et des outils du même genre, mais ils ne
disposent pas de vraie bibliothèque, ce qui vous oblige à déployer la vôtre si vous souhaitez utiliser
de beaux graphiques, le réseau ou l’accès aux bases de données. Java regroupe tout cela : un langage de
qualité, un environnement d’exécution idoine et une grande bibliothèque. C’est cette combinaison
qui fait de Java une proposition à laquelle de nombreux programmeurs ne résistent pas.
Les termes clés du livre blanc de Java
Les auteurs de Java ont écrit un important livre blanc qui présente les objectifs et les réalisations de
leur conception. Ce livre s’articule autour des onze termes clés suivants :
Simplicité
Portabilité
Orienté objet
Interprété
Distribué
Performances élevées
Fiabilité
Multithread
Sécurité
Dynamique
Architecture neutre
Dans la suite de ce chapitre, nous allons :
m
résumer par l’intermédiaire d’extraits du livre blanc ce que les concepteurs de Java ont voulu
traduire avec chacun de ces termes clés ;
m
exprimer ce que nous pensons de chaque terme, à partir de notre expérience de la version
actuelle de Java.
INFO
A l’heure où nous écrivons ces lignes, le livre blanc est disponible à l’adresse suivante : http://java.sun.com/docs/
white/langenv/. Le résumé des onze mots clés figure à l’adresse ftp://ftp.javasoft.com/docs/papers/java-overview.ps.
Simplicité
*
Nous avons voulu créer un système qui puisse être programmé simplement, sans nécessiter un apprentissage ésotérique, et qui tire parti de l’expérience standard actuelle. En conséquence, même si nous
pensions que C++ ne convenait pas, Java a été conçu de façon relativement proche de ce langage dans
le dessein de faciliter la compréhension du système. De nombreuses fonctions compliquées, mal comprises,
rarement utilisées de C++, qui nous semblaient par expérience apporter plus d’inconvénients que
d’avantages, ont été supprimées de Java.
Livre Java .book Page 9 Jeudi, 25. novembre 2004 3:04 15
Chapitre 1
Une introduction à Java
9
La syntaxe de Java représente réellement une version améliorée de C++. Les fichiers d’en-tête,
l’arithmétique des pointeurs (ou même une syntaxe de pointeur), les structures, les unions, la
surcharge d’opérateur, les classes de base virtuelles, etc. ne sont plus nécessaires (tout au long de cet
ouvrage, nous avons inclus des notes Info décrivant plus précisément les différences entre Java et
C++). Les concepteurs n’ont cependant pas tenté de modifier certaines fonctions pièges de C++
telles que l’instruction switch. Si vous connaissez C++, le passage à la syntaxe de Java vous
semblera facile.
Si vous êtes habitué à un environnement de programmation visuel (tel que Visual Basic), le langage
Java vous paraîtra plus complexe. Une partie de la syntaxe vous semblera étrange (même si sa
maîtrise est rapide). Plus important encore, la programmation en Java nécessitera davantage de
travail. L’intérêt de Visual Basic réside dans le fait que son environnement visuel de conception fournit de façon presque automatique une grande partie de l’infrastructure d’une application. En Java, la
fonctionnalité équivalente doit être programmée à l’aide des Java Swing, généralement à l’aide
d’une quantité respectable de code. Il existe cependant des environnements de développement tiers
qui permettent l’écriture de programmes à l’aide d’opérations "glisser-déplacer".
*
Un autre avantage de sa simplicité est sa petite taille. L’un des buts de Java est de permettre à des logiciels de s’exécuter intégralement sur de modestes machines. Ainsi, la taille cumulée de l’interpréteur de
base et du support des classes est d’environ 40 Ko ; pour supporter les classes standard ainsi que la
gestion multitraitement (threads), il faut ajouter 175 Ko.
C’est un exploit. Toutefois, sachez que la taille des bibliothèques de l’interface utilisateur graphique
(GUI) est notablement plus importante.
Orienté objet
*
Pour rester simples, disons que la conception orientée objet est une technique de programmation qui se
concentre sur les données (les objets) et sur les interfaces avec ces objets. Pour faire une analogie avec
la menuiserie, on pourrait dire qu’un menuisier "orienté objet" s’intéresse essentiellement à la chaise
(l’objet) qu’il fabrique et non à sa conception (le "comment"). Par opposition, le menuisier "non orienté
objet" penserait d’abord au "comment"...
Au cours des trente dernières années, la programmation orientée objet a prouvé ses avantages, et il
est inconcevable qu’un langage de programmation moderne n’en tire pas parti. En fait, les fonctionnalités orientées objet de Java sont comparables à celles de C++. Les différences majeures résident
dans l’héritage multiple (que Java a remplacé par le concept plus simple des interfaces) et le modèle
objet de Java (métaclasses). Le mécanisme de réflexion (voir Chapitre 5) et la fonctionnalité de
sérialisation des objets (voir Chapitre 12) facilitent énormément la mise en œuvre des objets persistants
et des générateurs GUI capables d’intégrer des composants réutilisables.
INFO
Si vous n’avez aucune expérience des langages de programmation orientée objet, prenez soin de lire attentivement
les Chapitres 4 à 6. Ils vous expliquent ce qu’est la programmation orientée objet et pour quelles raisons elle est plus
utile à la programmation de projets sophistiqués que ne le sont les langages procéduraux comme C ou Basic.
Livre Java .book Page 10 Jeudi, 25. novembre 2004 3:04 15
10
Au cœur de Java 2 - Notions fondamentales
Distribué
*
Java possède une importante bibliothèque de routines permettant de gérer les protocoles TCP/IP tels
que HTTP et FTP. Les applications Java peuvent charger, et accéder à, des objets sur Internet via des
URL avec la même facilité qu’elles accèdent à un fichier local sur le système.
Nous avons trouvé que les fonctionnalités réseau de Java étaient à la fois fiables et d’utilisation aisée.
Toute personne ayant essayé de faire de la programmation pour Internet avec un autre langage se
réjouira de la simplicité de Java lorsqu’il s’agit de mettre en œuvre des tâches lourdes, comme
l’ouverture d’une connexion avec une socket (nous verrons les réseaux dans le Volume 2 de cet
ouvrage). Le mécanisme d’invocation de méthode à distance (RMI) autorise la communication entre
objets distribués (voir également le Volume 2).
Il existe maintenant une architecture séparée, le Java 2 Entreprise Edition (J2EE), qui prend en
charge des applications distribuées à très large échelle.
Fiabilité
*
Java a été conçu pour que les programmes qui l’utilisent soient fiables sous différents aspects. Sa
conception encourage le programmeur à traquer préventivement les éventuels problèmes, à lancer des
vérifications dynamiques en cours d’exécution et à éliminer les situations génératrices d’erreurs... La
seule et unique grosse différence entre C++ et Java réside dans le fait que ce dernier intègre un modèle
de pointeur qui écarte les risques d’écrasement de la mémoire et d’endommagement des données.
Voilà encore une caractéristique fort utile. Le compilateur Java détecte de nombreux problèmes qui,
dans d’autres langages, ne sont visibles qu’au moment de l’exécution. Tous les programmeurs qui
ont passé des heures à récupérer une mémoire corrompue par un bogue de pointeur seront très
heureux d’exploiter cette caractéristique de Java.
Si vous connaissez déjà des langages comme Visual Basic, qui n’exploite pas les pointeurs de façon
explicite, vous vous demandez sûrement pourquoi ce problème est si important. Les programmeurs
en C n’ont pas cette chance. Ils ont besoin des pointeurs pour accéder à des chaînes, à des tableaux,
à des objets, et même à des fichiers. Dans Visual Basic, vous n’employez de pointeur pour aucune de
ces entités, pas plus que vous n’avez besoin de vous soucier de leurs allocations en mémoire. En
revanche, certaines structures de données sont difficiles à implémenter sans l’aide de pointeurs. Java
vous donne le meilleur des deux mondes. Vous n’avez pas besoin des pointeurs pour les constructions habituelles, comme les chaînes et les tableaux. Vous disposez de la puissance des pointeurs (via
des références) en cas de nécessité, par exemple, pour construire des listes chaînées. Et cela en toute
sécurité dans la mesure où vous ne pouvez jamais accéder à un mauvais pointeur ni faire des erreurs
d’allocation de mémoire, pas plus qu’il n’est nécessaire de vous protéger des "fuites" de mémoire.
Sécurité
*
Java a été conçu pour être exploité dans des environnements serveur et distribués. Dans ce but, la sécurité
n’a pas été négligée. Java permet la construction de systèmes inaltérables et sans virus.
Dans la première édition, nous déclarions : "Il ne faut jamais dire fontaine je ne boirai plus de ton
eau." Et nous avions raison. Peu après la publication de la première version du JDK, un groupe
d’experts de la sécurité de l’université de Princeton a localisé les premiers bogues des caractéristiques
Livre Java .book Page 11 Jeudi, 25. novembre 2004 3:04 15
Chapitre 1
Une introduction à Java
11
de sécurité de Java 1.0. Sun Microsystems a encouragé la recherche sur la sécurité de Java, en
rendant publiques les caractéristiques et l’implémentation de la machine virtuelle et des bibliothèques
de sécurité. Il a réparé rapidement tous les bogues connus. Quoi qu’il en soit, Java se charge de
rendre particulièrement difficile le contournement des mécanismes de sécurité. Jusqu’à présent, les
bogues qui ont été localisés étaient très subtils et (relativement) peu nombreux.
Dès le départ, Java a été conçu pour rendre impossibles certains types d’attaques et, parmi eux :
m
la surcharge de la pile d’exécution, une attaque commune des vers et des virus ;
m
l’endommagement de la mémoire située à l’extérieur de son propre espace de traitement ;
m
la lecture ou l’écriture des fichiers sans autorisation.
Avec le temps, un certain nombre de fonctionnalités relatives à la sécurité ont été ajoutées à Java.
Depuis la version 1.1, Java intègre la notion de classe signée numériquement (voir Au cœur de
Java 2 Volume 2, éditions CampusPress), qui vous permet de savoir qui l’a écrite. Votre degré
de confiance envers son auteur va déterminer l’ampleur des privilèges que vous allez accorder à la
classe sur votre machine.
INFO
Le mécanisme concurrent de mise à disposition de code, élaboré par Microsoft et fondé sur sa technologie ActiveX,
emploie exclusivement les signatures numériques pour assurer la sécurité. Bien évidemment, cela n’est pas suffisant.
Tous les utilisateurs des produits Microsoft peuvent confirmer que les programmes proposés par des concepteurs
bien connus rencontrent des défaillances qui provoquent des dégâts. Le modèle de sécurité de Java est nettement
plus puissant que celui d’ActiveX dans la mesure où il contrôle l’application en cours d’exécution et l’empêche de
faire des ravages.
Architecture neutre
*
Le compilateur génère un format de fichier objet dont l’architecture est neutre — le code compilé
est exécutable sur de nombreux processeurs, à partir du moment où le système d’exécution de Java est
présent. Pour ce faire, le compilateur Java génère des instructions en bytecode (ou pseudo-code) qui
n’ont de lien avec aucune architecture d’ordinateur particulière. Au contraire, ces instructions ont été
conçues pour être à la fois faciles à interpréter, quelle que soit la machine, et faciles à traduire à la volée
en code machine natif.
L’idée n’est pas nouvelle. Il y a plus de vingt ans, la mise en œuvre originale de Pascal par Niklaus
Wirth et le système Pascal UCSD utilisaient tous deux la même approche.
Bien entendu, l’interprétation des bytecodes est forcément plus lente que l’exécution des instructions machine à pleine vitesse. On peut donc douter de la pertinence de cette idée. Toutefois, les
machines virtuelles ont le choix de traduire les séquences de bytecode fréquemment utilisées en
code machine, une procédure appelée compilation en "juste-à-temps" (just-in-time ou JIT). Cette
stratégie s’est révélée tellement efficace que la plate-forme .NET de Microsoft va jusqu’à reposer sur
une machine virtuelle.
La machine virtuelle présente d’autres avantages. Elle accroît la sécurité car elle est en mesure de vérifier le comportement des suites d’instructions. Certains programmes vont jusqu’à produire des bytecodes
à la volée, en améliorant dynamiquement les capacités d’un programme en cours d’exécution.
Livre Java .book Page 12 Jeudi, 25. novembre 2004 3:04 15
12
Au cœur de Java 2 - Notions fondamentales
Portabilité
*
A la différence de C et de C++, on ne trouve pas les aspects de dépendance de la mise en œuvre dans la
spécification. Les tailles des types de données primaires sont spécifiées, ainsi que le comportement arithmétique qui leur est applicable.
Par exemple, en Java, un int est toujours un entier en 32 bits. Dans C/C++, int peut représenter un
entier 16 bits, un entier 32 bits, ou toute autre taille décidée par le concepteur du compilateur. La
seule restriction est que le type int doit contenir au moins autant d’octets qu’un short int et ne
peut pas en contenir plus qu’un long int. Le principe d’une taille fixe pour les types numériques
élimine les principaux soucis du portage d’applications. Les données binaires sont stockées et
transmises dans un format fixe, ce qui met fin à la confusion sur l’ordre des octets. Les chaînes sont
enregistrées au format Unicode standard.
*
Les bibliothèques intégrées au système définissent des interfaces portables. Par exemple, il existe une
classe Window abstraite, accompagnée de ses mises en œuvre pour UNIX, Windows et Macintosh.
Tous ceux qui ont essayé savent que l’élaboration d’un programme compatible avec Windows,
Macintosh et dix versions d’UNIX représente un effort héroïque. Java 1.0 a accompli cet effort en
proposant un kit d’outils simples sachant "coller" aux principaux composants d’interface utilisateur
d’un grand nombre de plates-formes. Malheureusement, le résultat était une bibliothèque qui donnait,
avec beaucoup de travail, des programmes à peine acceptables sur différents systèmes. En outre, on
rencontrait souvent des bogues différents sur les différentes implémentations graphiques. Mais ce
n’était qu’un début. Il existe de nombreuses applications pour lesquelles la portabilité est plus importante que l’efficacité de l’interface utilisateur, et ce sont celles qui ont bénéficié des premières
versions de Java. Aujourd’hui, le kit d’outils de l’interface utilisateur a été entièrement réécrit de
manière à ne plus dépendre de l’interface utilisateur de l’hôte. Le résultat est nettement plus cohérent,
et nous pensons qu’il est beaucoup plus intéressant que dans les précédentes versions de Java.
Interprété
*
L’interpréteur de Java peut exécuter les bytecodes directement sur n’importe quelle machine sur
laquelle il a été porté. Dans la mesure où la liaison est un processus plus incrémentiel et léger, le processus
de développement peut se révéler plus rapide et exploratoire.
La liaison incrémentielle présente des avantages, mais ils ont été largement exagérés. Quoi qu’il en
soit, nous avons trouvé les outils de développement relativement lents. Si vous êtes habitué à la
vitesse de l’environnement classique de Microsoft Visual Basic C++, vous serez certainement déçu
par les performances des environnements de développement Java (toutefois, la version actuelle de
Visual Studio n’est pas aussi dynamique que les environnements classiques. Quel que soit le langage
de programmation que vous utilisez, demandez à votre supérieur de vous procurer un ordinateur plus
rapide pour lancer les derniers environnements de développement).
Performances élevées
*
En règle générale, les performances des bytecodes interprétés sont tout à fait suffisantes ; il existe toutefois des situations dans lesquelles des performances plus élevées sont nécessaires. Les bytecodes
Livre Java .book Page 13 Jeudi, 25. novembre 2004 3:04 15
Chapitre 1
Une introduction à Java
13
peuvent être traduits à la volée (en cours d’exécution) en code machine pour l’unité centrale destinée à
accueillir l’application.
Si vous employez un interpréteur pour exécuter les bytecodes, "performances élevées" n’est pas
précisément la formule appropriée. Il existe toutefois sur de nombreuses plates-formes une autre
forme de compilation proposée par les compilateurs JIT (just-in-time, juste-à-temps). Ceux-ci
compilent les bytecodes en code natif, placent les résultats dans le cache, puis les appellent en cas de
besoin. Cette approche augmente considérablement la vitesse d’exécution du code couramment
utilisé, puisque l’interprétation n’est réalisée qu’une seule fois. Les compilateurs JIT n’en restent pas
moins légèrement plus lents que les vrais compilateurs de code natif ; ils accélèrent toutefois de dix
à vingt fois certains programmes et sont presque toujours beaucoup plus rapides qu’un interpréteur.
Cette technologie subit des améliorations constantes et finira peut-être par donner des résultats sans
commune mesure avec les systèmes classiques de compilation. Par exemple, un compilateur JIT est
capable d’identifier du code souvent exécuté et d’optimiser exclusivement la vitesse de celui-ci.
Multithread
*
Les avantages du multithread sont une meilleure interréactivité et un meilleur comportement en temps réel.
Si vous avez déjà essayé de programmer le multithread dans un autre langage, vous allez être agréablement surpris de la simplicité de cette tâche dans Java. Avec lui, les threads sont également capables de tirer parti des systèmes multiprocesseurs. Le côté négatif réside dans le fait que les
implémentations de threads sur les plates-formes principales sont très différentes, et Java ne fait
aucun effort pour être indépendant de la plate-forme à cet égard. La seule partie de code identique
entre les différentes machines est celle de l’appel du multithread. Java se décharge de l’implémentation du multithread sur le système d’exploitation sous-jacent ou sur une bibliothèque de threads (la
notion de thread est étudiée au Volume 2). Malgré tout, la simplicité du multithread est l’une des
raisons principales du succès de Java pour le développement côté serveur.
Java, langage dynamique
*
Sur plusieurs points, Java est un langage plus dynamique que C ou C++. Il a été conçu pour s’adapter à
un environnement en évolution constante. Les bibliothèques peuvent ajouter librement de nouvelles
méthodes et variables sans pour autant affecter leurs clients. La recherche des informations de type
exécution dans Java est simple.
Cette fonction est importante lorsque l’on doit ajouter du code à un programme en cours d’exécution.
Le premier exemple est celui du code téléchargé depuis Internet pour s’exécuter dans un navigateur.
Avec la version 1.0 de Java, la recherche d’information de type exécution était relativement
complexe. A l’heure actuelle, les versions courantes de Java procurent au programmeur un aperçu
complet de la structure et du comportement de ses objets. Cela se révèle très utile pour les systèmes
nécessitant une analyse des objets au cours de l’exécution, tels que les générateurs graphiques de
Java, les débogueurs évolués, les composants enfichables et les bases de données objet.
INFO
Microsoft a sorti un produit intitulé J++ qui présente un lien de parenté avec Java. Comme Java, J++ est exécuté par
une machine virtuelle compatible avec la machine virtuelle Java pour l’exécution des bytecodes Java ; or il existe des
Livre Java .book Page 14 Jeudi, 25. novembre 2004 3:04 15
14
Au cœur de Java 2 - Notions fondamentales
différences considérables lors de l’interfaçage avec un code externe. La syntaxe du langage de base est presque identique à celle de Java. Toutefois, Microsoft a ajouté des constructions de langage d’une utilité douteuse, sauf pour
l’interfaçage avec l’API Windows. Outre le fait que Java et J++ partagent une syntaxe commune, leurs bibliothèques
de base (chaînes, utilitaires, réseau, multithread, arithmétique, etc.) sont, pour l’essentiel, identiques. Toutefois, les
bibliothèques de graphiques, les interfaces utilisateur et l’accès aux objets distants sont totalement différents. Pour
l’heure, Microsoft ne prend plus en charge J++ mais a introduit un nouveau langage, appelé C#, qui présente aussi
de nombreuses similarités avec Java mais utilise une autre machine virtuelle. Il existe même un J# pour faire migrer
les applications J++ vers la machine virtuelle utilisée par C#. Sachez que nous ne reviendrons pas sur J++, C# ou J#
dans cet ouvrage.
Java et Internet
L’idée de base est simple : les utilisateurs téléchargent les bytecodes Java depuis Internet et les
exécutent sur leurs propres machines. Les programmes Java s’exécutant sur les pages Web sont
nommés applets. Pour utiliser un applet, vous devez disposer d’un navigateur Web compatible Java,
qui exécutera les bytecodes. Sun fournit sous licence le code source de Java et affirme qu’aucun
changement n’affectera le langage et la structure de la bibliothèque de base : vous pouvez donc être
assuré qu’un applet Java s’exécutera avec tout navigateur compatible Java. Malheureusement, la
réalité est différente. Plusieurs versions d’Internet Explorer et de Netscape exécutent différentes
versions de Java, certaines particulièrement obsolètes. Cette situation complique considérablement
le développement d’applets tirant parti de la version la plus récente de Java. Pour remédier à ce
problème, Sun a développé le Java Plug-in. Cet outil met à la disposition de Netscape et d’Internet
Explorer l’environnement d’exécution de Java le plus récent (voir Chapitre 10).
Lorsque l’utilisateur télécharge un applet, il s’agit presque de l’intégration d’une image dans une
page Web. L’applet s’insère dans la page et le texte se répartit dans son espace. Le fait est que
l’image est vivante. Elle réagit aux commandes utilisateur, change d’apparence et transfère les
données entre l’ordinateur qui présente l’applet et l’ordinateur qui la sert.
La Figure 1.1 présente un exemple intéressant de page Web dynamique, un applet qui permet d’afficher des molécules et réalise des calculs sophistiqués. A l’aide de la souris, vous pouvez faire pivoter
chaque molécule et zoomer dessus, pour mieux en comprendre la structure. Ce type de manipulation
directe est impossible avec des pages Web statiques, elle n’est possible qu’avec les applets (vous
trouverez cet applet à l’adresse http://jmol.sourceforge.net).
On peut aussi employer des applets pour ajouter des boutons et des champs d’entrée à une page Web.
Mais le téléchargement de ces applets via une connexion téléphonique est très lent. De plus, il vous
est possible d’obtenir pratiquement le même résultat avec le HTML dynamique, les formulaires
HTML et un langage de script tel que JavaScript. Les premiers applets ont bien sûr été utilisés pour
l’animation : les globes en rotation, les personnages animés de bande dessinée, etc. Mais les animations GIF sont en mesure d’exécuter tout cela. Les applets ne sont donc nécessaires que pour les
interactions riches, et non pour la conception de pages générales.
Les problèmes de compatibilité des navigateurs et l’inconvénient du téléchargement du code des
applets via des connexions réseau lentes expliquent l’échec relatif des applets sur les pages Web
d’Internet. La situation est complètement différente sur les intranets. Dans ce cas, il n’existe aucun
problème de bande passante. Le temps de téléchargement des applets ne constitue donc pas un
problème. Dans ce type d’environnement, il est également possible de contrôler la version de navigateur utilisée ou d’employer le Java Plug-in de façon cohérente. Des programmes distribués pour
Livre Java .book Page 15 Jeudi, 25. novembre 2004 3:04 15
Chapitre 1
Une introduction à Java
15
chaque utilisation via le Web ne peuvent être incorrectement installés et configurés par les employés.
De plus, aucun déplacement de l’administrateur système n’est nécessaire pour mettre le code à jour
sur les machines client. De nombreuses entreprises ont transformé des programmes tels que le
contrôle d’inventaire, la planification des congés, le remboursement des notes de frais, etc., en
applets qui utilisent le navigateur comme plate-forme de distribution.
Figure 1.1
L’applet Jmol.
Au moment où nous écrivons ces lignes, la tendance revient des programmes orientés client vers la
programmation côté serveur. En particulier, les serveurs d’applications peuvent utiliser les capacités
de surveillance de la machine virtuelle de Java pour réaliser la répartition automatique de la charge, le
regroupement de connexions base de données, la synchronisation d’objets, un arrêt et un redémarrage
sécurisés et autres opérations nécessaires pour les applications serveur évolutives, mais qui sont de
toute évidence difficiles à implémenter correctement. Les programmeurs d’application ont donc intérêt à acheter ces mécanismes sophistiqués, plutôt que les construire. Ils permettent d’accroître la
productivité du programmeur, qui peut se consacrer aux tâches pour lesquelles il est le plus compétent
— la logique de traitement de ses programmes — au lieu de jongler avec les performances du serveur.
Bref historique de Java
Cette section présente un bref historique de l’évolution de Java. Elle se réfère à diverses sources
publiées (et, plus important encore, à un entretien avec les créateurs de Java paru dans le magazine
en ligne SunWorld de juillet 1995).
La naissance de Java date de 1991, lorsqu’un groupe d’ingénieurs de Sun, dirigé par Patrick Naughton
et James Gosling, voulut concevoir un petit langage informatique adapté à des appareils de consommation comme les boîtes de commutation de câble TV. Ces appareils ne disposant que de très peu de
puissance ou de mémoire, le langage devait être concis et générer un code très strict. Des constructeurs
Livre Java .book Page 16 Jeudi, 25. novembre 2004 3:04 15
16
Au cœur de Java 2 - Notions fondamentales
différents étant susceptibles de choisir des unités centrales différentes, il était également important
que le langage ne soit pas lié à une seule architecture. Le nom de code du projet était "Green".
Les exigences d’un code concis et indépendant de la plate-forme conduisirent l’équipe à reprendre le
modèle que certaines implémentations de Pascal avaient adopté au début de l’avènement des PC.
Niklaus Wirth, l’inventeur de Pascal, avait préconisé la conception d’un langage portable générant
du code intermédiaire pour des machines hypothétiques (souvent nommées machines virtuelles —
de là, la machine virtuelle Java ou JVM). Ce code pouvait alors être utilisé sur toute machine disposant de l’interpréteur approprié. Les ingénieurs du projet Green utilisaient également une machine
virtuelle, ce qui résolvait leur principal problème.
Les employés de Sun avaient toutefois une culture UNIX. Ils ont donc basé leur langage sur C++
plutôt que sur Pascal. Au lieu de créer un langage fonctionnel, ils ont mis au point un langage orienté
objet. Cependant, comme Gosling l’annonce dans l’interview, "depuis toujours, le langage est un
outil et non une fin". Gosling décida de nommer son langage "Oak" (probablement parce qu’il
appréciait la vue de sa fenêtre de bureau, qui donnait sur un chêne). Les employés de Sun se sont
aperçus plus tard que ce nom avait déjà été attribué à un langage informatique. Ils l’ont donc transformé en Java. Ce choix s’est révélé heureux.
Le projet Green a donné naissance au produit nommé "*7", en 1992. Il s’agissait d’un contrôle à
distance extrêmement intelligent (il avait la puissance d’une SPARCstation dans une boîte de
15 × 10 × 10 cm). Malheureusement, personne ne fut intéressé pour le produire chez Sun. L’équipe
de Green dut trouver d’autres ouvertures pour commercialiser sa technologie. Le groupe soumit
alors un nouveau projet. Il proposa la conception d’un boîtier de câble TV capable de gérer de
nouveaux services câblés, tels que la vidéo à la demande. Malgré cela le contrat n’a pu être décroché
(pour la petite histoire, la compagnie qui l’a obtenu était dirigée par le même Jim Clark qui avait
démarré Netscape — une entreprise qui a beaucoup participé au succès de Java).
Le projet Green (sous le nouveau nom de "First Person Inc.") a passé l’année 1993 et la moitié de
1994 à rechercher des acheteurs pour sa technologie — peine perdue (Patrick Naughton, l’un des
fondateurs du groupe, prétend avoir parcouru plus de 80 000 km en avion pour vendre sa technologie). First Person a été dissoute en 1994.
Pendant ce temps, le World Wide Web d’Internet devenait de plus en plus important. L’élément clé
du Web est le navigateur qui traduit la page hypertexte à l’écran. En 1994, la plupart des gens utilisaient Mosaic, un navigateur Web non commercialisé et issu du Supercomputing Center de l’université de l’Illinois en 1993 (alors qu’il était encore étudiant, Marc Andreessen avait participé à la
création de Mosaic pour 6,85 $ l’heure. Par la suite, il a obtenu la notoriété et la fortune en tant que
cofondateur et directeur de technologie de Netscape).
Lors d’une interview pour le SunWorld, Gosling a déclaré qu’au milieu de l’année 1994, les développeurs de langage avaient réalisé qu’ils pouvaient créer un navigateur vraiment cool. Il s’agissait
d’une des rares choses dans le courant client/serveur qui nécessitait certaines des actions bizarres
qu’ils avaient réalisées : l’architecture neutre, le temps réel, la fiabilité, la sécurité — des questions
qui étaient peu importantes dans le monde des stations de travail. Ils ont donc créé un navigateur.
En fait, le vrai navigateur a été créé par Patrick Naughton et Jonathan Payne. Il a ensuite évolué pour
donner naissance au navigateur HotJava actuel. Ce dernier a été écrit en Java pour montrer la puissance de ce langage. Mais les concepteurs avaient également à l’esprit la puissance de ce qui est
actuellement nommé les applets. Ils ont donc donné la possibilité au navigateur d’exécuter le code
Livre Java .book Page 17 Jeudi, 25. novembre 2004 3:04 15
Chapitre 1
Une introduction à Java
17
au sein des pages Web. Cette "démonstration de technologie" fut présentée au SunWorld le 23 mai
1995, et elle fut à l’origine de l’engouement pour Java, qui ne s’est pas démenti.
Sun a diffusé la première version de Java début 1996. On a rapidement réalisé que cette version ne
pouvait être utilisée pour un développement d’applications sérieux. Elle permettait, bien sûr, de
créer un applet de texte animé qui se déplaçait de façon aléatoire sur un fond. Mais il était impossible
d’imprimer... Cette fonctionnalité n’était pas prévue par la version 1.02. Son successeur, la
version 1.1, a comblé les fossés les plus évidents, a grandement amélioré la capacité de réflexion et
ajouté un nouveau modèle pour la programmation GUI. Elle demeurait pourtant assez limitée.
Les grands projets de la conférence JavaOne de 1998 étaient la future version de Java 1.2. Cette
dernière était destinée à remplacer les premières boîtes à outils graphiques et GUI d’amateur par des
versions sophistiquées et modulaires se rapprochant beaucoup plus que leurs prédécesseurs de la
promesse du "Write Once, Run Anywhere"™ (un même programme s’exécute partout). Trois jours
après (!) sa sortie en décembre 1998, le service marketing de Sun a transformé le nom, qui est
devenu Java 2 Standard Edition Software Development Kit Version 1.2 !
Outre "l’Edition Standard", deux autres éditions ont été introduites : "Micro Edition" pour les services intégrés comme les téléphones portables, et "Entreprise Edition" pour le traitement côté serveur.
Cet ouvrage se concentre sur l’Edition Standard.
Les versions 1.3 et 1.4 constituent une amélioration incrémentielle par rapport à la version Java 2
initiale, avec une bibliothèque standard en pleine croissance, des performances accrues et, bien
entendu, un certain nombre de bogues corrigés. Pendant ce temps, la majeure partie de la passion
générée par les applets Java et les applications côté client a diminué, mais Java est devenu la plateforme de choix pour les applications côté serveur.
La version 5.0 est la première depuis la version 1.1 qui actualise le langage Java de manière significative (cette version était numérotée, à l’origine, 1.5, mais le numéro est devenu 5.0 lors de la conférence JavaOne de 2004). Après de nombreuses années de recherche, des types génériques ont été
ajoutés (à peu près comparables aux modèles C++), le défi étant d’intégrer cette fonctionnalité sans
exiger de changements de la machine virtuelle. Plusieurs autres fonctionnalités utiles ont été inspirées par le C# : une boucle for each, l’autoboxing (passage automatique entre type de base et classes
encapsulantes) et les métadonnées. Les changements de langage continuent à poser des problèmes
de compatibilité, mais plusieurs de ces fonctionnalités sont si séduisantes que les programmeurs
devraient les adopter rapidement. Le Tableau 1.1 montre l’évolution du langage Java.
Tableau 1.1 : Evolution du langage Java
Version
Nouvelles fonctionnalités du langage
1.0
Le langage lui-même
1.1
Classes internes
1.2
Aucune
1.3
Aucune
1.4
Assertions
5.0
Classes génériques, boucle for each, varargs, autoboxing, métadonnées, énumérations,
importation static
Livre Java .book Page 18 Jeudi, 25. novembre 2004 3:04 15
18
Au cœur de Java 2 - Notions fondamentales
Le Tableau 1.2 montre l’évolution de la bibliothèque Java au cours des années. Comme vous pouvez
le constater, la taille de l’interface de programmation d’application (API) a considérablement
augmenté.
Tableau 1.2 : Développement de l’API Java Standard Edition
Version
Nombre de classes et d’interfaces
1.0
211
1.1
477
1.2
1 524
1.3
1 840
1.4
2 723
5.0
3 270
Les idées fausses les plus répandues concernant Java
Nous clôturons ce chapitre par une liste de quelques idées fausses concernant Java. Elles seront
accompagnées de leur commentaire.
Java est une extension de HTML.
Java est un langage de programmation. HTML représente une façon de décrire la structure d’une
page Web. Ils n’ont rien en commun, à l’exception du fait qu’il existe des extensions HTML permettant
d’insérer des applets Java sur une page Web.
J’utilise XML, je n’ai donc pas besoin de Java.
Java est un langage de programmation ; XML est une manière de décrire les données. Vous pouvez
traiter des données XML avec tout langage de programmation, mais l’API Java en contient une
excellente prise en charge. En outre, de nombreux outils XML tiers très importants sont mis en place
en Java. Voir le Volume 2 pour en savoir plus.
Java est un langage de programmation facile à apprendre.
Aucun langage de programmation aussi puissant que Java n’est facile. Vous devez toujours distinguer la facilité de l’écriture de programmes triviaux et la difficulté que représente un travail sérieux.
Considérez également que quatre chapitres seulement de ce livre traitent du langage Java. Les autres
chapitres dans les deux volumes traitent de la façon de mettre le langage en application, à l’aide des
bibliothèques Java. Celles-ci contiennent des milliers de classes et d’interfaces, et des dizaines de
milliers de fonctions. Vous n’avez heureusement pas besoin de connaître chacune d’entre elles, mais
vous devez cependant être capable d’en reconnaître un grand nombre pour pouvoir obtenir quelque
chose de réaliste.
Livre Java .book Page 19 Jeudi, 25. novembre 2004 3:04 15
Chapitre 1
Une introduction à Java
19
Java va devenir un langage de programmation universel pour toutes les plates-formes.
En théorie, c’est possible, et il s’agit certainement du souhait de tous les vendeurs, à l’exception de
Microsoft. Il existe cependant de nombreuses applications, déjà parfaitement efficaces sur les ordinateurs de bureau, qui ne fonctionneraient pas sur d’autres unités ou à l’intérieur d’un navigateur.
Ces applications ont été écrites de façon à tirer parti de la vitesse du processeur et de la bibliothèque
de l’interface utilisateur native. Elles ont été portées tant bien que mal sur toutes les plates-formes
importantes. Parmi ces types d’applications figurent les traitements de texte, les éditeurs d’images et
les navigateurs Web. Ils sont écrits en C ou C++, et nous ne voyons aucun intérêt pour l’utilisateur
final à les réécrire en Java.
Java est simplement un autre langage de programmation.
Java est un bon langage de programmation. De nombreux programmeurs le préfèrent à C, C++ ou
C#. Mais des centaines de bons langages de programmation n’ont jamais réussi à percer, alors que
des langages avec des défauts évidents, tels que C++ et Visual Basic, ont remporté un large succès.
Pourquoi ? Le succès d’un langage de programmation est bien plus déterminé par la qualité du
système de support qui l’entoure que par l’élégance de sa syntaxe. Existe-t-il des bibliothèques
utiles, pratiques et standard pour les fonctions que vous envisagez de mettre en œuvre ? Des
vendeurs d’outils ont-ils créé de bons environnements de programmation et de débogage ? Le
langage et l’ensemble des outils s’intègrent-ils avec le reste de l’infrastructure informatique ?
Java a du succès parce que ses bibliothèques de classes vous permettent de réaliser facilement ce
qui représentait jusqu’alors une tâche complexe. La gestion de réseau et les multithreads en sont
des exemples. Le fait que Java réduise les erreurs de pointeur est un bon point. Il semble que les
programmeurs soient plus productifs ainsi. Mais il ne s’agit pas de la source de son succès.
L’arrivée de C# rend Java obsolète.
C# a repris plusieurs bonnes idées de Java, par exemple un langage de programmation propre, une
machine virtuelle et un ramasse-miettes. Mais, quelles qu’en soient les raisons, le C# a également
manqué certaines bonnes choses, comme la sécurité et l’indépendance de la plate-forme. Selon nous,
le plus gros avantage du C# reste son excellent environnement de développement. Si vous appréciez
Windows, optez pour le C#. Mais, si l’on en juge par les offres d’emploi, Java reste le langage
préféré de la majorité des développeurs.
Java est un outil propriétaire, il faut donc l’éviter.
Chacun agira selon sa conscience. Par moments, nous sommes déçus par certains aspects de Java et
souhaitons qu’une équipe concurrente propose une solution à source libre. Mais la situation n’est pas
aussi simple.
Même si Sun possède un contrôle total sur Java, il a, par le biais de la "Communauté Java", impliqué
de nombreuses autres sociétés dans le développement de versions et la conception de nouvelles
bibliothèques. Le code source de la machine virtuelle et des bibliothèques est disponible gratuitement,
mais pour étude uniquement et non pour modification et redistribution.
Si l’on étudie les langages à source libre qui existent, rien n’indique qu’ils fonctionnent mieux.
Les plus populaires sont les trois "P" dans "LAMP" (Linux, Apache, MySQL et Perl/PHP/Python).
Livre Java .book Page 20 Jeudi, 25. novembre 2004 3:04 15
20
Au cœur de Java 2 - Notions fondamentales
Ces langages présentent leurs avantages, mais ils ont aussi souffert de gros changements de versions,
de bibliothèques limitées et du manque d’outils de développement.
A l’autre extrême, nous avons C++ et C#, qui ont été standardisés par des comités indépendants du
fournisseur. Certes, cette procédure est plus transparente que celle de la Communauté Java. Toutefois, les résultats n’ont pas été aussi utiles qu’on aurait pu l’espérer. Standardiser le langage et les
bibliothèques les plus basiques ne suffit pas. Dans une véritable programmation, on dépasse rapidement la gestion des chaînes, des collections et des fichiers. Dans le cas du C#, Microsoft a indiqué
qu’il mettrait la plupart des bibliothèques hors de la procédure de standardisation.
L’avenir de Java réside peut-être dans une procédure à source libre. Mais, pour l’heure, Sun a
convaincu de nombreuses personnes de sa qualité de leader responsable.
Java est interprété, il est donc trop lent pour les applications sérieuses.
Aux premiers jours de Java, le langage était interprété. Aujourd’hui, sauf sur les plates-formes
"Micro" comme les téléphones portables, la machine virtuelle Java utilise un compilateur JIT. Les hot
spots de votre code s’exécuteront aussi rapidement en Java qu’en C++.
Java est moins rapide que le C++, problème qui n’a rien à voir avec la machine virtuelle. Le ramassemiettes est légèrement plus lent que la gestion manuelle de la mémoire, et les programmes Java, à
fonctionnalités similaires, sont plus gourmands en mémoire que les programmes C++. Le démarrage
du programme peut être lent, en particulier avec de très gros programmes. Les GUI Java sont plus
lents que leurs homologues natifs car ils sont conçus indépendamment de la plate-forme.
Le public se plaint depuis des années de la lenteur de Java par rapport au C++. Toutefois, les ordinateurs actuels sont plus rapides. Un programme Java lent s’exécutera un peu mieux que ces programmes C++ incroyablement rapides d’il y a quelques années. Pour l’heure, ces plaintes semblent
obsolètes et certains détracteurs ont tendance à viser plutôt la laideur des interfaces utilisateur de
Java que leur lenteur.
Tous les programmes Java s’exécutent dans une page Web.
Tous les applets Java s’exécutent dans un navigateur Web. C’est la définition même d’un applet —
un programme Java s’exécutant dans un navigateur. Mais il est tout à fait possible, et très utile,
d’écrire des programmes Java autonomes qui s’exécutent indépendamment d’un navigateur Web.
Ces programmes (généralement nommés applications) sont totalement portables. Il suffit de prendre
le code et de l’exécuter sur une autre machine ! Java étant plus pratique et moins sujet aux erreurs
que le langage C++ brut, il s’agit d’un bon choix pour l’écriture des programmes. Ce choix sera
d’autant meilleur qu’il sera associé à des outils d’accès aux bases de données tels que JDBC (Java
Database Connectivity) étudié au Chapitre 4 du Volume 2. C’est probablement le meilleur des choix
comme premier langage d’apprentissage de la programmation.
La majeure partie des programmes de cet ouvrage sont autonomes. Les applets sont bien sûr un sujet
passionnant. Mais les programmes Java autonomes sont plus importants et plus utiles dans la pratique.
Les programmes Java représentent un risque majeur pour la sécurité.
Aux premiers temps de Java, son système de sécurité a quelquefois été pris en défaut, et ces incidents ont été largement commentés. La plupart sont dus à l’implémentation de Java dans un navigateur
Livre Java .book Page 21 Jeudi, 25. novembre 2004 3:04 15
Chapitre 1
Une introduction à Java
21
spécifique. Les chercheurs ont considéré ce système de sécurité comme un défi à relever et ont tenté
de détecter les failles de l’armure de Java. Les pannes techniques découvertes ont toutes été rapidement corrigées et, à notre connaissance, aucun des systèmes actuels n’a jamais été pris en défaut.
Pour replacer ces incidents dans une juste perspective, prenez en compte les millions de virus qui
attaquent les fichiers exécutables de Windows ainsi que les macros de Word. De réels dégâts sont
alors causés, mais curieusement, peu de critiques sont émises concernant la faiblesse de la plateforme concernée. Le mécanisme ActiveX d’Internet Explorer pourrait également représenter une
cible facile à prendre en défaut, mais ce système de sécurité est tellement simple à contourner que
très peu ont pensé à publier leur découverte.
Certains administrateurs système ont même désactivé Java dans les navigateurs de leur entreprise,
alors qu’ils continuent à autoriser les utilisateurs à télécharger des fichiers exécutables, des contrôles
ActiveX et des documents Word. Il s’agit d’un comportement tout à fait ridicule — actuellement, le
risque de se voir attaqué par un applet Java hostile est à peu près comparable au risque de mourir
dans un accident d’avion. Le risque d’infection inhérent à l’ouverture d’un document Word est, au
contraire, comparable au risque de mourir en traversant à pied une autoroute surchargée.
JavaScript est une version simplifiée de Java.
JavaScript, un langage de script que l’on peut utiliser dans les pages Web, a été inventé par Netscape
et s’appelait à l’origine LiveScript. On trouve dans la syntaxe de JavaScript des réminiscences de
Java, mais il n’existe aucune relation (à l’exception du nom, bien sûr) entre ces deux langages. Un
sous-ensemble de JavaScript est standardisé sous le nom de ECMA-262, mais les extensions requises pour pouvoir effectivement travailler n’ont pas été standardisées. En conséquence, l’écriture d’un
code JavaScript qui s’exécute à la fois dans Netscape et Internet Explorer est relativement difficile.
Avec Java, je peux remplacer mon ordinateur par une "boîte noire Internet" bon marché.
Lors de la première sortie de Java, certains pariaient gros là-dessus. Depuis la première édition de cet
ouvrage, nous pensons qu’il est absurde d’imaginer que l’on puisse abandonner une machine de
bureau puissante et pratique pour une machine limitée sans mémoire locale. Cependant, un ordinateur réseau pourvu de Java est une option plausible pour une "initiative zéro administration". En
effet, vous éliminez ainsi le coût des ordinateurs de l’entreprise, mais même cela n’a pas tenu ses
promesses.
Par ailleurs, Java est devenu largement distribué sur les téléphones portables. Nous devons avouer
que nous n’avons pas encore vu d’application Java indispensable fonctionnant sur les téléphones
portables, mais les jeux et les économiseurs d’écran usuels semblent bien se vendre sur de nombreux
marchés.
ASTUCE
Pour obtenir des réponses aux questions communes sur Java, consultez les FAQ Java sur le Web : http://
www.apl.jhu.edu/~hall/java/FAQs-and-Tutorials.html.
Livre Java .book Page 22 Jeudi, 25. novembre 2004 3:04 15
Livre Java .book Page 23 Jeudi, 25. novembre 2004 3:04 15
2
L’environnement
de programmation de Java
Au sommaire de ce chapitre
✔ Installation du kit de développement Java
✔ Choix d’un environnement de développement
✔ Utilisation des outils de ligne de commande
✔ Utilisation d’un environnement de développement intégré
✔ Compilation et exécution des programmes à partir d’un éditeur de texte
✔ Exécution d’une application graphique
✔ Elaboration et exécution d’applets
Ce chapitre traite de l’installation du kit de développement Java et de la façon de compiler et d’exécuter
différents types de programmes : les programmes consoles, les applications graphiques et les applets.
Vous lancez les outils JDK en tapant les commandes dans une fenêtre shell. De nombreux programmeurs préfèrent toutefois le confort d’un environnement de développement intégré. Vous apprendrez à
utiliser un environnement disponible gratuitement, pour compiler et exécuter les programmes Java.
Faciles à comprendre et à utiliser, les environnements de développement intégrés sont longs à charger
et nécessitent des ressources importantes. Comme solution intermédiaire, vous disposez des éditeurs de
texte appelant le compilateur Java et exécutant les programmes Java. Lorsque vous aurez maîtrisé les
techniques présentées dans ce chapitre et choisi vos outils de développement, vous serez prêt à aborder
le Chapitre 3, où vous commencerez à explorer le langage de programmation Java.
Installation du kit de développement Java
Les versions les plus complètes et les plus récentes de Java 2 Standard Edition (J2SE) sont disponibles
auprès de Sun Microsystems pour Solaris, Linux et Windows. Certaines versions de Java sont disponibles en divers degrés de développement pour Macintosh et de nombreuses autres plates-formes, mais
ces versions sont fournies sous licence et distribuées par les fournisseurs de ces plates-formes.
Livre Java .book Page 24 Jeudi, 25. novembre 2004 3:04 15
24
Au cœur de Java 2 - Notions fondamentales
Télécharger le JDK
Pour télécharger le JDK, accédez au site Web de Sun ; vous devrez passer une grande quantité de
jargon avant de pouvoir obtenir le logiciel.
Vous avez déjà rencontré l’abréviation JDK (Java Development Kit). Pour compliquer un peu les
choses, les versions 1.2 à 1.4 du kit étaient connues sous le nom de Java SDK (Software Development Kit). Sachez que vous retrouverez des mentions occasionnelles de cet ancien acronyme.
Vous rencontrerez aussi très souvent le terme "J2SE". Il signifie "Java 2 Standard Edition", par
opposition à J2EE (Java 2 Entreprise Edition) et à J2ME (Java 2 Micro Edition).
Le terme "Java 2" est apparu en 1998, l’année où les commerciaux de Sun ont considéré qu’augmenter le numéro de version par une décimale ne traduisait pas correctement les nouveautés du JDK 1.2.
Or ils ne s’en sont aperçus qu’après la sortie et ont donc décidé de conserver le numéro 1.2 pour le
kit de développement. Les versions consécutives ont été numérotées 1.3, 1.4 et 5.0. La plate-forme a
toutefois été renommée de "Java" en "Java 2". Ce qui nous donne donc Java 2 Standard Edition
Development Kit version 5.0, soit J2SE 5.0.
Ceci peut être assez désarmant pour les ingénieurs, mais c’est là le côté caché du marketing.
Pour les utilisateurs de Solaris, de Linux ou de Windows, accédez à l’adresse http://java.sun.com/
j2se pour télécharger le JDK. Demandez la version 5.0 ou supérieure, puis choisissez votre plateforme.
Sun sort parfois des modules contenant à la fois le Java Development Kit et un environnement de
développement intégré. Cet environnement a, selon les époques, été nommé Forte, Sun ONE Studio,
Sun Java Studio et Netbeans. Nous ne savons pas ce que les arcanes du marketing auront trouvé lorsque vous visiterez le site Web de Sun. Nous vous suggérons de n’installer pour l’heure que le Java
Development Kit. Si vous décidez par la suite d’utiliser l’environnement de développement intégré
de Sun, téléchargez-le simplement à l’adresse http://netbeans.org.
Une fois le JDK téléchargé, suivez les instructions d’installation, qui sont fonction de la plate-forme.
A l’heure où nous écrivons, ils étaient disponibles à l’adresse http://java.sun.com/j2se/5.0/
install.html.
Seules les instructions d’installation et de compilation pour Java dépendent du système. Une fois
Java installé et opérationnel, les autres informations fournies dans ce livre s’appliqueront à votre
situation. L’indépendance vis-à-vis du système est un avantage important de Java.
INFO
La procédure d’installation propose un répertoire d’installation par défaut incluant le numéro de version de Java
JDK, comme jdk5.0. Ceci peut paraître pénible, mais le numéro de version est finalement assez pratique, puisque
vous pouvez tester facilement une nouvelle version du JDK.
Sous Windows, nous vous recommandons fortement de ne pas accepter l’emplacement par défaut avec des espaces
dans le nom du chemin, comme C:\Program Files\jdk5.0. Enlevez simplement la partie Program Files.
Dans cet ouvrage, nous désignons le répertoire d’installation par jdk. Par exemple, lorsque nous faisons référence au
répertoire jdk/bin, nous désignons le répertoire ayant le nom /usr/local/jdk5.0/bin ou C:\jdk5.0\bin.
Livre Java .book Page 25 Jeudi, 25. novembre 2004 3:04 15
Chapitre 2
L’environnement de programmation de Java
25
Configurer le chemin d’exécution
Après avoir installé le JDK, vous devez effectuer une étape supplémentaire : ajouter le répertoire
jdk/bin au chemin d’exécution, la liste des répertoires que traverse le système d’exploitation pour
localiser les fichiers exécutables. Les directives concernant cette étape varient également en fonction
du système d’exploitation.
m
Sous UNIX (y compris Solaris ou Linux), la procédure pour modifier le chemin d’exécution
dépend du shell que vous utilisez. Si vous utilisez le C shell (qui est le défaut pour Solaris), vous
devez ajouter une ligne analogue à celle qui suit à la fin de votre fichier ~/.cshrc :
set path=(/usr/local/jdk/bin $path)
Si vous utilisez le Bourne Again shell (défaut pour Linux), ajoutez une ligne analogue à celle qui
suit, à la fin de votre fichier ~/.bashrc ou ~/.bash_profile :
export PATH=/usr/local/jdk/bin:$PATH
Pour les autres shells UNIX, vous devez rechercher les directives permettant de réaliser la procédure analogue.
m
Sous Windows 95/98/Me, placez une ligne comme celle qui suit à la fin de votre fichier C:\
AUTOEXEC.BAT :
SET PATH=c:\jdk\bin;%PATH%
Notez qu’il n’y a pas d’espaces autour du signe =. Vous devez redémarrer votre ordinateur pour
rendre effective cette modification.
m
Sous Windows NT/2000/XP, ouvrez le Panneau de configuration, sélectionnez Système, puis
Environnement. Parcourez la fenêtre Variables utilisateur pour rechercher la variable nommée
PATH (si vous voulez mettre les outils Java à disposition de tous les utilisateurs de votre machine,
utilisez plutôt la fenêtre Variables système). Ajoutez le répertoire jdk\bin au début du chemin,
en ajoutant un point-virgule pour séparer la nouvelle entrée, de la façon suivante :
c:\jdk\bin;le reste
Sauvegardez votre configuration. Toute nouvelle fenêtre console que vous lancerez comprendra
le chemin correct.
Procédez de la façon suivante pour vérifier que vous avez effectué les manipulations appropriées :
1. Démarrez une fenêtre shell. Cette opération dépend de votre système d’exploitation. Tapez la
ligne :
java -version
2. Appuyez sur la touche Entrée. Vous devez obtenir un affichage comparable à ce qui suit :
java version "5.0"
Java(TM) 2 Runtime Environment, Standard Edition
Java HotSpot(TM) Client VM
Si, à la place, vous obtenez un message du type "java: command not found", "Bad command", "Bad
file name" ou "The name specified is not recognized as an internal or external command, operable
program or batch file", messages qui signalent une commande ou un fichier erroné, vous devez vérifier
votre installation.
Livre Java .book Page 26 Jeudi, 25. novembre 2004 3:04 15
26
Au cœur de Java 2 - Notions fondamentales
Installer la bibliothèque et la documentation
Les fichiers source de bibliothèque sont fournis dans le JDK sous la forme d’un fichier compressé
src.zip, et vous devez le décompresser pour avoir accès au code source. La procédure suivante est
hautement recommandée.
1. Assurez-vous que le JDK est installé et que le répertoire jdk/bin figure dans le chemin d’exécution.
2. Ouvrez une commande shell.
3. Positionnez-vous dans le répertoire jdk (par ex. /usr/local/jdk5.0 ou C:\jdk5.0).
4. Créez un sous-répertoire src
mkdir src
cd src
5. Exécutez la commande :
jar xvf ../src.zip
(ou jar xvf ..\src.zip sous Windows)
ASTUCE
Le fichier src.zip contient le code source pour toutes les bibliothèques publiques. Vous pouvez obtenir d’autres
codes source (pour le compilateur, la machine virtuelle, les méthodes natives et les classes privées helper), à l’adresse
http://www.sun.com/software/communitysource/j2se/java2/index.html.
La documentation est contenue dans un fichier compressé séparé du JDK. Vous pouvez télécharger
la documentation à l’adresse http://java.sun.com/docs. Plusieurs formats (.zip, .gz et .Z) sont
disponibles. Décompressez le format qui vous convient le mieux. En cas de doute, utilisez le fichier
zip, car vous pouvez le décompresser à l’aide du programme jar qui fait partie du JDK. Si vous
décidez d’utiliser jar, procédez de la façon suivante :
1. Assurez-vous que le JDK est installé et que le répertoire jdk/bin figure dans le chemin d’exécution.
2. Copiez le fichier zip de documentation dans le répertoire qui contient le répertoire jdk. Le fichier
est nommé j2sdkversion-doc.zip, où version ressemble à 5_0.
3. Lancez une commande shell.
4. Positionnez-vous dans le répertoire jdk.
5. Exécutez la commande :
jar xvf j2sdkversion-doc.zip
où version est le numéro de version approprié.
Installer les exemples de programmes
Il est conseillé d’installer les exemples de programmes à partir du CD-ROM ou de les télécharger à l’adresse http://www.phptr.com/corejava. Les programmes sont compressés dans un
fichier zip corejava.zip. Décompressez-les dans un répertoire séparé que nous vous recommandons d’appeler CoreJavaBook. Vous pouvez utiliser n’importe quel utilitaire tel que
Livre Java .book Page 27 Jeudi, 25. novembre 2004 3:04 15
Chapitre 2
L’environnement de programmation de Java
27
WinZip (http:// www.winzip.com), ou simplement avoir recours à l’utilitaire jar qui fait partie
du JDK. Si vous utilisez jar, procédez de la façon suivante :
1. Assurez-vous que le JDK est installé et que le répertoire jdk/bin figure dans le chemin d’exécution.
2. Créez un répertoire nommé CoreJavaBook.
3. Copiez le fichier corejava.zip dans ce répertoire.
4. Lancez une commande shell.
5. Positionnez-vous dans le répertoire CoreJavaBook.
6. Exécutez la commande :
jar xvf corejava.zip
Explorer les répertoires de Java
Au cours de votre étude, vous aurez à examiner des fichiers source Java. Vous devrez également,
bien sûr, exploiter au maximum la documentation bibliothèque. Le Tableau 2.1 présente l’arborescence des répertoires du JDK.
Tableau 2.1 : Arborescence des répertoires de Java
Structure du
répertoire
Description
Le nom peut être différent, par exemple jdk5.0
jdk
bin
Compilateur et outils
demo
Démos
docs
Documentation de la bibliothèque au format HTML (après décompression
de j2sdkversion-doc.zip)
include
Fichiers pour les méthodes natives (voir Volume 2)
jre
Fichiers d’environnement d’exécution de Java
lib
Fichiers de bibliothèque
src
Source de bibliothèque (après décompression de src.zip)
Les deux sous-répertoires les plus importants sont docs et src. Le répertoire docs contient la
documentation de la bibliothèque Java au format HTML. Vous pouvez la consulter à l’aide de tout
navigateur Web tel que Netscape.
ASTUCE
Définissez un signet dans votre navigateur pour le fichier docs/api/index.html. Vous consulterez fréquemment
cette page au cours de votre étude de la plate-forme Java !
Livre Java .book Page 28 Jeudi, 25. novembre 2004 3:04 15
28
Au cœur de Java 2 - Notions fondamentales
Le répertoire src contient le code source de la partie publique des bibliothèques de Java. Lorsque ce
langage vous sera plus familier, ce livre et les informations en ligne ne vous apporteront sans doute
plus les infos dont vous avez besoin. Le code source de Java constituera alors un bon emplacement
où commencer les recherches. Il est quelquefois rassurant de savoir que l’on a toujours la possibilité
de se plonger dans le code source pour découvrir ce qui est réellement réalisé par une fonction de
bibliothèque. Si vous vous intéressez, par exemple, à la classe System, vous pouvez consulter src/
java/lang/System.java.
Choix de l’environnement de développement
Si vous avez l’habitude de programmer avec Microsoft Visual Studio, vous êtes accoutumé à un
environnement de développement qui dispose d’un éditeur de texte intégré et de menus vous permettant de compiler et d’exécuter un programme avec un débogueur intégré. La version de base du JDK
ne contient rien de tel, même approximativement. Tout se fait par l’entrée de commandes dans une
fenêtre shell. Nous vous indiquons comment installer et utiliser la configuration de base du JDK, car
nous avons constaté que les environnements de développement sophistiqués ne facilitent pas nécessairement l’apprentissage de Java — ils peuvent se révéler complexes et masquer certains détails
intéressants et importants pour le programmeur.
Les environnements de développement intégrés sont plus fastidieux à utiliser pour un programme
simple. Ils sont plus lents, nécessitent des ordinateurs plus puissants, et requièrent une configuration
du projet assez lourde pour chaque programme que vous écrivez. Ces environnements ont un léger
avantage si vous écrivez des programmes Java plus importants qui contiennent de nombreux fichiers
source. Ces environnements fournissent aussi des débogueurs et des systèmes de contrôle de la
version. Vous apprendrez dans cet ouvrage les rudiments de l’utilisation d’Eclipse, un environnement de développement disponible gratuitement et lui-même écrit en Java. Bien entendu, si vous
disposez déjà d’un environnement de développement, tel que NetBeans ou JBuilder, qui prend en
charge la version actuelle de Java, n’hésitez pas à l’utiliser.
Pour les programmes simples, un bon compromis entre les outils de ligne de commande et un
environnement de développement intégré sera d’utiliser un éditeur qui s’intègre au JDK. Sous
Linux, le meilleur choix est Emacs. Sous Windows, nous conseillons TextPad, un excellent éditeur
de programmation shareware pour Windows s’intégrant bien avec Java. Enfin, JEdit constitue une
excellente alternative multi-plate-forme. Un éditeur de texte qui s’intègre au JDK peut simplifier
et accélérer le développement de programmes Java. Nous avons adopté cette approche pour le
développement et le test de la plupart des programmes de cet ouvrage. Puisque vous pouvez
compiler et exécuter le code source à partir de l’éditeur, il peut devenir ici votre environnement de
développement de facto.
En résumé, vous disposez de trois choix possibles pour un environnement de développement :
m
Utiliser le JDK et votre éditeur de texte préféré. Compilez et lancez les programmes dans une
commande shell.
m
Utiliser un environnement de développement intégré tel qu’Eclipse ou l’un des autres environnements disponibles, gratuitement ou non.
Livre Java .book Page 29 Jeudi, 25. novembre 2004 3:04 15
Chapitre 2
m
L’environnement de programmation de Java
29
Utiliser le JDK et un éditeur de texte intégrable au JDK. Emacs, TextPad et JEdit sont des choix
possibles, et il existe bien d’autres programmes. Compilez et lancez les programmes au sein de
l’éditeur.
Utilisation des outils de ligne de commande
Commençons par le plus difficile : compiler et lancer un programme Java à partir de la ligne de
commande.
Ouvrez un shell. Positionnez-vous dans le répertoire CoreJavaBook/v1ch2/Welcome (le répertoire
CoreJavaBook est celui dans lequel vous avez installé le code source pour les exemples du livre, tel
qu’expliqué précédemment).
Entrez les commandes suivantes :
javac Welcome.java
java Welcome
Vous devez voir apparaître le message de la Figure 2.1 à l’écran.
INFO
Sous Windows, ouvrez une fenêtre shell comme suit : choisissez la commande Exécuter du menu Démarrer. Si vous
utilisez Windows NT/2000/XP, tapez cmd, sinon tapez command. Appuyez sur Entrée, le shell apparaît. Si vous n’avez
jamais utilisé cette fonctionnalité, nous vous suggérons de suivre un didacticiel pour apprendre les bases des lignes
de commande. De nombreux départements d’enseignement ont installé des didacticiels sur leurs sites tel (en anglais)
http://www.cs.sjsu.edu/faculty/horstman/CS46A/windows/tutorial.html.
Figure 2.1
Compilation et exécution
de Welcome.java.
Livre Java .book Page 30 Jeudi, 25. novembre 2004 3:04 15
30
Au cœur de Java 2 - Notions fondamentales
Félicitations ! Vous venez de compiler et d’exécuter votre premier programme Java.
Que s’est-il passé ? Le programme javac est le compilateur Java. Il compile le fichier Welcome.java en
un fichier Welcome.class. Le programme java lance la machine virtuelle Java. Il interprète les
bytecodes que le compilateur a placés dans le fichier class.
INFO
Si vous obtenez un message d’erreur relatif à la ligne
for (String g : greeting)
cela signifie que vous utilisez probablement une ancienne version du compilateur Java. JDK 5.0 introduit plusieurs
fonctions utiles au langage de programmation Java dont nous profiterons dans cet ouvrage.
Si vous utilisez une ancienne version, vous devez réécrire la boucle comme suit :
for (int i = 0; i < greeting.length; i++=
System.out.println(greeting[i]);
Dans cet ouvrage, nous utilisons toujours les fonctionnalités du langage JDK 5.0. Leur transformation en leur équivalent de l’ancienne version est très simple (voir l’Annexe B pour en savoir plus).
Le programme Welcome est extrêmement simple. Il se contente d’afficher un message sur l’écran.
Vous pouvez examiner ce programme dans l’Exemple 2.1 — nous en expliquerons le fonctionnement
dans le prochain chapitre.
Exemple 2.1 : Welcome.java
public class Welcome
{
public static void main(String[] args)
{
String[] greeting = new String[3];
greeting[0] = "Welcome to Core Java";
greeting[1] = "by Cay Horstmann";
greeting[2] = "and Gary Cornell";
for (String g : greeting)
System.out.println(g);
}
}
Conseils pour la recherche d’erreurs
A l’heure des environnements de développement visuels, les programmeurs n’ont pas l’habitude de
lancer des programmes dans une fenêtre shell. Tant de choses peuvent mal tourner et mener à des
résultats décevants.
Surveillez particulièrement les points suivants :
m
Si vous tapez le programme manuellement, faites attention aux lettres majuscules et minuscules.
En particulier, le nom de classe est Welcome et non welcome ou WELCOME.
Livre Java .book Page 31 Jeudi, 25. novembre 2004 3:04 15
Chapitre 2
L’environnement de programmation de Java
31
m
Le compilateur requiert un nom de fichier Welcome.java. Lorsque vous exécutez le programme,
vous spécifiez un nom de classe (Welcome) sans extension .java ni .class.
m
Si vous obtenez un message tel que "Bad command or file name" ou "javac: command not
found", signalant une commande erronée, vous devez vérifier votre installation, en particulier la
configuration du chemin d’exécution.
m
Si javac signale une erreur "cannot read: Welcome.java", signalant une erreur de lecture du
fichier, vérifiez si ce fichier est présent dans le répertoire.
Sous UNIX, vérifiez que vous avez respecté la casse des caractères pour Welcome.java.
Sous Windows, utilisez la commande shell dir, et non l’outil Explorateur graphique. Certains
éditeurs de texte (en particulier le Bloc-notes) ajoutent systématiquement une extension .txt
après chaque fichier. Si vous utilisez le Bloc-notes pour modifier Welcome.java, le fichier sera
enregistré sous le nom Welcome.java.txt. Dans la configuration par défaut de Windows,
l’Explorateur conspire avec le Bloc-notes et masque l’extension .txt, car elle est considérée
comme un "type de fichier connu". Dans ce cas, vous devez renommer le fichier à l’aide de la
commande shell ren ou le réenregistrer, en plaçant des guillemets autour du nom de fichier :
"Welcome.java".
m
Si java affiche un message signalant une erreur "java.lang.NoClassDefFoundError", vérifiez
soigneusement le nom de la classe concernée.
Si l’interpréteur se plaint que welcome contient un w minuscule, vous devez réémettre la
commande java Welcome avec un W majuscule. Comme toujours, la casse doit être respectée
dans Java.
Si l’interpréteur signale un problème concernant Welcome/java, vous avez accidentellement
tapé java Welcome.java. Réémettez la commande java Welcome.
m
Si vous avez tapé java Welcome et que la machine virtuelle ne trouve pas la classe Welcome,
vérifiez si quelqu’un a configuré le chemin de classe (class path) sur votre système. Pour des
programmes simples, il vaut mieux l’annuler. Pour ce faire, tapez set CLASSPATH=. Cette
commande fonctionne sous Windows et UNIX/Linux avec le shell C. Sous UNIX/Linux,
avec le shell Bourne/bash, utilisez export CLASSPATH=. Voir le Chapitre 4 pour plus de
détails.
m
Si vous recevez un message d’erreur sur une nouvelle construction de langage, vérifiez que votre
compilateur supporte le JDK 5.0. Si vous ne parvenez pas à utiliser le JDK 5.0 ou version ultérieure, modifiez le code source, comme indiqué à l’Annexe B.
m
Si vous avez trop d’erreurs dans votre programme, tous les messages vont défiler très vite. Le
compilateur envoie les messages vers la sortie d’erreur standard, ce qui rend leur capture difficile
s’ils occupent plus d’un écran.
Sur un système UNIX ou Windows NT/2000/XP, vous pouvez utiliser l’opérateur shell 2> pour
rediriger les erreurs vers un fichier :
javac MyProg.java 2> errors.txt
Livre Java .book Page 32 Jeudi, 25. novembre 2004 3:04 15
32
Au cœur de Java 2 - Notions fondamentales
Sous Windows 95/98/Me, il est impossible de rediriger le flux d’erreur standard à partir du shell.
Vous pouvez télécharger le programme errout à partir de l’adresse http://www.horstmann.com/corejava/faq.html et exécuter
errout javac MyProg.java > errors.txt
ASTUCE
Il existe à l’adresse http://java.sun.com/docs/books/tutorial/getStarted/cupojava/ un excellent didacticiel qui
explore en détail les pièges qui peuvent dérouter les débutants.
Utilisation d’un environnement de développement intégré
Dans cette section, vous apprendrez à compiler un programme à l’aide d’Eclipse, un environnement
de développement intégré gratuit disponible à l’adresse http://eclipse.org. Eclipse est écrit en Java mais,
comme il utilise une bibliothèque de fenêtre non standard, il n’est pas aussi portable que Java. Il en
existe néanmoins des versions pour Linux, Mac OS X, Solaris et Windows.
Après le démarrage d’Eclipse, choisissez File/New Project, puis sélectionnez "Java Project" dans la
boîte de dialogue de l’assistant (voir Figure 2.2). Ces captures d’écran proviennent d’Eclipse 3.0M8.
Votre version sera peut-être légèrement différente.
Cliquez sur Next. Indiquez le nom du projet, à savoir "Welcome", et tapez le nom de chemin complet
jusqu’au répertoire qui contient Welcome.java ; consultez la Figure 2.3. Vérifiez que l’option intitulée
"Create project in workspace" est décochée. Cliquez sur Finish.
Figure 2.2
Boîte de dialogue
New Project dans Eclipse.
Le projet est maintenant créé. Cliquez sur le triangle situé dans le volet de gauche près de la fenêtre
de projet pour l’ouvrir, puis cliquez sur le triangle près de "Default package". Double-cliquez sur
Welcome.java. Une fenêtre s’ouvre qui contient le code du programme (voir Figure 2.4).
Livre Java .book Page 33 Jeudi, 25. novembre 2004 3:04 15
Chapitre 2
L’environnement de programmation de Java
33
Figure 2.3
Configuration
d’un projet Eclipse.
Figure 2.4
Modification d’un fichier
source avec Eclipse.
Cliquez sur le nom du projet (Welcome) du bouton droit de la souris, dans le volet le plus à gauche.
Sélectionnez Build Project dans le menu qui apparaît. Votre programme est maintenant compilé. Si
l’opération réussit, choisissez Run/Run As/Java Application. Une fenêtre apparaît au bas de la fenêtre.
Le résultat du programme s’y affiche (voir Figure 2.5).
Livre Java .book Page 34 Jeudi, 25. novembre 2004 3:04 15
34
Au cœur de Java 2 - Notions fondamentales
Figure 2.5
Exécution
d’un programme
dans Eclipse.
Localiser les erreurs de compilation
Notre programme ne devrait pas contenir d’erreur de frappe ou de bogue (après tout, il ne comprend que
quelques lignes de code). Supposons, pour la démonstration, qu’il contienne une coquille (peut-être
même une erreur de syntaxe). Essayez d’exécuter le fichier en modifiant la casse de String de la façon
suivante :
public static void main(string[] args)
Compilez à nouveau le programme. Vous obtiendrez des messages d’erreur (voir Figure 2.6) qui
signalent un type string inconnu. Cliquez simplement sur le message. Le curseur se positionne sur
la ligne correspondante dans la fenêtre d’édition pour vous permettre de corriger l’erreur.
Figure 2.6
Des messages d’erreur
dans Eclipse.
Livre Java .book Page 35 Jeudi, 25. novembre 2004 3:04 15
Chapitre 2
L’environnement de programmation de Java
35
Ces instructions doivent vous amener à vouloir travailler dans un environnement intégré. Nous
étudierons le débogueur Eclipse au Chapitre 11.
Compilation et exécution de programmes à partir d’un éditeur
de texte
Un environnement de développement intégré procure de nombreux avantages, mais présente aussi
certains inconvénients. En particulier, s’il s’agit de programmes simples qui ne sont pas répartis en
plusieurs fichiers source, un tel environnement avec un temps de démarrage assez long et de
nombreuses fioritures peut sembler quelque peu excessif. Heureusement, de nombreux éditeurs de texte
ont la possibilité de lancer le compilateur et des programmes Java et d’intercepter les messages
d’erreur et la sortie du programme. Dans cette section, nous allons étudier un exemple typique
d’éditeur de texte, Emacs.
INFO
GNU Emacs est disponible à l’adresse http://www.gnu.org/software/emacs/. Pour le port Windows de GNU
Emacs, voyez http://www.gnu.org/software/emacs/windows/ntemacs.html. Veillez à installer le package JDEE
(Java Development Environment for Emacs) si vous utilisez Emacs pour Java. Vous pouvez le télécharger à
l’adresse http://jdee.sunsite.dk. Pour JDK 5.0, vous devez utiliser JDEE version 2.4.3beta 1 ou supérieure.
Emacs est un merveilleux éditeur de texte, disponible gratuitement pour UNIX, Linux,
Windows et Mac OS X. Pourtant, de nombreux programmeurs Windows rechignent à apprendre
à l’utiliser. Nous leur recommandons alors d’utiliser TextPad. A la différence d’Emacs, TextPad
se conforme aux conventions Windows. Il est disponible à l’adresse http://www.textpad.com.
Sachez qu’il s’agit d’un shareware. Vous êtes censé payer pour l’utiliser au-delà de la période
d’essai (nous n’avons aucun intérêt à le faire vendre, mais nous sommes très satisfaits de son
utilisation).
Autre choix populaire : JEdit, un très bon éditeur écrit en Java et disponible gratuitement à l’adresse
http://jedit.org. Que vous utilisiez Emacs, TextPad, JEdit ou un autre éditeur, l’idée est la même.
L’éditeur lance le compilateur et capture les messages d’erreur. Vous corrigez les erreurs, recompilez
le programme et appellez une autre commande pour exécuter votre programme.
La Figure 2.7 montre l’éditeur Emacs compilant un programme Java (choisissez JDE/Compile dans
le menu pour lancer le compilateur).
Les messages d’erreur s’affichent dans la moitié inférieure de l’écran. Lorsque vous déplacez le
curseur sur un message d’erreur et que vous appuyez sur la touche Entrée, le curseur se positionne
sur la ligne correspondante du code source.
Lorsque toutes les erreurs ont été corrigées, vous pouvez exécuter le programme en choisissant JDE/
Run App dans le menu. La sortie s’affiche dans une fenêtre d’édition (voir Figure 2.8).
Livre Java .book Page 36 Jeudi, 25. novembre 2004 3:04 15
36
Au cœur de Java 2 - Notions fondamentales
Figure 2.7
Compilation
d’un programme
avec Emacs.
Figure 2.8
Exécution
d’un programme
à partir d’Emacs.
Livre Java .book Page 37 Jeudi, 25. novembre 2004 3:04 15
Chapitre 2
L’environnement de programmation de Java
37
Exécution d’une application graphique
Le programme Welcome ne présentait pas beaucoup d’intérêt. Exécutons maintenant une application
graphique. Il s’agit d’un afficheur simple de fichiers image. Compilons ce programme et exécutonsle depuis la ligne de commande.
1. Ouvrez une fenêtre shell.
2. Placez-vous sur le répertoire CoreJavaBook/v1ch2/ImageViewer.
3. Tapez :
javac ImageViewer.java
java ImageViewer
Une nouvelle fenêtre de programme apparaît avec notre visionneuse, ImageViewer (voir Figure 2.9).
Sélectionnez maintenant File/Open et recherchez le fichier GIF à ouvrir (nous avons inclus quelques
exemples de fichiers dans le même répertoire).
Pour fermer le programme, cliquez sur le bouton de fermeture dans la barre de titre ou déroulez le
menu système et choisissez Quitter (pour compiler et exécuter ce programme dans un éditeur de
texte ou un environnement de développement intégré, procédez comme précédemment. Par exemple,
dans le cas d’Emacs, choisissez JDE/Compile, puis JDE/Run App.
Figure 2.9
Exécution de l’application
ImageViewer.
Nous espérons que vous trouverez ce programme pratique et intéressant. Examinez rapidement le
code source. Ce programme est plus long que le précédent, mais il n’est pas très compliqué en
comparaison avec la quantité de code qui aurait été nécessaire pour écrire une application analogue
en C ou C++. Ce type de programme est bien sûr très facile à écrire avec Visual Basic ou plutôt à
copier et coller. Le JDK ne propose pas de générateur d’interface visuel, vous devez donc tout
programmer à la main (voir Exemple 2.2). Vous apprendrez à créer des programmes graphiques tels
que celui-ci aux Chapitres 7 à 9.
Livre Java .book Page 38 Jeudi, 25. novembre 2004 3:04 15
38
Au cœur de Java 2 - Notions fondamentales
Exemple 2.2 : ImageViewer.java
import
import
import
import
java.awt.*;
java.awt.event.*;
java.io.*;
javax.swing.*;
/**
Un programme permettant d’afficher des images.
*/
public class ImageViewer
{
public static void main(String[] args)
{
JFrame frame = new ImageViewerFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec une étiquette permettant d’afficher une image.
*/
class ImageViewerFrame extends JFrame
{
public ImageViewerFrame()
{
setTitle("ImageViewer");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// utiliser une étiquette pour afficher les images
label = new JLabel();
add(label);
// configurer le sélecteur de fichiers
chooser = new JFileChooser();
chooser.setCurrentDirectory(new File("."));
// configurer la barre de menus
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
JMenu menu = new JMenu("File");
menuBar.add(menu);
JMenuItem openItem = new JMenuItem("Open");
menu.add(openItem);
openItem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
// montrer la boîte de dialogue du sélecteur
int result = chooser.showOpenDialog(null);
// en cas de sélection d’un fichier, définir comme icône
// de l’étiquette
if (result == JFileChooser.APPROVE_OPTION)
Livre Java .book Page 39 Jeudi, 25. novembre 2004 3:04 15
Chapitre 2
L’environnement de programmation de Java
39
{
String name = chooser.getSelectedFile().getPath();
label.setIcon(new ImageIcon(name));
}
}
});
JMenuItem exitItem = new JMenuItem("Exit");
menu.add(exitItem);
exitItem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
System.exit(0);
}
});
private
private
private
private
JLabel label;
JFileChooser chooser;
static final int DEFAULT_WIDTH = 300;
static final int DEFAULT_HEIGHT = 400;
}
Elaboration et exécution d’applets
Les deux premiers programmes présentés dans cet ouvrage sont des applications Java, des programmes autonomes comme tout programme natif. Comme nous l’avons mentionné dans le dernier
chapitre, la réputation de Java est due en grande partie à sa capacité à exécuter des applets dans un
navigateur Web. Nous allons vous montrer comment créer et exécuter un applet à partir de la ligne de
commande. Puis nous allons charger l’applet dans l’éditeur d’applets fourni avec le JDK et enfin,
nous l’afficherons dans un navigateur Web.
Ouvrez un shell et placez-vous dans le répertoire CoreJavaBook/v1ch2/WelcomeApplet, puis
saisissez les commandes suivantes :
javac WelcomeApplet.java
appletviewer WelcomeApplet.html
La Figure 2.10 montre ce que vous voyez dans la fenêtre de l’afficheur d’applets.
Figure 2.10
L’applet WelcomeApplet
dans l’afficheur d’applets.
Livre Java .book Page 40 Jeudi, 25. novembre 2004 3:04 15
40
Au cœur de Java 2 - Notions fondamentales
La première commande, qui nous est maintenant familière, est la commande d’appel du compilateur
Java. Celui-ci compile le fichier source WelcomeApplet.java et produit le fichier de bytecodes
WelcomeApplet.class.
Cependant, nous n’exécutons pas cette fois l’interpréteur Java, mais nous invoquons le programme
appletviewer. Ce programme est un outil particulier inclus avec le JDK qui vous permet de tester
rapidement un applet. Vous devez lui transmettre un fichier HTML plutôt que le nom d’un fichier de
classe java. Le contenu du fichier WelcomeApplet.html est présenté dans l’Exemple 2.3 ci-après.
Exemple 2.3 : WelcomeApplet.html
<html>
<head>
<title>WelcomeApplet</title>
</head>
<body>
<hr/>
<p>
This applet is from the book
<a href="http://www.horstmann.com/corejava.html">Core Java</a>
by <em>Cay Horstmann</em> and <em>Gary Cornell</em>,
published by Sun Microsystems Press.
</p>
<applet code=WelcomeApplet.class width="400" height="200">
<param name="greeting" value="Welcome to Core Java!"/>
</applet>
<hr/>
<p><a href="WelcomeApplet.java">The source.</a></p>
</body>
</html>
Si vous connaissez le code HTML, vous remarquerez quelques instructions standard et la balise
applet qui demande à l’afficheur de charger l’applet dont le code est stocké dans WelcomeApplet.class. Cet afficheur d’applets ignore toutes les balises HTML exceptée la balise applet.
Les autres balises sont visibles si vous affichez le fichier HTML dans un navigateur Java 2. Cependant,
la situation des navigateurs est un peu confuse.
m
Mozilla (et Netscape 6 et versions ultérieures) prennent en charge Java 2 sous Windows, Linux
et Mac OS X. Pour tester ces applets, téléchargez simplement la dernière version et vérifiez que
Java est activé.
m
Certaines versions d’Internet Explorer ne prennent pas du tout en charge Java. D’autres ne prennent en charge que la très obsolète machine virtuelle Microsoft Java. Si vous exécutez Internet
Explorer sous Windows, accédez à l’adresse http://java.com et installez le plug-in Java.
m
Safari et Internet Explorer sont intégrés dans l’implémentation Macintosh Java de Macintosh
sous OS X, qui prend en charge JDK 1.4 au moment où nous rédigeons. OS 9 ne supporte que la
vieille version 1.1.
A condition d’avoir un navigateur compatible Java 2, vous pouvez tenter de charger l’applet dans le
navigateur.
1. Démarrez votre navigateur.
2. Sélectionnez Fichier/Ouvrir (ou l’équivalent).
3. Placez-vous sur le répertoire CoreJavaBook/v1ch2/WelcomeApplet.
Livre Java .book Page 41 Jeudi, 25. novembre 2004 3:04 15
Chapitre 2
L’environnement de programmation de Java
41
Vous devez voir apparaître le fichier WelcomeApplet.html dans la boîte de dialogue. Chargez le
fichier. Votre navigateur va maintenant charger l’applet avec le texte qui l’entoure. Votre écran doit
ressembler à celui de la Figure 2.11.
Vous pouvez constater que cette application est réellement dynamique et adaptée à l’environnement
Internet. Cliquez sur le bouton Cay Horstmann, l’applet commande au navigateur d’afficher la page
Web de Cay. Cliquez sur le bouton Gary Cornell, l’applet lance l’affichage d’une fenêtre de courrier
électronique avec l’adresse e-mail de Gary préenregistrée.
Figure 2.11
Exécution de l’applet
WelcomeApplet dans
un navigateur.
Vous remarquerez qu’aucun de ces deux boutons ne fonctionne dans l’afficheur d’applets. Cet afficheur n’a pas la possibilité d’envoyer du courrier ou d’afficher une page Web, il ignore donc vos
demandes. L’afficheur d’applets est uniquement destiné à tester les applets de façon isolée, mais
vous devrez placer ces dernières dans un navigateur pour contrôler leur interaction avec le navigateur
et Internet.
ASTUCE
Vous pouvez aussi exécuter des applets depuis votre éditeur ou votre environnement de développement intégré.
Dans Emacs, sélectionnez JDE/Run Applet dans le menu. Dans Eclipse, utilisez Run/Run as/Java Applet.
Pour terminer, le code de l’applet est présenté dans l’Exemple 2.4. A ce niveau de votre étude, contentezvous de l’examiner rapidement. Nous reviendrons sur l’écriture des applets au Chapitre 10.
Dans ce chapitre, vous avez appris les mécanismes de la compilation et de l’exécution des programmes Java. Vous êtes maintenant prêt à aborder le Chapitre 3, où vous attaquerez l’apprentissage du
langage Java.
Livre Java .book Page 42 Jeudi, 25. novembre 2004 3:04 15
42
Au cœur de Java 2 - Notions fondamentales
Exemple 2.4 : WelcomeApplet.java
import
import
import
import
javax.swing.*;
java.awt.*;
java.awt.event.*;
java.net.*;
public class WelcomeApplet extends JApplet
{
public void init()
{
setLayout(new BorderLayout());
JLabel label = new JLabel(getParameter("greeting"),
SwingConstants.CENTER);
label.setFont(new Font("Serif", Font.BOLD, 18));
add(label, BorderLayout.CENTER);
JPanel panel = new JPanel();
JButton cayButton = new JButton("Cay Horstmann");
cayButton.addActionListener(getURLActionListener
("http://www.horstmann.com"));
panel.add(cayButton);
JButton garyButton = new JButton("Gary Cornell");
garyButton.addActionListener(makeURLActionListener
("mailto:[email protected]"));
panel.add(garyButton);
add(panel, BorderLayout.SOUTH);
}
private ActionListener makeURLActionListener(final String u)
{
return new
ActionListener()
{
public void actionPerformed(ActionEvent evt)
{
try
{
getAppletContext().showDocument(new URL(u));
}
catch(MalformedURLException e)
{
e.printStackTrace();
}
}
};
}
}
Livre Java .book Page 43 Jeudi, 25. novembre 2004 3:04 15
3
Structures fondamentales
de la programmation Java
Au sommaire de ce chapitre
✔ Un exemple simple de programme Java
✔ Commentaires
✔ Types de données
✔ Variables
✔ Opérateurs
✔ Chaînes
✔ Entrées et sorties
✔ Flux de contrôle
✔ Grands nombres
✔ Tableaux
Nous supposons maintenant que vous avez correctement installé le JDK et que vous avez pu exécuter
les exemples de programmes proposés au Chapitre 2. Il est temps d’aborder la programmation.
Ce chapitre vous montrera comment sont implémentés en Java certains concepts fondamentaux de la
programmation, tels que les types de données, les instructions de branchement et les boucles.
Malheureusement, Java ne permet pas d’écrire facilement un programme utilisant une interface
graphique — il faut connaître de nombreuses fonctionnalités pour construire des fenêtres, ajouter
des zones de texte, des boutons et les autres composants d’une interface. La présentation des
techniques exigées par une interface graphique nous entraînerait trop loin de notre sujet — les
concepts fondamentaux de la programmation — et les exemples de programmes proposés dans
ce chapitre ne seront que des programmes conçus pour illustrer un concept. Tous ces exemples utilisent
simplement une fenêtre shell pour l’entrée et la sortie d’informations.
Livre Java .book Page 44 Jeudi, 25. novembre 2004 3:04 15
44
Au cœur de Java 2 - Notions fondamentales
Si vous être un programmeur C++ expérimenté, vous pouvez vous contenter de parcourir ce chapitre
et de vous concentrer uniquement sur les rubriques Info C++. Les programmeurs qui viennent d’un
autre environnement, comme Visual Basic, rencontreront à la fois des concepts familiers et une
syntaxe très différente : nous leur recommandons de lire ce chapitre attentivement.
Un exemple simple de programme Java
Examinons le plus simple des programmes Java ; il se contente d’afficher un message à la console :
public class firstSample
{
public static void main(String[] args)
{
System.out.println("We will not use ’Hello, World!’");
}
}
Même si cela doit prendre un peu de temps, il est nécessaire de vous familiariser avec la présentation de
cet exemple ; les éléments qui le composent se retrouveront dans toutes les applications. Avant tout,
précisons que Java est sensible à la casse des caractères (majuscules et minuscules). Si vous commettez
la moindre erreur en tapant, par exemple, Main au lieu de main, le programme ne pourra pas s’exécuter !
Etudions maintenant le code source, ligne par ligne. Le mot clé public est appelé un modificateur
d’accès (ou encore un spécificateur d’accès ou spécificateur de visibilité) ; les modificateurs déterminent quelles autres parties du programme peuvent être utilisées par notre exemple. Nous reparlerons des modificateurs au Chapitre 5. Le mot clé class est là pour vous rappeler que tout ce que l’on
programme en Java se trouve à l’intérieur d’une classe. Nous étudierons en détail les classes dans le
prochain chapitre, mais considérez dès à présent qu’une classe est une sorte de conteneur renfermant
la logique du programme qui définit le comportement d’une application. Comme nous l’avons vu au
Chapitre 1, les classes sont des briques logicielles avec lesquelles sont construites toutes les applications
ou applets Java. Dans un programme Java, tout doit toujours se trouver dans une classe.
Derrière le mot clé class se trouve le nom de la classe. Les règles de Java sont assez souples en ce
qui concerne l’attribution des noms de classes. Ceux-ci doivent commencer par une lettre et peuvent
ensuite contenir n’importe quelle combinaison de lettres et de chiffres. Leur taille n’est pas limitée.
Il ne faut cependant pas attribuer un mot réservé de Java (comme public ou class) à un nom de
classe (vous trouverez une liste des mots réservés dans l’Annexe A).
Comme vous pouvez le constater avec notre classe FirstSample, la convention généralement
admise est de former les noms de classes avec des substantifs commençant par une majuscule.
Lorsqu’un nom est constitué de plusieurs mots, placez l’initiale de chaque mot en majuscule.
Il faut donner au fichier du code source le même nom que celui de la classe publique, avec l’extension .java. Le code sera donc sauvegardé dans un fichier baptisé FirstSample.java (répétons que
la casse des caractères est importante, il ne faut pas nommer le fichier firstsample.java).
Si vous n’avez pas commis d’erreur de frappe en nommant le fichier et en tapant le code source, la
compilation de ce code produira un fichier contenant le pseudo-code de la classe. Le compilateur Java
nomme automatiquement le fichier de pseudo-code FirstSample.class et le sauvegarde dans le même
répertoire que le fichier source. Lorsque tout cela est terminé, lancez le programme à l’aide de la
commande suivante : java FirstSample.
Livre Java .book Page 45 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
45
Ne spécifiez pas l’extension .class. L’exécution du programme affiche simplement la chaîne We
will not use ’Hello, World’! à la console.
Lorsque vous tapez la commande
java NomDeClasse
pour lancer un programme compilé, la machine virtuelle démarre toujours l’exécution par les
instructions de la méthode main de la classe spécifiée. Par conséquent, vous devez écrire une
méthode main dans le fichier source de la classe pour que le code puisse être exécuté. Bien entendu,
il est possible d’ajouter vos propres méthodes à une classe et de les appeler à partir de la méthode
main (vous verrez au prochain chapitre comment écrire vos propres méthodes).
INFO
Selon la spécification du langage Java, la méthode main doit être déclarée public. La spécification est le document
officiel qui décrit le langage Java. Vous pouvez le consulter ou le télécharger à l’adresse http://java.sun.com/docs/
books/jls. Toutefois, plusieurs versions du lanceur Java avaient pour intention d’exécuter les programmes Java même
lorsque la méthode main n’était pas public. Un programmeur a alors rédigé un rapport de bogue. Pour le voir,
consultez le site http://bugs.sun.com/bugdatabase/index.jsp et entrez le numéro d’identification 4252539. Le bogue
a toutefois fait l’objet de la mention "clos, ne sera pas résolu". Un ingénieur Sun a ajouté une explication indiquant
que la spécification de la machine virtuelle (à l’adresse http://java.sun.com/docs/books/vmspec) n’obligeait pas à ce
que main soit public et a précisé que "le résoudre risquait d’entraîner des problèmes". Heureusement, le bon sens
a fini par parler. Le lanceur Java du JDK 1.4 et au-delà oblige à ce que la méthode main soit public.
Cette histoire est assez intéressante. D’un côté, il est désagréable de voir que des ingénieurs d’assurance qualité,
souvent débordés et pas toujours au fait des aspects pointus de Java, prennent des décisions contestables sur les
rapports de bogues. De l’autre, il est remarquable que Sun place les rapports de bogues et leurs résolutions sur le
Web, afin que tout le monde puisse les étudier. La "parade des bogues" est une ressource très utile pour les programmeurs. Vous pouvez même "voter" pour votre bogue favori. Ceux qui réuniront le plus de suffrages pourraient bien
être résolus dans la prochaine version du JDK.
Remarquez les accolades dans le code source. En Java, comme en C/C++, les accolades sont employées
pour délimiter les parties (généralement appelées blocs) de votre programme. En Java, le code de chaque
méthode doit débuter par une accolade ouvrante { et se terminer par une accolade fermante }.
La manière d’employer des accolades a provoqué une inépuisable controverse. Nous employons
dans cet ouvrage un style d’indentation classique, en alignant les accolades ouvrantes et fermantes
de chaque bloc. Comme les espaces ne sont pas pris en compte par le compilateur, vous pouvez utiliser le style de présentation que vous préférez. Nous reparlerons de l’emploi des accolades lorsque
nous étudierons les boucles.
Pour l’instant, ne vous préoccupez pas des mots clé static void, songez simplement qu’ils sont
nécessaires à la compilation du programme. Cette curieuse incantation vous sera familière à la fin du
Chapitre 4. Rappelez-vous surtout que chaque application Java doit disposer d’une méthode main
dont l’en-tête est identique à celui que nous avons présenté dans notre exemple :
public class NomDeClasse
{
public static void main(String[] args)
{
Instructions du programme
}
}
Livre Java .book Page 46 Jeudi, 25. novembre 2004 3:04 15
46
Au cœur de Java 2 - Notions fondamentales
INFO C++
Les programmeurs C++ savent ce qu’est une classe. Les classes Java sont comparables aux classes C++, mais certaines
différences risquent de vous induire en erreur. En Java, par exemple, toutes les fonctions sont des méthodes d’une
classe quelconque (la terminologie standard les appelle des méthodes et non des fonctions membres). Ainsi, Java
requiert que la méthode main se trouve dans une classe. Sans doute êtes-vous également familiarisé avec la notion
de fonction membre statique en C++. Il s’agit de fonctions membres définies à l’intérieur d’une classe et qui
n’opèrent pas sur des objets. En Java, la méthode main est toujours statique. Précisons enfin que, comme en C/C++,
le mot clé void indique que la méthode ne renvoie aucune valeur. Contrairement à C/C++, la méthode main ne
renvoie pas un "code de sortie" au système d’exploitation. Si la méthode main se termine normalement, le
programme Java a le code de sortie 0 qui l’indique. Pour terminer le programme avec un code de sortie différent,
utilisez la méthode System.exit.
Portez maintenant votre attention sur ce fragment de code :
{
System.out.println("We will not use ’Hello, World!’");
}
Les accolades délimitent le début et la fin du corps de la méthode. Celle-ci ne contient qu’une seule
instruction. Comme dans la plupart des langages de programmation, vous pouvez considérer les
instructions Java comme les phrases du langage. En Java, chaque instruction doit se terminer par un
point-virgule. En particulier, les retours à la ligne ne délimitent pas la fin d’une instruction, et une
même instruction peut donc occuper plusieurs lignes en cas de besoin.
Le corps de la méthode main contient une instruction qui envoie une ligne de texte vers la console.
Nous employons ici l’objet System.out et appelons sa méthode println. Remarquez que le point
sert à invoquer la méthode. Java utilise toujours la syntaxe
objet.méthode(paramètres)
pour ce qui équivaut à un appel de fonction.
Dans ce cas précis, nous appelons la méthode println et lui passons une chaîne en paramètre. La
méthode affiche la chaîne sur la console. Elle passe ensuite à la ligne afin que chaque appel à
println affiche la chaîne spécifiée sur une nouvelle ligne. Notez que Java, comme C/C++, utilise les
guillemets pour délimiter les chaînes (pour plus d’informations, voir la section de ce chapitre consacrée
aux chaînes).
Comme les fonctions de n’importe quel langage de programmation, les méthodes de Java peuvent
utiliser zéro, un ou plusieurs paramètres (certains langages les appellent arguments). Même si une
méthode ne prend aucun paramètre, il faut néanmoins employer des parenthèses vides). Il existe par
exemple une variante sans paramètres de la méthode println qui imprime une ligne vide. Elle est
invoquée de la façon suivante :
System.out.println();
INFO
System.out dispose également d’une méthode print qui n’ajoute pas de retour à la ligne en sortie. Par exemple,
System.out.print("Bonjour") imprime "Bonjour" sans retourner à la ligne. La sortie suivante apparaîtra
immédiatement derrière le "r" de "Bonjour".
Livre Java .book Page 47 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
47
Commentaires
Les commentaires de Java, comme ceux de la plupart des langages de programmation, n’apparaissent pas dans le programme exécutable. Vous pouvez donc ajouter autant de commentaires que vous
le souhaitez sans craindre de gonfler la taille du code compilé. Java propose trois types de commentaires. Le plus courant est //, qui débute un commentaire allant jusqu’à la fin de ligne :
System.out.println("We will not use ’Hello, World!’");
// Ce gag est-il trop connu?
Lorsque des commentaires plus longs sont nécessaires, il est possible de placer // au début de
chaque ligne, ou vous pouvez utiliser /* et */ pour délimiter le début et la fin d’un long commentaire.
Nous voyons ce type de délimiteur à l’Exemple 3.1.
Exemple 3.1 : FirstSample.java
/*
Voici le premier exemple de programme de Au coeur de Java, Chapitre 3
Copyright (C) 1997 Cay Horstmann et Gary Cornell
*/
public class FirstSample
{
public static void main(String[] args)
{
System.out.println("We will not use ’Hello, World!’");
}
}
Il existe un troisième type de commentaire utilisable pour la génération automatique de documentation. Ce type de commentaire commence par /** et se termine par */. Pour en savoir plus sur la
génération automatique de documentation, consultez le Chapitre 4.
ATTENTION
Les commentaires /* */ ne peuvent pas être imbriqués en Java. Autrement dit, pour désactiver un bloc de code, il
ne suffit pas de l’enfermer entre un /* et un */, car ce bloc peut lui-même contenir un délimiteur */.
Types de données
Java est un langage fortement typé. Cela signifie que le type de chaque variable doit être déclaré.
Il existe huit types primitifs (prédéfinis) en Java. Quatre d’entre eux sont des types entiers (integer) ;
deux sont des types réels à virgule flottante ; un est le type caractère char utilisé pour le codage
Unicode (voir la section consacrée au type char) et le type boolean, pour les valeurs booléennes
(vrai/faux).
INFO
Java dispose d’un package arithmétique de précision arbitraire. Cependant, les "grands nombres", comme on les
appelle, sont des objets Java et ne constituent pas un nouveau type Java. Vous apprendrez à les utiliser plus loin dans
ce chapitre.
Livre Java .book Page 48 Jeudi, 25. novembre 2004 3:04 15
48
Au cœur de Java 2 - Notions fondamentales
Entiers
Les types entiers représentent les nombres sans partie décimale. Les valeurs négatives sont autorisées.
Java dispose des quatre types présentés au Tableau 3.1.
Tableau 3.1 : Les types entiers de Java
Type
Occupation
en mémoire
Intervalle (limites incluses)
int
4 octets
– 2 147 483 648 à 2 147 483 647 (un peu plus de 2 milliards)
short
2 octets
– 32768 à 32767
long
8 octets
– 9 223 372 036 854 775 808 à 9 223 372 036 854 775 807
byte
1 octet
– 128 à 127
Le type int se révèle le plus pratique dans la majorité des cas. Bien entendu, si vous désirez exprimer le nombre des habitants de la planète, vous devrez employer le type long. Les types byte et
short sont essentiellement destinés à des applications spécialisées, telles que la gestion bas niveau
des fichiers ou la manipulation de tableaux volumineux, lorsque l’occupation mémoire doit être
réduite au minimum.
En Java, la plage valide des types de nombres entiers ne dépend pas de la machine sur laquelle
s’exécute le code. Cela épargne bien des efforts au programmeur souhaitant porter un logiciel
d’une plate-forme vers une autre, ou même entre différents systèmes d’exploitation sur une
même plate-forme. En revanche, les programmes C et C++ utilisent le type d’entier le plus efficace pour chaque processeur. Par conséquent, un programme C qui fonctionne bien sur un
processeur 32 bits peut provoquer un dépassement de capacité sur un système 16 bits. Comme
les programmes Java doivent s’exécuter identiquement sur toutes les machines, les plages de
valeur des différents types sont fixes.
Le suffixe des entiers longs est L (par exemple, 4000000000L). Le préfixe des entiers hexadécimaux
est 0x (par exemple, 0xCAFE). Les valeurs octales ont le préfixe 0. Par exemple, 010 vaut 8. Cela peut
prêter à confusion, il est donc déconseillé d’avoir recours aux constantes octales.
INFO C++
En C et C++, int représente le type entier qui dépend de l’ordinateur cible. Sur un processeur 16 bits, comme le 8086,
les entiers sont codés sur 2 octets. Sur un processeur 32 bits, comme le SPARC de Sun, ils sont codés sur 4 octets. Sur
un Pentium Intel, le codage du type entier C et C++ dépend du système d’exploitation : 2 octets sous DOS et
Windows 3.1, 4 octets sous Windows en mode 32 bits. En Java, la taille de tous les types numériques est indépendante de la plate-forme utilisée.
Remarquez que Java ne possède pas de type non signé.
Livre Java .book Page 49 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
49
Types à virgule flottante
Les types à virgule flottante expriment les nombres réels disposant d’une partie décimale. Il existe
deux types de réels, présentés au Tableau 3.2.
Tableau 3.2 : Les types à virgule flottante
Type
Occupation
en mémoire
Intervalle
float
4 octets
Environ ± 3.40282347E + 38F (6 ou 7 décimales significatives)
double
8 octets
Environ ± 1.79769313486231570E + 308 (15 chiffres significatifs)
Le terme double indique que les nombres de ce type ont une précision deux fois supérieure à ceux
du type float (on les appelle parfois nombres à double précision). On choisira de préférence le type
double dans la plupart des applications. La précision limitée de float se révèle insuffisante dans de
nombreuses situations. On ne l’emploiera que dans les rares cas où la vitesse de calcul (plus élevée
pour les nombres à précision simple) est importante pour l’exécution, ou lorsqu’une grande quantité
de nombres doit être stockée (afin d’économiser la mémoire).
Les nombres de type float ont pour suffixe F, par exemple 3.402F. Les nombres à décimales exprimés sans ce suffixe F, par exemple 3.402, sont toujours considérés comme étant du type double.
Pour ces derniers, il est également possible de spécifier un suffixe D, par exemple 3.402D.
Depuis le JDK 5.0, vous pouvez spécifier des nombres à virgule flottante en hexadécimal. Par exemple, 0.125 équivaut à 0x1.0p-3. Dans la notation hexadécimale, vous utilisez p, et non e, pour indiquer
un exposant.
Tous les calculs en virgule flottante respectent la spécification IEEE 754. En particulier, il existe trois
valeurs spéciales en virgule flottante :
m
infinité positive ;
m
infinité négative ;
m
NaN (Not a Number — pas un nombre).
Elles servent à indiquer les dépassements et les erreurs. Par exemple, le résultat de la division d’un
nombre positif par 0 est "infinité positive". Le calcul 0/0 ou la racine carrée d’un nombre négatif
donne "NaN".
INFO
Il existe des constantes Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY et Double.NaN (ainsi que
les constantes correspondantes Float) permettant de représenter ces valeurs spéciales. Elles sont cependant rarement
utilisées dans la pratique. En particulier, vous ne pouvez tester
if (x == Double.NaN) // n’est jamais vrai
pour vérifier si un résultat particulier est égal à Double.NaN. Toutes les valeurs "pas un nombre" sont considérées
comme distinctes. Vous pouvez cependant employer la méthode Double.isNaN :
if (Double.isNaN(x)) // vérifier si x est "pas un nombre"
Livre Java .book Page 50 Jeudi, 25. novembre 2004 3:04 15
50
Au cœur de Java 2 - Notions fondamentales
ATTENTION
Les nombres à virgule flottante ne conviennent pas aux calculs financiers, dans lesquels les erreurs d’arrondi sont inadmissibles. La commande System.out.println(2.0 - 1.1) produit 0.8999999999999999, et non 0.9 comme
vous pourriez le penser. Ces erreurs d’arrondi proviennent du fait que les nombres à virgule flottante sont représentés
dans un système de nombres binaires. Il n’existe pas de représentation binaire précise de la fraction 1/10, tout comme
il n’existe pas de représentation précise de la fraction 1/3 dans le système décimal. Si vous voulez obtenir des calculs
numériques précis sans erreurs d’arrondi, utilisez la classe BigDecimal, présentée plus loin dans ce chapitre.
Le type char
Pour bien comprendre le type char, vous devez connaître le schéma de codage Unicode. Unicode a
été inventé pour surmonter les limitations des schémas de codage traditionnels. Avant lui, il existait
de nombreuses normes différentes : ASCII aux Etats-Unis, ISO 8859-1 pour les langues d’Europe de
l’Est, KOI-8 pour le russe, GB18030 et BIG-5 pour le chinois, etc. Deux problèmes se posaient. Une
valeur de code particulière correspondait à des lettres différentes selon le schéma de codage. De
plus, le codage des langues ayant de grands jeux de caractères avait des longueurs variables :
certains caractères communs étaient codés sous la forme d’octets simples, d’autres nécessitaient
deux octets ou plus.
Unicode a été conçu pour résoudre ces problèmes. Aux débuts des efforts d’unification, dans les
années 1980, un code de largeur fixe sur 2 octets était plus que suffisant pour coder tous les caractères utilisés dans toutes les langues du monde, et il restait encore de la place pour une future extension
(ou, du moins, c’est ce que l’on pensait). En 1991 est sorti Unicode 1.0, qui utilisait légèrement
moins de la moitié des 65 536 valeurs de code disponibles. Java a été conçu dès le départ pour utiliser les caractères Unicode à 16 bits, ce qui constituait une avancée majeure sur les autres langages de
programmation, qui utilisaient des caractères sur 8 bits.
Malheureusement, au fil du temps, ce qui devait arriver arriva. Unicode a grossi au-delà des
65 536 caractères, principalement du fait de l’ajout d’un très grand jeu d’idéogrammes utilisés
pour le chinois, le japonais et le coréen. Le type char à 16 bits est désormais insuffisant pour
décrire tous les caractères Unicode.
Nous ferons appel à la terminologie pour expliquer comment ce problème a été résolu en Java, et ce
depuis le JDK 5.0. Un point de code est une valeur de code associée à un caractère dans un schéma
de codage. Dans la norme Unicode, les points de code sont écrits en hexadécimal et préfixés par U+,
par exemple U+0041 pour le point de code de la lettre A. Unicode possède des points de code regroupés en 17 plans de codes. Le premier, appelé plan multilingue de base, est constitué des caractères
Unicode "classiques" ayant les points de code U+0000 à U+FFFF. Seize autres plans, ayant les points
de code U+10000 à U+10FFFF, contiennent les caractères complémentaires.
Le codage UTF-16 permet de représenter tous les points de code Unicode dans un code de longueur
variable. Les caractères du plan multilingue de base sont représentés sous forme de valeurs de
16 bits, appelées unités de code. Les caractères complémentaires sont englobés sous forme de paires
consécutives d’unités de code. Chacune des valeurs d’une paire de codage se situe dans une plage
inusitée de 2 048 octets du plan multilingue de base, appelé zone de remplacement (de U+D800 à
U+DBFF pour la première unité de code, de U+DC00 à U+DFFF pour la deuxième unité de code). Ceci
est assez intéressant, car vous pouvez immédiatement dire si une unité de code procède à un codage
sur un seul caractère ou s’il s’agit de la première ou de la deuxième partie d’un caractère complémentaire. Par exemple, le symbole mathématique pour le jeu d’entiers a le point de code U+1D56B et
Z
Livre Java .book Page 51 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
51
est codé par les deux unités de code U+D835 et U+DD6B (voir http://en.wikipedia.org/wiki/UTF-16
pour obtenir une description de l’algorithme de codage).
En Java, le type char décrit une unité de code en codage UTF-16.
Nous recommandons fortement de ne pas utiliser le type char dans vos programmes, à moins d’être
à l’aise dans la manipulation des unités de code UTF-16. Il vaut presque mieux traiter les chaînes (ce
que nous verrons plus loin) sous forme de types de données abstraits.
Ceci étant dit, vous risquez tout de même de rencontrer des valeurs char. Le plus souvent, ce seront
des constantes de caractères. Par exemple ’A’ est une constante de caractère de valeur 65. Elle diffère
de "A", une chaîne contenant un seul caractère. Les unités de code Unicode peuvent être exprimées
sous la forme hexadécimale, de \u0000 à \uFFFF. Par exemple, \u2122 représente le symbole de
marque déposée (™).
En plus du caractère d’échappement \u indiquant le codage d’une unité de code Unicode,
plusieurs séquences d’échappement existent pour les caractères spéciaux cités dans le
Tableau 3.3. Vous pouvez les utiliser dans des constantes et des chaînes de caractères entre apostrophes ou guillemets, comme ’\u2122’ ou "Hello\n". La séquence d’échappement \u (mais
elle seule) peut même être utilisée en dehors des constantes et des chaînes entre apostrophes ou
guillemets. Par exemple,
public static void main(String\u005B\u005D args)
est tout à fait autorisé : \u005B et \u005D sont les codages UTF-16 des points de code Unicode pour
[ et ].
Tableau 3.3 : Séquences d’échappement pour caractères spéciaux
Code d’échappement
Désignation
Valeur Unicode
\b
Effacement arrière
\u0008
\t
Tabulation horizontale
\u0009
\n
Saut de ligne
\u000a
\r
Retour chariot
\u000d
\"
Guillemet
\u0022
\’
Apostrophe
\u0027
\\
Antislash
\u005c
INFO
Bien que l’on puisse théoriquement utiliser n’importe quel caractère Unicode dans une application ou un applet
Java, l’affichage d’un caractère dépend, en définitive, des capacités de votre système d’exploitation et éventuellement
de votre navigateur (pour un applet).
Livre Java .book Page 52 Jeudi, 25. novembre 2004 3:04 15
52
Au cœur de Java 2 - Notions fondamentales
Type booléen
Le type boolean peut avoir deux valeurs, false (faux) ou true (vrai). Il est employé pour l’évaluation de conditions logiques. Les conversions entre valeurs entières et booléennes sont impossibles.
INFO C++
En C++, des nombres et même des pointeurs peuvent être employés à la place des valeurs booléennes. La valeur 0
équivaut à la valeur booléenne false, et une valeur non zéro équivaut à true. Ce n’est pas le cas avec Java.
Les programmeurs Java sont donc mis en garde contre ce type d’accidents :
if (x = 0) // pardon... je voulais dire x == 0
En C++, ce test est compilé et exécuté, il donne toujours le résultat false. En Java, la compilation du test échoue,
car l’expression entière x = 0 ne peut pas être convertie en une valeur booléenne.
Variables
En Java, toute variable a un type. Vous déclarez une variable en spécifiant d’abord son type, puis son
nom. Voici quelques exemples de déclarations :
double salary;
int vacationDays;
long earthPopulation;
boolean done;
Remarquez que chaque déclaration se termine par un point-virgule. Il est nécessaire, car une déclaration est une instruction Java complète.
Un nom de variable doit débuter par une lettre et doit être une séquence de lettres ou de chiffres.
Notez que le sens des termes "lettres" et "chiffres" est plus large en Java que dans beaucoup d’autres
langages. Une lettre est définie comme l’un des caractères ’A’...’Z’, ’a’...’z’, ’_’, ou
n’importe quel caractère Unicode représentant une lettre dans une langue quelconque. Par exemple,
un utilisateur français ou allemand peut employer des caractères accentués, tels que ä ou ê, dans un
nom de variable. De même, les lecteurs grecs peuvent utiliser π. D’une manière comparable, un
chiffre est un des caractères ’0’ à ’9’, ou n’importe quel caractère Unicode représentant un chiffre
dans une langue. Des espaces ou des symboles tels que ’+’ ou © ne peuvent pas être employés.
Tous les caractères d’un nom de variable sont significatifs, ainsi que la casse de chaque caractère
(minuscule ou majuscule). La longueur possible d’un nom est théoriquement illimitée.
ASTUCE
Si vous vous demandez quels caractères Unicode sont des "lettres" pour Java, vous pouvez le savoir en appelant les
méthodes isJavaIdentifierStart et isJavaIdentifierPart de la classe Character.
Il est évidemment interdit de donner à une variable le même nom qu’un mot réservé de Java (voir la
liste des mots réservés dans l’Annexe A).
Il est possible de déclarer plusieurs variables sur une seule ligne, comme suit :
int i, j; // ce sont deux entiers en Java
Livre Java .book Page 53 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
53
Ce style de déclaration n’est pas recommandé. Si vous définissez chaque variable séparément, vos
programmes seront plus faciles à lire.
INFO
Vous avez vu que les noms étaient sensibles à la casse. Par exemple, hireday et hireDay sont deux noms différents.
En règle générale, n’employez pas deux noms différant seulement par la casse des caractères. Il est toutefois difficile
parfois de définir un nom correct pour une variable. Les programmeurs attribuent alors fréquemment à la variable
le même nom que le type, par exemple :
Box box; // OK--Box est le type et box le nom de la variable
Cependant, une meilleure solution consiste à attribuer un préfixe "a" à la variable
Box aBox;.
Initialisation des variables
Après avoir déclaré une variable, vous devez explicitement l’initialiser à l’aide d’une instruction
d’affectation. Vous ne devez pas utiliser une variable non initialisée. Le compilateur Java signale la
suite d’instructions suivantes comme une erreur :
int vacationDays;
System.out.println(vacationDays); //ERREUR--variable non-initialisée
L’affectation d’une variable déclarée se fait à l’aide du symbole d’égalité (=), précédé à gauche du
nom de la variable et suivi à droite par une expression Java représentant la valeur appropriée :
int vacationDays;
vacationDays = 12;
Une agréable fonctionnalité de Java permet de déclarer et d’initialiser simultanément une variable en
une seule instruction, de la façon suivante :
int vacationDays = 12;
Précisons enfin que Java permet de déclarer des variables n’importe où dans le code. Par exemple, le
code suivant est valide en Java :
double salary = 65000.0;
System.out.println(salary);
int vacationDays = 12; // vous pouvez déclarer la variable ici
En Java, il est recommandé de déclarer les variables aussi près que possible du point de leur
première utilisation.
INFO C++
C et C++ font une distinction entre une déclaration et une définition de variables.
Par exemple,
int i = 10;
est une définition, alors que
extern int i;
est une déclaration. En Java, il n’y a pas de déclaration séparée de la définition.
Livre Java .book Page 54 Jeudi, 25. novembre 2004 3:04 15
54
Au cœur de Java 2 - Notions fondamentales
Constantes
En Java, le mot clé final sert à désigner une constante. Voici un exemple :
public class Constants
{
public static void main(String[] args)
{
final double CM_PER_INCH = 2.54;
double paperWidth = 8.5;
double paperHeight = 11;
System.out.println("Paper size in centimeters: "
+ paperWidth * CM_PER_INCH + " by "
+ paperHeight * CM_PER_INCH);
}
}
Le mot clé final signifie que vous affectez une valeur à la variable une seule fois, et une fois pour
toutes. Par convention, les noms des constantes sont entièrement en majuscules.
Il est plus courant de créer une constante qui est accessible à plusieurs méthodes de la même classe.
Les méthodes de ce genre sont appelées généralement constantes de classe. On définit une constante
de classe à l’aide des mots clés static final. Voici un exemple utilisant une constante de classe :
public class Constants2
{
public static void main(String[] args)
{
double paperWidth = 8.5;
double paperHeight = 11;
System.out.println("Paper size in centimeters: "
+ paperWidth * CM_PER_INCH + " by "
+ paperHeight * CM_PER_INCH);
}
public static final double CM_PER_INCH = 2.54;
}
Notez que la définition de la constante de classe apparaît en dehors de la méthode main. La constante
peut donc aussi être employée dans d’autres méthodes de la même classe. De plus, si (comme dans
notre exemple), la constante est déclarée public, les méthodes des autres classes peuvent aussi utiliser
la constante — sous la forme Constants2.CM_PER_INCH, dans notre exemple.
INFO C++
Bien que const soit un mot réservé de Java, il n’est pas toujours utilisé. C’est le mot clé final qui permet de définir
une constante.
Opérateurs
Les habituels opérateurs arithmétiques + – * / sont respectivement utilisés en Java pour les opérations d’addition, de soustraction, de multiplication et de division. L’opérateur de division / indique
une division entière si les deux arguments sont des entiers et une division en virgule flottante dans les
Livre Java .book Page 55 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
55
autres cas. Le reste d’une division entière est représenté par le symbole %. Par exemple, 15/2 donne
7 ; 15 % 2 donne 1 et 15.0/2 donne 7.5.
Notez que la division d’un entier par 0 déclenche une exception, alors que la division d’une valeur en
virgule flottante par 0 donne un résultat infini ou NaN.
Les opérateurs arithmétiques peuvent être utilisés en combinaison avec l’opérateur d’affectation
pour simplifier l’écriture d’une affectation. Par exemple, l’instruction
x += 4;
équivaut à l’instruction
x = x + 4;
En règle générale, placez l’opérateur arithmétique à gauche du signe =, par exemple *= ou %=.
INFO
L’un des intérêts évidents de la programmation en langage Java est la portabilité. Un calcul doit aboutir au même
résultat quelle que soit la machine virtuelle sur laquelle il est exécuté. Dans le cas de calculs avec des nombres à
virgule flottante, il est étonnamment difficile d’obtenir cette portabilité. Le type double utilise 64 bits pour le
stockage d’une valeur numérique, mais certains processeurs emploient des registres virgule flottante de 80 bits.
Ces registres ajoutent une précision lors des étapes intermédiaires d’un calcul. Examinez, par exemple, le calcul
suivant :
double w = x * y / z;
De nombreux processeurs Intel vont calculer x * y et placer le résultat dans un registre 80 bits, puis diviser par z
pour finalement tronquer le résultat à 64 bits. Cela peut donner un résultat plus précis et éviter un dépassement
d’exposant. Cependant le résultat peut être différent de celui d’un calcul effectué constamment sur 64 bits. Pour
cette raison, la spécification initiale de la machine virtuelle Java prévoyait la troncation de tous les calculs intermédiaires, au grand dam de la communauté numérique. Non seulement les calculs tronqués peuvent provoquer un
dépassement de capacité, mais ils sont aussi plus lents, du fait du temps nécessaire aux opérations de troncation.
C’est pourquoi le langage Java a été actualisé pour tenir compte des besoins conflictuels de performance optimale
et de reproductibilité parfaite. Par défaut, les concepteurs de machine virtuelle peuvent maintenant utiliser une
précision étendue pour les calculs intermédiaires. Toutefois, les méthodes balisées avec le mot clé strictfp doivent
utiliser des opérations virgule flottante strictes menant à des résultats reproductibles. Vous pouvez, par exemple,
baliser la méthode main de la façon suivante :
public static strictfp void main(String[] args)
Toutes les instructions au sein de main auront alors recours à des calculs virgule flottante stricts. Si vous balisez une
classe à l’aide de strictfp, toutes ses méthodes utiliseront les calculs virgule flottante stricts.
Les détails sordides dépendent essentiellement du comportement des processeurs Intel. En mode par défaut, les
résultats intermédiaires sont autorisés à utiliser un exposant étendu, mais pas une mantisse étendue (les puces Intel
gèrent la troncation de la mantisse sans perte de performance). Par conséquent, la seule différence entre les modes
par défaut et strict est que les calculs stricts peuvent engendrer des dépassements de capacité, à la différence des
calculs par défaut.
Si vos yeux s’arrondissent à la lecture de cette Info, ne vous inquiétez pas. Le dépassement de capacité en virgule
flottante ne se pose pas pour la majorité des programmes courants. Nous n’utiliserons pas le mot clé strictfp dans
cet ouvrage.
Livre Java .book Page 56 Jeudi, 25. novembre 2004 3:04 15
56
Au cœur de Java 2 - Notions fondamentales
Opérateurs d’incrémentation et de décrémentation
Les programmeurs savent évidemment qu’une des opérations les plus courantes effectuées sur une
variable numérique consiste à lui ajouter ou à lui retrancher 1. Suivant les traces de C et de C++, Java
offre des opérateurs d’incrémentation et de décrémentation : x++ ajoute 1 à la valeur courante et x-retranche 1 à cette valeur. Ainsi, cet exemple :
int n = 12;
n++;
donne à n la valeur 13. Comme ces opérateurs modifient la valeur d’une variable, ils ne peuvent pas
être appliqués à des nombres. Par exemple, 4++ n’est pas une instruction valide.
Ces opérateurs peuvent en réalité prendre deux formes ; vous avez vu la forme "suffixe", où l’opérateur est situé après l’opérande : n++. Il peut également être placé en préfixe : ++n. Dans les deux cas,
la variable est incrémentée de 1. La différence entre ces deux formes n’apparaît que lorsqu’elles sont
employées dans des expressions. Lorsqu’il est placé en préfixe, l’opérateur effectue d’abord l’addition ;
en suffixe, il fournit l’ancienne valeur de la variable :
int
int
int
int
m
n
a
b
=
=
=
=
7;
7;
2 * ++m; // maintenant a vaut 16, m vaut 8
2 * n++; // maintenant b vaut 14, n vaut 8
Nous déconseillons vivement l’emploi de l’opérateur ++ dans d’autres expressions, car cela entraîne
souvent un code confus et des bogues difficiles à détecter.
L’opérateur ++ a bien évidemment donné son nom au langage C++, mais il a également provoqué une des
premières plaisanteries à propos de ce langage. Les anti-C++ ont fait remarquer que même le nom du
langage contenait un bogue : "En fait, il devrait s’appeler ++C, car on ne désire employer le
langage qu’après son amélioration".
Opérateurs relationnels et booléens
Java offre le jeu complet d’opérateurs relationnels. Un double signe égal, ==, permet de tester
l’égalité de deux opérandes. Par exemple, l’évaluation de
3 == 7
donne un résultat faux (false).
Utilisez != pour pratiquer un test d’inégalité. Par exemple, le résultat de
3 != 7
est vrai (true).
De plus, nous disposons des habituels opérateurs < (inférieur à), > (supérieur à), <= (inférieur ou égal
à) et >= (supérieur ou égal à).
Suivant l’exemple de C++, Java utilise && comme opérateur "et" logique et || comme opérateur "ou"
logique. Le point d’exclamation ! représente l’opérateur de négation. Les opérateurs && et || sont
évalués de manière optimisée (en court-circuit). Le deuxième argument n’est pas évalué si le
premier détermine déjà la valeur. Si vous combinez deux expressions avec l’opérateur &&,
expression && expression
Livre Java .book Page 57 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
57
la valeur de la deuxième expression n’est pas calculée si la première a pu être évaluée à false (puisque le résultat final serait false de toute façon). Ce comportement peut éviter des erreurs. Par exemple,
dans l’expression
x != 0 && 1 / x > x + y // pas de division par 0
la seconde partie n’est jamais évaluée si x égale zéro. Par conséquent, 1/x n’est pas calculé si x vaut
zéro, et une erreur "division par zéro" ne peut pas se produire.
De même, si la première expression est évaluée à true, la valeur de expression1 || expression2
est automatiquement true, sans que la deuxième expression doive être évaluée.
Enfin, Java gère l’opérateur ternaire ? qui se révèle utile à l’occasion. L’expression
condition ? expression1 : expression2
est évaluée à expression1 si la condition est true, à expression2 sinon. Par exemple,
x < y ? x : y
donne le plus petit entre x et y.
Opérateurs binaires
Lorsqu’on manipule des types entiers, il est possible d’employer des opérateurs travaillant directement sur les bits qui composent ces entiers. Certaines techniques de masquage permettent de récupérer
un bit particulier dans un nombre. Les opérateurs binaires sont les suivants :
&("et")
|("ou")
^("ou exclusif")
~("non")
Ces opérateurs travaillent sur des groupes de bits. Par exemple, si n est une variable entière, alors la
déclaration
int quatrièmeBitPartantDeDroite = (n & 8) / 8;
renvoie 1 si le quatrième bit à partir de la droite est à 1 dans la représentation binaire de n, et zéro
dans le cas contraire. L’emploi de & avec la puissance de deux appropriée permet de masquer tous les
bits sauf un.
INFO
Lorsqu’ils sont appliqués à des valeurs boolean, les opérateurs & et | donnent une valeur boolean. Ces opérateurs
sont comparables aux opérateurs && et ||, excepté que & et | ne sont pas évalués de façon optimisée "court-circuit".
C’est-à-dire que les deux arguments sont évalués en premier, avant le calcul du résultat.
Il existe également des opérateurs >> et << permettant de décaler un groupe de bits vers la droite ou
vers la gauche. Ces opérateurs se révèlent pratiques lorsqu’on veut construire des masques binaires :
int quatrièmeBitPartantDeDroite = (n & (1 << 3)) >> 3;
Signalons enfin qu’il existe également un opérateur >>> permettant de remplir les bits de poids fort
avec des zéros, alors que >> étend le bit de signature dans les bits de poids fort. Il n’existe pas
d’opérateur <<<.
Livre Java .book Page 58 Jeudi, 25. novembre 2004 3:04 15
58
Au cœur de Java 2 - Notions fondamentales
ATTENTION
L’argument à droite des opérateurs de décalage est réduit en modulo 32 (à moins qu’il n’y ait un type long du
côté gauche, auquel cas le côté droit est réduit en modulo 64). Par exemple, la valeur de 1 << 35 est la même
que 1 << 3 soit 8.
INFO C++
En C/C++, rien ne vous garantit que >> accomplit un décalage arithmétique (en conservant le signe) ou un décalage
logique (en remplissant les bits avec des zéros). Les concepteurs de l’implémentation sont libres de choisir le mécanisme le plus efficace. Cela signifie que l’opérateur >> de C/C++ n’est réellement défini que pour des nombres qui ne
sont pas négatifs. Java supprime cette ambiguïté.
Fonctions mathématiques et constantes
La classe Math contient un assortiment de fonctions mathématiques qui vous seront parfois utiles,
selon le type de programmation que vous réalisez.
Pour extraire la racine carrée d’un nombre, vous disposez de la méthode sqrt :
double x = 4;
double y = Math.sqrt(x);
System.out.println(y); // affiche 2.0
INFO
Il existe une subtile différence entre les méthodes println et sqrt. La méthode println opère sur un objet,
System.out, défini dans la classe System. Mais la méthode sqrt dans la classe Math n’opère pas sur un objet. Une
telle méthode est qualifiée de statique. Vous étudierez les méthodes statiques au Chapitre 4.
Le langage de programmation Java ne dispose pas d’opérateur pour élever une quantité à une puissance :
vous devez avoir recours à la méthode pow de la classe Math. L’instruction
double y = Math.pow(x, a);
définit y comme la valeur x élevée à la puissance a (xa). La méthode pow a deux paramètres qui sont
du type double, et elle renvoie également une valeur double.
La classe Math fournit les fonctions trigonométriques habituelles :
Math.sin
Math.cos
Math.tan
Math.atan
Math.atan2
et la fonction exponentielle et son inverse, le logarithme naturel :
Math.exp
Math.log
Il y a enfin deux constantes,
Math.PI
Math.E
qui donnent les approximations les plus proches possibles des constantes mathématiques π et e.
Livre Java .book Page 59 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
59
ASTUCE
Depuis le JDK 5.0, vous pouvez éviter le préfixe Math pour les méthodes mathématiques et les constantes en ajoutant la ligne suivante en haut de votre fichier source :
import static java.lang.Math.*;
Par exemple,
System.out.println("The square root of \u03C0 is " + sqrt(PI));
Nous verrons les importations statiques au Chapitre 4.
INFO
Les fonctions de la classe Math utilisent les routines dans l’unité à virgule flottante du calculateur pour une meilleure
performance. Si des résultats totalement prévisibles sont plus importants que la rapidité, employez plutôt la classe
StrictMath. Elle implémente les algorithmes provenant de la bibliothèque mathématique fdlibm "Freely Distributable Math Library", qui garantit des résultats identiques quelle que soit la plate-forme. Consultez le site http://
www.netlib.org/fdlibm/index.html pour obtenir la source de ces algorithmes (alors que fdlibm fournit plus d’une
définition pour une fonction, la classe StrictMath respecte la version IEEE 754 dont le nom commence par un "e").
Conversions de types numériques
Il est souvent nécessaire de convertir un type numérique en un autre. La Figure 3.1 montre les
conversions légales :
Figure 3.1
char
Conversions légales
entre types numériques.
byte
short
int
long
float
double
Les six flèches noires à la Figure 3.1 indiquent les conversions sans perte d’information. Les trois
flèches grises indiquent celles pouvant souffrir d’une perte de précision. Par exemple, un entier large
tel que 123456789 a plus de chiffres que ne peut en représenter le type float. S’il est converti en
type float, la valeur résultante a la magnitude correcte, mais elle perd en précision :
int n = 123456789;
float f = n; // f vaut 1.234567 92E8
Lorsque deux valeurs sont combinées à l’aide d’un opérateur binaire (par exemple, n + f où n est un
entier et f une valeur à virgule flottante), les deux opérandes sont convertis en un type commun avant
la réalisation de l’opération :
m
Si l’un quelconque des opérandes est du type double, l’autre sera converti en type double.
Livre Java .book Page 60 Jeudi, 25. novembre 2004 3:04 15
60
Au cœur de Java 2 - Notions fondamentales
m
Sinon, si l’un quelconque des opérandes est du type float, l’autre sera converti en type float.
m
Sinon, si l’un quelconque des opérandes est du type long, l’autre sera converti en type long.
m
Sinon les deux opérandes seront convertis en type int.
Transtypages
Dans la section précédente, vous avez vu que les valeurs int étaient automatiquement converties en
valeurs double en cas de besoin. D’autre part, il existe évidemment des cas où vous voudrez considérer un double comme un entier. Les conversions numériques sont possibles en Java, mais bien
entendu, au prix d’une perte possible d’information. Les conversions risquant des pertes d’information sont faites à l’aide de transtypages (ou conversions de type). La syntaxe du transtypage fournit
le type cible entre parenthèses, suivi du nom de la variable. Par exemple :
double x = 9.997;
int nx = (int)x;
La variable nx a alors la valeur 9, puisque la conversion d’un type flottant en un type entier fait
perdre la partie fractionnaire.
Si vous voulez arrondir un nombre à virgule flottante en l’entier le plus proche (l’opération la plus
utile dans la plupart des cas), utilisez la méthode Math.round :
double x = 9.997;
int nx = (int)Math.round(x);
Maintenant, la variable nx a la valeur 10. Vous devez toujours utiliser le transtypage (int) si vous
appelez round. C’est parce que la valeur renvoyée par la méthode round est du type long et qu’un
long ne peut qu’être affecté à un int avec un transtypage explicite, puisqu’il existe une possibilité
de perte d’information.
ATTENTION
Si vous essayez de convertir un nombre d’un type en un autre qui dépasse l’étendue du type cible, le résultat sera un
nombre tronqué ayant une valeur différente. Par exemple, (byte) 300 vaut en réalité 44.
INFO C++
Vous ne pouvez convertir des valeurs boolean en quelque type numérique que ce soit. Cela évite les erreurs courantes. Si, exceptionnellement, vous voulez convertir une valeur boolean en un nombre, vous pouvez avoir recours à
une expression conditionnelle telle que b ? 1 : 0.
Parenthèses et hiérarchie des opérateurs
La hiérarchie normale des opérations en Java est présentée au Tableau 3.4. En l’absence de parenthèses, les opérations sont réalisées dans l’ordre hiérarchique indiqué. Les opérateurs de même niveau
sont traités de gauche à droite, sauf pour ceux ayant une association à droite, comme indiqué dans le
tableau. Par exemple, && ayant une priorité supérieure à ||, l’expression
a && b || c
signifie
(a && b) || c
Livre Java .book Page 61 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
61
Etant donné que += a une associativité de droite à gauche, l’expression
a += b += c
signifie
a += (b += c)
En fait, la valeur de b += c (qui est la valeur de b après l’addition) est ajoutée à a.
Tableau 3.4 : Préséance des opérateurs
Opérateurs
Associativité
[] . () (appel de méthode)
De la gauche vers la droite
! ~ ++ -- + (unaire) - (unaire) () (transtypage) new
De la droite vers la gauche
* / %
De la gauche vers la droite
+ -
De la gauche vers la droite
<< >> >>>
De la gauche vers la droite
< <= > >= instanceof
De la gauche vers la droite
== !=
De la gauche vers la droite
&
De la gauche vers la droite
^
De la gauche vers la droite
|
De la gauche vers la droite
&&
De la gauche vers la droite
||
De la gauche vers la droite
?:
De la droite vers la gauche
= += -= *= /= %= &= |= ^= <<= >>= >>>=
De la droite vers la gauche
INFO C++
Contrairement à C et à C++, Java ne dispose pas d’un opérateur "virgule". Il est néanmoins possible d’utiliser une
liste d’expressions séparées par des virgules comme premier ou troisième élément d’une instruction for.
Types énumérés
Une variable ne doit quelquefois contenir qu’un jeu de valeurs limité. Vous pouvez par exemple
vendre des vêtements ou des pizzas en quatre formats : petit, moyen, grand, très grand. Bien sûr, ces
formats pourraient être codés sous forme d’entiers 1, 2, 3, 4 ou de caractères S, M, L et X. Mais cette
configuration est sujette à erreur. Une variable peut trop facilement contenir une valeur erronée
(comme 0 ou m).
Depuis le JDK 5.0, vous pouvez définir votre propre type énuméré dès qu’une situation se présente.
Un type énuméré possède un nombre fini de valeurs nommées. Par exemple,
enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE };
Livre Java .book Page 62 Jeudi, 25. novembre 2004 3:04 15
62
Au cœur de Java 2 - Notions fondamentales
Vous pouvez maintenant déclarer des variables de ce type :
Size s = Size.MEDIUM;
Une variable de type Size ne peut contenir que l’une des valeurs listées dans la déclaration de type
ou la valeur spéciale null qui indique que la variable n’est définie sur aucune valeur.
Nous verrons les types énumérés plus en détail au Chapitre 5.
Chaînes
Les chaînes sont des suites de caractères Unicode. Par exemple, la chaîne "Java\u2122" est constituée de cinq caractères Unicode J, a, v, a et ™. Java ne possède pas de type chaîne intégré. En revanche, la bibliothèque standard Java contient une classe prédéfinie appelée, assez naturellement,
String. Chaque chaîne entre guillemets est une instance de la classe String :
String e = ""; // une chaîne vide
String greeting = "Hello";
Points et unités de code
Les chaînes Java sont implémentées sous forme de suites de valeurs char. Comme nous l’avons vu,
le type char est une unité de code permettant de représenter des points de code Unicode en codage
UTF-16. Les caractères Unicode les plus souvent utilisés peuvent être représentés par une seule
unité de code. Les caractères complémentaires exigent quant à eux une paire d’unités de code.
La méthode length produit le nombre d’unités de code exigé pour une chaîne donnée dans le
codage UTF-16. Par exemple,
String greeting = "Hello";
int n = greeting.length(); // vaut 5
Pour obtenir la bonne longueur, c’est-à-dire le nombre de points de code, appelez
int cpCount = greeting.codePointCount(0, greeting.length());
L’appel s.chartAt(n) renvoie l’unité de code présente à la position n, où n est compris entre 0 et
s.length() -1.
Par exemple,
char first = greeting.charAt(0); // le premier est ’H’
char last = greeting.charAt(4); // le dernier est ’o’
Pour accéder au ième point de code, utilisez les instructions
int index = greeting.offsetByCodePoints(0, i);
int cp = greeting.codePointAt(index);
INFO
Java compte les unités de code dans les chaînes d’une manière particulière : la première unité de code d’une chaîne
occupe la position 0. Cette convention est née dans le C, à une époque où il existait une raison technique à démarrer
les positions à 0. Cette raison a disparu depuis longtemps, et seule la nuisance demeure. Toutefois, les programmeurs
sont tellement habitués à cette convention que les concepteurs Java ont décidé de la garder.
Livre Java .book Page 63 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
63
Pourquoi nous préoccuper tant des unités de code ? Etudiez la phrase
Zis
the set of integers
Le caractère
Zexige deux unités de code en codage UTF-16. Appeler
char ch = sentence.charAt(1)
ne renvoie pas un espace mais la deuxième unité de code de
mieux ne pas utiliser le type char, qui est de trop bas niveau.
Z. Pour éviter ce problème, il vaut
Si votre code traverse une chaîne et que vous souhaitiez étudier chaque point de code tour à tour,
utilisez ces instructions :
int cp = sentence.codePointAt(i);
if (Character.isSupplementaryCodePoint(cp)) i += 2;
else i++;
Heureusement, la méthode codePointAt peut dire si une unité de code est la première ou la seconde
moitié d’un caractère complémentaire, elle renvoie le bon résultat quel que soit le cas. Ainsi, vous
pouvez revenir en arrière avec les instructions suivantes :
i--;
int cp = sentence.codePointAt(i);
if (Character.isSupplementaryCodePoint(cp)) i--;
Sous-chaînes
Pour extraire une sous-chaîne à partir d’une chaîne plus grande, utilisez la méthode substring de la
classe String. Par exemple,
String greeting = "Hello";
String s = greeting.substring(0, 3);
crée une chaîne constituée des caractères "Hel".
Le deuxième paramètre de substring est la première unité de code que vous ne souhaitez pas
copier. Ici, nous voulons copier les unités de code des positions 0, 1 et 2 (de la position 0 à la position
2 incluse). Pour substring, cela va de la position 0 incluse à la position 3 (exclue).
On reconnaît un avantage au fonctionnement de substring : il facilite le calcul du nombre d’unités
de code présentes dans la sous-chaîne. La chaîne s.substring(a, b) a toujours les unités de code
b - a. Par exemple, la sous-chaîne "Hel" a 3 – 0 = 3 unités de code.
Modification de chaînes
La classe String ne fournit pas de méthode permettant de modifier un caractère dans une chaîne existante. Au cas où vous voudriez transformer le contenu de la variable greeting en "Help!", vous ne
pouvez pas remplacer directement la dernière position de greeting par ’p’ et par ’!’. Si vous êtes un
programmeur C, vous devez vous sentir frustré. Et, pourtant, la solution est très simple en Java : il suffit
de récupérer la sous-chaîne à conserver et de la concaténer avec les caractères à remplacer :
greeting := greeting.substring(0, 3) + "p!";
Cette instruction donne à la variable greeting la valeur "Help!".
Comme il n’est pas possible de modifier individuellement des caractères dans une chaîne Java, la
documentation indique que les objets de la classe String sont inaltérables. Tout comme le nombre 3
Livre Java .book Page 64 Jeudi, 25. novembre 2004 3:04 15
64
Au cœur de Java 2 - Notions fondamentales
vaut toujours 3, la chaîne "Hello" contient toujours la suite de caractères ’H’, ’e’, ’l’, ’l’, ’o’.
Il est impossible de changer cette valeur. En revanche, comme nous venons de le voir, il est possible
de modifier le contenu de la variable chaîne greeting et de lui faire référencer une chaîne différente
(de même qu’une variable numérique contenant la valeur 3 peut recevoir la valeur 4).
On pourrait penser que tout cela n’est pas très performant. Ne serait-il pas plus simple de changer les
unités de code au lieu de construire une nouvelle chaîne ? Oui et non. En vérité, il n’est pas très intéressant de générer une nouvelle chaîne contenant la concaténation de "Hel" et de "p!". Mais les
chaînes inaltérables possèdent pourtant un énorme avantage : le compilateur peut les partager.
Pour comprendre cette technique, imaginez que les diverses chaînes résident dans un pool commun.
Les variables chaînes, quant à elles, pointent sur des positions dans le pool. Si vous copiez une variable chaîne, la chaîne d’origine et la copie partagent les mêmes caractères. Tout bien considéré, les
concepteurs de Java ont estimé que l’efficacité du partage des chaînes surpassait l’inconvénient de la
modification des chaînes par extraction de sous-chaînes et concaténation.
Examinez vos propres programmes ; la plupart du temps, vous ne modifiez sans doute pas les chaînes — vous vous contentez de les comparer. Bien entendu, il existe des situations où une manipulation directe se révèle plus efficace (par exemple lorsqu’on assemble des chaînes à partir de caractères
individuels tapés au clavier ou récupérés dans un fichier). Pour ces cas-là, Java fournit la classe
StringBuilder, que nous décrivons au Chapitre 12. Si la gestion des chaînes ne vous préoccupe
pas, vous pouvez ignorer StringBuilder et utiliser simplement la classe String.
INFO C++
Les programmeurs C sont souvent stupéfaits lorsqu’ils rencontrent des chaînes Java pour la première fois, car ils
considèrent les chaînes comme des tableaux de caractères :
char greeting[] = "Hello";
La comparaison n’est pas bonne ; une chaîne Java ressemble davantage à un pointeur char* :
char* greeting = "Hello";
Lorsque vous remplacez la valeur de greeting par une autre chaîne, le code Java exécute à peu près ce qui suit :
char* temp = malloc(6);
strncpy(temp, greeting, 3);
strcpy(temp + 4, "p!", 3);
greeting = temp;
greeting pointe maintenant vers la chaîne "Help!". Et même le plus intégriste des programmeurs C doit
reconnaître que la syntaxe de Java est plus agréable qu’une série d’appels à strncpy. Mais que se passe-t-il si
nous affectons une nouvelle valeur à greeting ?
greeting = "Howdy";
La chaîne d’origine ne va-t-elle pas occuper inutilement la mémoire, puisqu’elle a été allouée dans le pool ? Heureusement, Java récupère automatiquement la mémoire libérée. Si un bloc de mémoire n’est plus utilisé, il finira par être
recyclé.
Si vous programmez en C++ et utilisez la classe string définie par ANSI C++, vous vous sentirez à l’aise avec le type
String de Java. Les objets string de C++ se chargent automatiquement de l’allocation et de la récupération de la
mémoire. La gestion de la mémoire est accomplie explicitement par les constructeurs, les opérateurs d’affectations
et les destructeurs. Cependant, les chaînes C++ sont altérables — il est possible de modifier individuellement un
caractère dans une chaîne.
Livre Java .book Page 65 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
65
Concaténation
Java, comme la plupart des langages de programmation, autorise l’emploi du signe + pour joindre
(concaténer) deux chaînes :
String expletive = "Expletive";
String PG13 = "deleted";
String message = expletive + PG13;
Le code qui précède donne à la variable message le contenu "Expletivedeleted" (remarquez qu’il
n’insère pas d’espace entre les mots : le signe + accole les deux chaînes exactement telles qu’elles
sont fournies).
Lorsque vous concaténez une chaîne et une valeur qui n’est pas une chaîne, cette valeur est convertie
en chaîne (comme vous le verrez au Chapitre 5, tout objet Java peut être converti en chaîne). Voici un
exemple,
int age = 13;
String rating = "PG" + age;
qui donne à la chaîne rating la valeur "PG13".
Cette caractéristique est utilisée couramment pour l’affichage. Par exemple, l’instruction
System.out.println("The answer is " + answer);
est parfaitement valide et affichera la réponse voulue avec un espacement correct (notez l’espace
derrière le mot is).
Test d’égalité des chaînes
Pour savoir si deux chaînes sont égales, utilisez la méthode equals ; l’expression
s.equals(t)
donne true si les chaînes s et t sont égales, et false dans le cas contraire. Notez que s et t peuvent
être des variables chaînes ou des constantes chaînes. Par exemple,
"Hello".equals(greeting)
est parfaitement valide. Pour tester si deux chaînes sont identiques à l’exception de la casse des
caractères, utilisez la méthode equalsIgnoreCase :
"Hello".equalsIgnoreCase("hello")
Attention, n’employez pas l’opérateur == pour tester l’égalité de deux chaînes ! Cet opérateur détermine seulement si les chaînes sont stockées au même emplacement. Il est évident que si deux chaînes se trouvent à la même adresse, elles doivent être égales. Mais des copies de chaînes identiques
peuvent être stockées à des emplacements différents dans la mémoire :
String greeting = "Hello"; //initialise la chaîne greeting
if (greeting == "Hello") . . .
// probablement vrai
if (greeting.substring(0, 3) == "Hel") . . .
// probablement faux
Si la machine virtuelle faisait en sorte de toujours partager les chaînes identiques, l’opérateur ==
pourrait être utilisé pour un test d’égalité. Mais seules les constantes chaînes sont partagées, et non
les chaînes créées à l’aide de l’opérateur + ou de la méthode substring. Par conséquent,
Livre Java .book Page 66 Jeudi, 25. novembre 2004 3:04 15
66
Au cœur de Java 2 - Notions fondamentales
n’employez jamais == pour comparer des chaînes, car cela pourrait générer le pire des bogues : un
bogue intermittent qui semble se produire aléatoirement.
INFO C++
Si vous êtes familiarisé avec la classe string de C++, vous devez être particulièrement attentif aux tests d’égalité,
car la classe string surcharge l’opérateur == pour tester l’égalité du contenu de deux chaînes. Il est peut-être
dommage que les chaînes Java aient un "aspect général" comparable à celui des valeurs numériques, mais qu’elles
se comportent comme des pointeurs lors des tests d’égalité. Les concepteurs auraient pu redéfinir == pour l’adapter
aux chaînes, comme ils l’ont fait pour l’opérateur +, mais aucun langage n’est parfait.
Pour comparer des chaînes, les programmeurs C n’emploient pas l’opérateur ==, mais la fonction strcmp. La
méthode analogue en langage Java est compareTo. Vous pouvez écrire
if (greeting.compareTo("Hello") == 0) . . .
mais il est plus pratique d’appeler la méthode equals.
La classe String de Java contient plus de 50 méthodes. Beaucoup d’entre elles sont assez utiles pour
être employées couramment. La note API qui suit présente les méthodes qui nous semblent les plus
intéressantes pour le programmeur.
INFO
Vous rencontrerez ces notes API dans tout l’ouvrage. Elles vous aideront à comprendre l’interface de programmation Java, ou API (Application Programming Interface). Chaque note API commence par le nom d’une classe,
tel que java.lang.String — la signification du nom de package java.lang sera expliquée au Chapitre 4. Le
nom de la classe est suivi des noms, explications et descriptions des paramètres d’une ou plusieurs méthodes de
cette classe.
Nous ne donnons pas la liste de toutes les méthodes d’une classe donnée, mais seulement celles qui sont utilisées le
plus fréquemment, décrites sous une forme concise. Consultez la documentation en ligne si vous désirez obtenir une
liste complète des méthodes d’une classe.
Nous indiquons également le numéro de version dans laquelle une classe particulière a été introduite. Si une
méthode a été ajoutée par la suite, elle présente un numéro de version distinct.
java.util.HashSet
•
HashSet()
java.lang.String 1.0
•
char charAt(int index)
Renvoie l’unité de code située à la position spécifiée. Vous ne voudrez probablement pas appeler
cette méthode à moins d’être intéressé par les unités de code de bas niveau.
•
int codePointAt(int index) 5.0
Renvoie le point de code qui démarre ou se termine à l’emplacement spécifié.
•
int offsetByCodePoints(int startIndex, int cpCount) 5.0
Renvoie l’indice du point de code d’où pointe cpCount, depuis le point de code jusqu’à startIndex.
•
int compareTo(String other)
Livre Java .book Page 67 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
67
Renvoie une valeur négative si la chaîne se trouve avant other (dans l’ordre alphabétique), une
valeur positive si la chaîne se trouve après other ou un 0 si les deux chaînes sont identiques.
•
boolean endsWith(String suffix)
Renvoie true si la chaîne se termine par suffix.
•
boolean equals(Object other)
Renvoie true si la chaîne est identique à other.
•
boolean equalsIgnoreCase(String other)
Renvoie true si la chaîne est identique à other, sans tenir compte de la casse.
•
•
•
•
int indexOf(String str)
int indexOf(String str, int fromIndex)
int indexOf(int cp)
int indexOf(int cp, int fromIndex)
Renvoient la position de départ de la première sous-chaîne égale à str ou au point de code cp,
en commençant par la position 0 ou par fromIndex ou –1 si str n’apparaît pas dans cette chaîne.
•
•
•
•
int lastIndexOf(String str)
int lastIndexOf(String str, int fromIndex)
int lastIndexOf(int cp)
int lastIndexOf(int cp, int fromIndex)
Renvoient la position de départ de la dernière sous-chaîne égale à str ou au point de code cp, en
commençant à la fin de la chaîne ou par fromIndex.
•
int length()
Renvoie la taille (ou longueur) de la chaîne.
•
int codePointCount(int startIndex, int endIndex) 5.0
Renvoie le nombre de points de code entre startIndex et endIndex - 1. Les substitutions sans
paires sont considérées comme des points de code.
•
String replace(CharSequence oldString, CharSequence newString)
Renvoie une nouvelle chaîne, obtenue en remplaçant tous les caractères oldString de la chaîne
par les caractères newString. Vous pouvez fournir des objets String ou StringBuilder pour
les paramètres CharSequence.
•
boolean startsWith(String prefix)
Renvoie true si la chaîne commence par prefix.
•
•
String substring(int beginIndex)
String substring(int beginIndex, int endIndex)
Renvoient une nouvelle chaîne composée de toutes les unités de code situées entre beginIndex
et, soit la fin de la chaîne, soit endIndex - 1.
•
String toLowerCase()
Renvoie une nouvelle chaîne composée de tous les caractères de la chaîne d’origine, mais dont
les majuscules ont été converties en minuscules.
•
String toUpperCase()
Renvoie une nouvelle chaîne composée de tous les caractères de la chaîne d’origine, mais dont
les minuscules ont été converties en majuscules.
Livre Java .book Page 68 Jeudi, 25. novembre 2004 3:04 15
68
•
Au cœur de Java 2 - Notions fondamentales
String trim()
Renvoie une nouvelle chaîne en éliminant tous les espaces qui auraient pu se trouver devant ou
derrière la chaîne d’origine.
Lire la documentation API en ligne
Vous avez vu que la classe String comprend quantité de méthodes. Il existe de plus des centaines
de classes dans les bibliothèques standard, avec bien d’autres méthodes encore. Il est impossible de
mémoriser toutes les classes et méthodes utiles. Il est donc essentiel que vous puissiez facilement
consulter la documentation API en ligne concernant les classes et méthodes de la bibliothèque standard. La documentation API fait partie du JDK. Elle est au format HTML. Pointez votre navigateur
Web sur le sous-répertoire docs/api/index.html de votre installation JDK. Vous verrez apparaître
un écran comme celui de la Figure 3.2.
Figure 3.2
Les trois panneaux
de la documentation
API.
L’écran est divisé en trois fenêtres. Une petite fenêtre en haut à gauche affiche tous les packages
disponibles. Au-dessous, une fenêtre plus grande énumère toutes les classes. Cliquez sur un nom
de classe pour faire apparaître la documentation API pour cette classe dans la fenêtre de droite
(voir Figure 3.3). Par exemple, pour obtenir des informations sur les méthodes de la classe
String, faites défiler la deuxième fenêtre jusqu’à ce que le lien String soit visible, puis cliquez
dessus.
Faites défiler la fenêtre de droite jusqu’à atteindre le résumé de toutes les méthodes, triées par ordre
alphabétique (voir Figure 3.4). Cliquez sur le nom d’une méthode pour afficher sa description
détaillée (voir Figure 3.5). Par exemple, si vous cliquez sur le lien compareToIgnoreCase, vous
obtiendrez la description de la méthode compareToIgnoreCase.
Livre Java .book Page 69 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
Figure 3.3
Description
de la classe String.
Figure 3.4
Liste des méthodes
de la classe String.
ASTUCE
Affectez dès maintenant un signet à la page docs/api/index.html dans votre navigateur.
69
Livre Java .book Page 70 Jeudi, 25. novembre 2004 3:04 15
70
Au cœur de Java 2 - Notions fondamentales
Figure 3.5
Description détaillée
d’une méthode
de la classe String.
Entrées et sorties
Pour rendre nos programmes d’exemple plus intéressants, il faut accepter les saisies et mettre correctement en forme le programme. Bien entendu, les langages de programmation modernes utilisent
une interface graphique pour recueillir la saisie utilisateur. Mais la programmation de cette interface
exige plus d’outils et de techniques que ce que nous avons à disposition pour le moment. L’intérêt
pour l’instant étant de se familiariser avec le langage de programmation Java, nous allons nous
contenter de notre humble console pour l’entrée et la sortie. La programmation des interfaces
graphiques est traitée aux Chapitres 7 à 9.
Lire les caractères entrés
Vous avez pu constater combien il était simple d’afficher une sortie sur l’unité de "sortie standard"
(c’est-à-dire la fenêtre de la console) en appelant System.out.println. Bizarrement, avant le JDK
5.0, il n’existait aucune méthode commode pour lire des entrées depuis la fenêtre de la console.
Heureusement, cette situation vient d’être rectifiée.
La lecture d’une entrée au clavier se fait en construisant un Scanner attaché sur l’unité "d’entrée
standard" System.in.
Scanner in = new Scanner(System.in);
Les diverses méthodes de la classe Scanner permettent ensuite de lire les entrées. Par exemple, la
méthode nextLine lit une ligne saisie :
System.out.print("What is your name?");
String name = in.nextLine();
Livre Java .book Page 71 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
71
Ici, nous utilisons la méthode nextLine car la saisie pourrait contenir des espaces. Pour lire un seul
mot (délimité par des espaces), appelez
String firstName = in.next();
Pour lire un entier, utilisez la méthode nextInt :
System.out.print("How old are you? ");
int age = in.nextInt();
De même, la méthode nextDouble lit le prochain chiffre à virgule flottante.
Le programme de l’Exemple 3.2 demande le nom de l’utilisateur et son âge, puis affiche un message
du style
Hello, Cay. Next year, you’ll be 46
Enfin, ajoutez la ligne
import java.util.*;
au début du programme. La classe Scanner est définie dans le package java.util. Dès que vous
utilisez une classe qui n’est pas définie dans le package de base java.lang, vous devez utiliser une
directive import. Nous étudierons les packages et les directives import plus en détail au Chapitre 4.
Exemple 3.2 : InputTest.java
import java.util.*;
public class InputTest
{
public static void main(String[] args)
{
Scanner in = new Scanner(System.in);
// récupérer la première entrée
System.out.print("What is your name? ");
String name = in.nextLine();
// récupérer la seconde entrée
System.out.print("How old are you? ");
int age = in nextInt();
// afficher la sortie à la console
System.out.println("Hello, " + name +
". Next year, you’ll be " + (age + 1));
}
}
INFO
Si vous n’utilisez pas le JDK 5.0 ou version supérieure, votre travail sera un peu plus difficile. La méthode la plus
simple consiste à utiliser une boîte de dialogue de saisie (voir Figure 3.6) :
String input = JOptionPane.showInputDialog(promptString);
La valeur de retour est la chaîne tapée par l’utilisateur.
Voici, par exemple, comment demander le nom de l’utilisateur :
String input = JOptionPane.showInputDialog("What is your name?");
Livre Java .book Page 72 Jeudi, 25. novembre 2004 3:04 15
72
Au cœur de Java 2 - Notions fondamentales
La lecture d’un nombre demande un travail supplémentaire. La méthode JOptionPane.showInputDialog
renvoie une chaîne au lieu d’un nombre. La chaîne est convertie en valeur numérique à l’aide de la méthode
Integer.parseInt ou Double.parseDouble. Par exemple :
String input = JOptionPane.showInputDialog("How old are you?");
int age = Integer.parseInt(input);
Si l’utilisateur tape 45, la variable chaîne input prend la valeur de la chaîne "45". La méthode Integer.parseInt
convertit la chaîne en sa valeur numérique, le nombre 45.
La classe JOptionPane est définie dans le package javax.swing, vous devez donc ajouter l’instruction
import javax.swing.*;
Enfin, chaque fois que votre programme appelle JOptionPane.showInputDialog, vous devez le terminer par
un appel à System.exit(0). La raison est un peu technique. L’affichage d’une boîte de dialogue démarre un
nouveau thread de contrôle. Lors de la sortie de la méthode main, le nouveau thread ne se termine pas automatiquement. Pour terminer tous les threads, vous devez appeler la méthode System.exit (pour plus d’informations
sur les threads, consultez le Chapitre 1 de Au cœur de Java 2 Volume 2). Le programme suivant est l’équivalent de
l’Exemple 3.2 avant le JDK 5.0 :
import javax.swing.*;
public class InputTest
{
public static void main(String[] args)
{
String name = JOptionPane.showInputDialog
("What is your name?");
String input = JOptionPane.showInputDialog
("How old are you?");
int age = Integer.parseInt(input);
System.out.println("Hello, " + name +
". Next year, you’ll be " + (age + 1));
System.exit(0);
}
}
Figure 3.6
Une boîte de dialogue
de saisie.
java.util.Scanner 5.0
•
Scanner(InputStream in)
Construit un objet Scanner à partir du flux de saisie donné.
•
String nextLine()
Lit la prochaine ligne saisie.
•
String next()
Lit le prochain mot saisi (délimité par une espace).
•
•
int nextInt()
double nextDouble()
Livre Java .book Page 73 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
73
Lisent et transforment la prochaine suite de caractères qui représente un entier ou un nombre à
virgule flottante.
•
boolean hasNext()
Teste s’il y a un autre mot dans la saisie.
•
•
boolean hasNextInt()
boolean hasNextDouble()
Testent si la prochaine suite de caractères représente un entier ou un nombre à virgule flottante.
javax.swing.JOptionPane 1.2
• static String showInputDialog(Object message)
Affiche une boîte de dialogue avec un message d’invite, un champ de saisie et les boutons "OK"
et "Cancel" (Annuler). La méthode renvoie la chaîne entrée par l’utilisateur.
java.lang.System 1.0
• static void exit(int status)
Arrête la machine virtuelle et passe le code de status au système d’exploitation. Par convention, un code non zéro signale une erreur.
Mise en forme de l’affichage
L’instruction System.out.print(x) permet d’afficher un nombre x à la console. Cette instruction
affichera x avec le maximum de chiffres différents de zéro (pour le type donné). Par exemple,
double x = 10000.0 / 3.0;
System.out.print(x);
affiche
3333.3333333333335
Cela pose un problème si vous désirez afficher, par exemple, des euros et des centimes.
Avant le JDK 5.0, l’affichage des nombres posait quelques problèmes. Heureusement, cette nouvelle
version a rapatrié la vénérable méthode printf de la bibliothèque C. Par exemple, l’appel
System.out.printf("%8.2f", x);
affiche x avec une largeur de champ de 8 caractères et une précision de 2 caractères. En fait, l’affichage contient une espace préalable et les sept caractères
3333.33
Vous pouvez fournir plusieurs paramètres à printf, et notamment
System.out.printf("Hello, %s. Next year, you’ll be %d", name, age);
Chacun des spécificateurs de format qui commencent par le caractère % est remplacé par l’argument
correspondant. Le caractère de conversion qui termine un spécificateur de format indique le type de
Livre Java .book Page 74 Jeudi, 25. novembre 2004 3:04 15
74
Au cœur de Java 2 - Notions fondamentales
la valeur à mettre en forme : f est un nombre à virgule flottante, s une chaîne et d une valeur décimale.
Le Tableau 3.5 montre tous les caractères de conversion.
Tableau 3.5 : Conversions pour printf
Caractère de
conversion
Type
Exemple
d
Entier décimal
159
x
Entier hexadécimal
9f
o
Entier octal
237
f
Virgule fixe, virgule flottante
15.9
e
Virgule flottante exponentielle
1.59e+01
g
Virgule flottante générale (le plus court entre e et f)
a
Virgule flottante hexadécimale
0x1.fccdp3
s
Chaîne
Hello
c
Caractère
H
b
Valeur booléenne
true
h
Code de hachage
42628b2
tx
Date et heure
Voir Tableau 3.7
%
Symbole du pourcentage
%
n
Séparateur de ligne fonction de la plate-forme
Vous pouvez également spécifier des drapeaux qui contrôleront l’apparence du résultat mis en
forme. Le Tableau 3.6 énumère les drapeaux. Par exemple, le drapeau virgule ajoute des séparateurs
de groupe. Ainsi,
System.out.printf("%,.2f", 1000.0 / 3.0);
affiche
3,333.33
Vous pouvez afficher plusieurs drapeaux, par exemple "%,(.2f", pour utiliser des séparateurs de
groupe et inclure les nombres négatifs entre parenthèses.
INFO
Vous pouvez utiliser la conversion s pour mettre en forme des objets arbitraires. Lorsqu’un objet arbitraire implémente l’interface Formattable, la méthode formatTo de l’objet est appelée. Dans le cas contraire, c’est la
méthode toString qui est appelée pour transformer l’objet en chaîne. Nous traiterons de la méthode toString
au Chapitre 5 et des interfaces au Chapitre 6.
Livre Java .book Page 75 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
75
Tableau 3.6 : Drapeaux pour printf
Drapeau
Rôle
Exemple
+
Affiche le signe des nombres positifs et négatifs.
+3333.33
espace
Ajoute une espace avant les nombres positifs.
| 3333.33|
0
Ajoute des zéros préalables.
003333.33
-
Justifie le champ à gauche.
|3333.33|
(
Entoure le nombre négatif de parenthèses.
(3333.33)
,
Ajoute des séparateurs de groupe.
3,333.33
# (pour format f)
Inclut toujours une décimale.
3,333
# (pour format x ou o)
Ajoute le préfixe 0x ou 0.
0xcafe
^
Transforme en majuscules.
0XCAFE
$
Indique l’indice de l’argument à mettre en forme ; par
exemple, %1$d %1$x affiche le premier argument en décimal et hexadécimal.
159 9F
<
Met en forme la même valeur que la spécification précédente ; par exemple, %d %<x affiche le même nombre en
décimal et hexadécimal.
159 9F
Vous pouvez utiliser la méthode statique String.format pour créer une chaîne mise en forme sans
l’afficher :
String message = String.format("Hello, %s. Next year, you’ll be %d", name, age);
Même si nous ne décrirons pas le type Date en détail avant le Chapitre 4, pour être complets, voyons
brièvement les options de mise en forme de la date et de l’heure de la méthode printf. On utilise un
format à deux lettres commençant par t et se terminant par l’une des lettres du Tableau 3.7. Par exemple,
System.out.printf("%tc", new Date());
affiche la date et l’heure courantes au format
Mon Feb 09 18:05:19 PST 2004
Tableau 3.7 : Caractères de conversion de la date et de l’heure
Caractère de
conversion
Type
Exemple
C
Date et heure complètes
Mon Feb 09
18:05:19 PST 2004
F
Date au format ISO 8601
2004-02-09
D
Date au format américain (mois/jour/année)
02/09/2004
T
Heure sur 24 heures
18:05:19
r
Heure sur 12 heures
06:05:19 pm
R
Heure sur 24 heures sans secondes
18:05
Livre Java .book Page 76 Jeudi, 25. novembre 2004 3:04 15
76
Au cœur de Java 2 - Notions fondamentales
Tableau 3.7 : Caractères de conversion de la date et de l’heure (suite)
Caractère de
conversion
Type
Exemple
Y
Année sur quatre chiffres (avec zéros préalables, le cas
échéant)
2004
y
Deux derniers chiffres de l’année (avec zéro préalable,
le cas échéant)
04
C
Deux premiers chiffres de l’année (avec zéro préalable,
le cas échéant)
20
B
Nom complet du mois
February
b ou h
Nom du mois abrégé
Feb
m
Mois sur deux chiffres (avec zéro préalable, le cas échéant)
02
d
Jour sur deux chiffres (avec zéro préalable, le cas échéant)
09
e
Jour sur deux chiffres (avec zéro préalable, le cas échéant)
9
A
Jour de la semaine complet
Monday
a
Jour de la semaine abrégé
Mon
j
Jour de l’année sur trois chiffres (avec zéros préalables,
le cas échéant), entre 001 et 366
069
H
Heure sur deux chiffres (avec zéro préalable, le cas
échéant), entre 00 et 23
18
k
Heure sur deux chiffres (sans zéro préalable), entre 0 et 23
18
I
Heure sur deux chiffres (avec zéro préalable, le cas
échéant), entre 01 et 12
06
l
Heure sur deux chiffres (sans zéro préalable), entre 1 et 12
6
M
Minutes sur deux chiffres (avec zéro préalable, le cas échéant)
05
S
Secondes sur deux chiffres (avec zéro préalable, le cas
échéant)
19
L
Millièmes de seconde, sur trois chiffres (avec zéros
préalables, le cas échéant)
047
N
Nanosecondes sur neuf chiffres (avec zéros préalables,
le cas échéant)
047000000
P
Indicateur du matin ou de l’après-midi en majuscules
PM
p
Indicateur du matin ou de l’après-midi en minuscules
pm
z
Décalage numérique RFC 822 du GMT
-0800
Z
Fuseau horaire
PST
s
Secondes depuis le 1970-01-01 00:00:00 GMT
1078884319
E
Millièmes de seconde depuis le 1970-01-01 00:00:00 GMT
1078884319047
Livre Java .book Page 77 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
77
Comme vous pouvez le voir au Tableau 3.7, certains des formats produisent une partie seulement
d’une date donnée, par exemple le jour ou le mois. Il serait pourtant assez stupide de fournir la date
plusieurs fois pour la mettre totalement en forme. C’est pourquoi une chaîne peut indiquer l’indice
de l’argument à mettre en forme. L’indice doit suivre immédiatement le % et se terminer par un $.
Par exemple,
System.out.printf("%1$s %2$tB %2$te, %2$tY", "Due date:", new Date());
affiche
Due date: February 9, 2004
Vous pouvez aussi utiliser le drapeau <. Il indique qu’il faut utiliser l’argument avec le format qui
précède. Ainsi, l’instruction
System.out.printf("%s %tB %<te, %<tY", "Due date:", new Date());
produit le même résultat que l’instruction précédente.
ATTENTION
Les valeurs d’indice d’argument commencent à 1, et non à 0 : %1$... met en forme le premier argument. Ceci évite
la confusion avec le drapeau 0.
Vous avez maintenant vu toutes les fonctionnalités de la méthode printf. La Figure 3.7 présente un
diagramme syntaxique des spécificateurs de mise en forme.
format-specifier:
conversion
character
%
argument
index
$
flag
width
.
precision
t
conversion
character
Figure 3.7
Syntaxe du spécificateur de mise en forme.
INFO
Plusieurs règles de mise en forme sont spécifiques aux paramètres régionaux. En France, par exemple, le séparateur
des dizaines est un point, et non une virgule, et Monday devient lundi. Vous verrez au Volume 2 comment modifier
le comportement de vos applications en fonction des pays.
ASTUCE
Si vous utilisez une version de Java préalable au JDK 5.0, utilisez les classes NumberFormat et DateFormat au lieu
de printf.
Livre Java .book Page 78 Jeudi, 25. novembre 2004 3:04 15
78
Au cœur de Java 2 - Notions fondamentales
Flux d’exécution
Comme tout langage de programmation, Java gère les instructions conditionnelles et les boucles afin de
contrôler le flux d’exécution (ou flux de contrôle). Nous commencerons par étudier les instructions
conditionnelles avant de passer aux boucles. Nous terminerons par l’instruction switch, qui peut être
employée lorsque vous devez tester les nombreuses valeurs possibles d’une même expression.
INFO C++
Les constructions du flux d’exécution de Java sont comparables à celles de C et de C++, à deux exceptions près.
Il n’existe pas d’instruction goto, mais une version "étiquetée" de break peut être employée pour sortir d’une
boucle imbriquée (là où vous auriez peut-être employé goto dans un programme C). Enfin, le JDK 5.0 ajoute une
variante à la boucle for qui n’a pas son pareil en C ou C++. Elle est identique à la boucle foreach du C#.
Portée d’un bloc
Avant d’examiner les structures de contrôle, vous devez savoir ce qu’est un bloc.
Un bloc, ou instruction composée, est un groupe d’instructions simples délimité par une paire
d’accolades. Les blocs déterminent la portée des variables. Ils peuvent être imbriqués à l’intérieur
d’un autre bloc. Voici un bloc imbriqué dans le bloc de la méthode main :
public static void main(String[] args)
{
int n;
. . .
{
int k;
. . .
} // k n’est défini que jusqu’ici
}
Précisons qu’il n’est pas possible de déclarer des variables homonymes dans deux blocs imbriqués.
Dans l’exemple suivant, la seconde déclaration de n est une erreur et le programme ne peut pas être
compilé :
public static void main(String[] args)
{
int n;
. . .
{
int k;
int n; // erreur--impossible de redéfinir n dans un bloc imbriqué
. . .
}
}
INFO C++
En C++, il est possible de redéfinir une variable à l’intérieur d’un bloc imbriqué. La définition la plus interne cache
alors la définition externe. Cela constitue une source d’erreur, et Java ne l’autorise pas.
Livre Java .book Page 79 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
79
Instructions conditionnelles
L’instruction conditionnelle en Java prend la forme :
if (condition) instruction
La condition doit être incluse entre parenthèses. En Java, comme dans la plupart des langages de
programmation, vous souhaiterez souvent exécuter plusieurs instructions lorsqu’une condition est
vraie. Dans ce cas, vous utiliserez un bloc d’instructions qui prend la forme :
{
instruction1
instruction2
. . .
}
Par exemple :
if (yourSales >= target)
{
performance = "Satisfactory";
bonus = 100;
}
Dans cet extrait de code, toutes les instructions qui se trouvent à l’intérieur des accolades seront
exécutées lorsque la valeur de yourSales sera supérieure à la valeur de target (voir Figure 3.8).
Figure 3.8
Organigramme
de l’instruction if.
NO
yourSales target
YES
performance
= Satisfactory
bonus=100
INFO
Un bloc (parfois appelé instruction composée) permet de regrouper plus d’une instruction (simple) dans une structure de programmation Java qui, sans ce regroupement, ne pourrait contenir qu’une seule instruction (simple).
Livre Java .book Page 80 Jeudi, 25. novembre 2004 3:04 15
80
Au cœur de Java 2 - Notions fondamentales
L’instruction conditionnelle de Java a l’aspect suivant (voir Figure 3.9) :
Figure 3.9
Organigramme
de l’instruction if/else.
YES
if (condition)
yourSales target
NO
performance
=“Satisfactory”
performance
=“Unsatisfactory”
bonus=
100+0.01*
(yourSales–target)
bonus=0
instruction1
else
instruction2;
Par exemple :
if (yourSales >= target)
{
performance = "Satisfactory";
bonus = 100 + 0.01 * (yourSales - target);
}
else
{
performance = "Unsatisfactory";
bonus = 0;
}
La partie else est toujours facultative. Une directive else est toujours associée à l’instruction if la
plus proche. Par conséquent, dans l’instruction
if (x <= 0) if (x == 0) sign = 0; else sign = -1;
la directive else appartient au second if.
Des séquences répétées if . . . else if . . . sont très fréquentes (voir Figure 3.10). Par exemple :
if (yourSales >= 2 * target)
{
performance = "Excellent";
bonus = 1000;
}
else if (yourSales >= 1.5 * target)
Livre Java .book Page 81 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
{
performance = "Fine";
bonus = 500;
}
else if (yourSales >= target)
{
performance = "Satisfactory";
bonus = 100;
}
else
{
System.out.println("You’re fired");
}
yourSales 2*target
YES
performance
bonus=1000
performance
=“Fine”
bonus=500
performance
=“Satisfactory”
bonus=100
NO
yourSales 1.5*target
YES
NO
YES
yourSales target
NO
Print
“You're fired”
Figure 3.10
Organigramme de l’instruction if/else if (branchement multiple).
81
Livre Java .book Page 82 Jeudi, 25. novembre 2004 3:04 15
82
Au cœur de Java 2 - Notions fondamentales
Boucles
La boucle while exécute une instruction (qui peut être une instruction de bloc) tant qu’une condition
est vraie. Sa forme générale est la suivante :
while (condition) instruction
La boucle while ne s’exécute jamais si la condition est fausse dès le départ (voir Figure 3.11).
Figure 3.11
Organigramme
de l’instruction while.
NO
balance<goal
YES
update
balance
years++
Print years
Dans l’Exemple 3.3, nous écrivons un programme permettant de déterminer combien de temps sera
nécessaire pour économiser une certaine somme vous permettant de prendre une retraite bien méritée, en
supposant que vous déposiez chaque année une même somme d’argent à un taux d’intérêt spécifié.
Dans notre exemple, nous incrémentons un compteur et nous mettons à jour le total cumulé dans le
corps de la boucle jusqu’à ce que le total excède le montant souhaité :
while (balance < goal)
{
balance += payment;
Livre Java .book Page 83 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
83
double interest = balance * interestRate/ 100;
balance += interest;
years++;
}
System.out.println(years + " years.");
Ne vous fiez pas à ce programme pour prévoir votre retraite. Nous avons laissé de côté quelques
détails comme l’inflation et votre espérance de vie.
Le test d’une boucle while est effectué avant l’exécution du corps de la boucle. Par conséquent, ce
bloc peut ne jamais être exécuté. Si vous voulez être certain que le bloc soit exécuté au moins une
fois, vous devez placer la condition de test en fin de boucle. Pour cela, employez une boucle do/while,
dont voici la syntaxe :
do instruction while (condition);
Cette instruction exécute le bloc avant de tester la condition. Si celle-ci est fausse, le programme
réexécute le bloc avant d’effectuer un nouveau test, et ainsi de suite. Par exemple, le code de l’Exemple 3.4 calcule le nouveau solde de votre compte retraite, puis vous demande si vous êtes prêt à partir
à la retraite :
do
{
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
year++;
// afficher le solde actuel
. . .
// demander si prêt à prendre la retraite
// et récupérer la réponse
. . .
}
while (input.equals("N"));
Tant que la réponse de l’utilisateur est "N", la boucle est répétée (voir Figure 3.12). Ce programme
est un bon exemple d’une boucle devant être exécutée au moins une fois, car l’utilisateur doit
pouvoir vérifier le solde avant de décider s’il est suffisant pour assurer sa retraite.
Exemple 3.3 : Retirement.java
import java.util.*;
public class Retirement
{
public static void main(String[] args)
{
// lire les infos entrées
Scanner input = new Scanner(System.in);
System.out.print("How much do you need to retire?");
double goal = in.nextDouble();
System.out.print("How much money will you contribute every year?");
double payment = in.nextDouble();
System.out.print("Interst rate in %:");
double interestRate = in.nextDouble();
Livre Java .book Page 84 Jeudi, 25. novembre 2004 3:04 15
84
Au cœur de Java 2 - Notions fondamentales
double balance = 0;
int years = 0;
// mettre à jour le solde du compte tant que cible non atteinte
while (balance < goal)
{
// ajouter versements et intérêts de cette année
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
years++;
}
System.out.println
("You can retire in " + years + " years.");
}
}
Exemple 3.4 : Retirement2.java
import java.util.*;
public class Retirement2
{
public static void main(String[] args)
{
Scanner in = new Scanner(System.in);
System.out.print("How much money will you contribute every year?");
double payment = in.nextDouble();
System.out.print("Interest rate in %:");
double interestRate = in.nextDouble();
double balance = 0;
int year = 0;
String input;
// mettre à jour le solde du compte tant que l’utilisateur
// n’est pas prêt à prendre sa retraite
do
{
// ajouter versements et intérêts de cette année
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
year++;
// afficher le solde actuel
System.out.println("After year %d, your balance is %,.2f%n",
year, balance);
// demander si prêt pour la retraite
System.out.print("Ready to retire? (Y/N)");
input = in.next();
}
Livre Java .book Page 85 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
85
while (input.equals("N"));
}
}
Figure 3.12
Organigramme
de l’instruction do/while.
update
balance
print balance
ask “Ready
to retire?
(Y/N)”
read
input
YES
input=“N”
NO
Boucles déterminées
La boucle for est une construction très générale pour gérer une itération contrôlée par un compteur
ou une variable similaire, mis à jour après chaque itération. Comme le montre la Figure 3.13, le code
suivant affiche les nombres 1 à 10 sur l’écran :
for (int i = 1; i <= 10; i++)
System.out.println(i);
Le premier élément de l’instruction for contient généralement l’initialisation du compteur. Le
deuxième élément fournit la condition de test qui sera vérifiée avant chaque passage dans la boucle ;
le troisième indique comment le compteur doit être mis à jour.
Bien que Java, comme C++, autorise pratiquement n’importe quelle expression dans les trois
éléments d’une boucle for, une convention tacite fait que ces éléments doivent respectivement se
Livre Java .book Page 86 Jeudi, 25. novembre 2004 3:04 15
86
Au cœur de Java 2 - Notions fondamentales
contenter d’initialiser, de tester et de mettre à jour la même variable compteur. Il est possible d’écrire
des boucles très absconses si l’on ne respecte pas cette convention.
Figure 3.13
Organigramme
de l’instruction for.
i=1
i
10
NO
YES
Print i
i++
Même en suivant cette règle, de nombreuses possibilités sont offertes. Il est ainsi possible de créer
des boucles décrémentales :
for (int i = 10; i > 0; i--)
System.out.println("Counting down . . . " + i);
System.out.println("Blastoff!");
ATTENTION
Soyez prudent lorsque vous testez l’égalité de deux nombres réels. Une boucle for comme celle-ci :
for (double x = 0; x != 10; x += 0.1) . . .
risque de ne jamais se terminer. Du fait des erreurs d’arrondi, la valeur finale risque de n’être jamais atteinte. Par
exemple, dans la boucle ci-dessus, x saute de 9.99999999999998 à 10.09999999999998, puisqu’il n’existe pas de
représentation binaire exacte pour 0.1.
Livre Java .book Page 87 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
87
Lorsque vous déclarez une variable dans le premier élément d’une instruction for, la portée de cette
variable s’étend jusqu’à la fin du corps de la boucle :
for (int i = 1; i <= 10; i++)
{
. . .
}
// i n’est plus défini ici
En particulier, si vous définissez une variable à l’intérieur d’une instruction for, vous ne pouvez pas
utiliser cette variable en dehors de la boucle. En conséquence, si vous désirez utiliser la valeur finale du
compteur en dehors d’une boucle for, la variable compteur doit être déclarée en dehors de cette boucle !
int i;
for (i = 1; i <= 10; i++)
{
. . .
}
// i est toujours défini ici
En revanche, vous pouvez définir des variables de même nom dans des boucles for séparées :
for (int i = 1; i <= 10; i++)
{
. . .
}
. . .
for (int i = 11; i <= 20; i++) // ok pour redéfinir i
{
. . .
}
Bien entendu, une boucle for équivaut à une boucle while. Pour être plus précis,
for (int i = 10; i > 0; i--)
System.out.println("Counting down . . . " + i);
équivaut exactement à :
int i = 10;
while (i > 0)
{
System.out.println("Counting down . . . " + i);
i--;
}
L’Exemple 3.5 montre un exemple typique de boucle for.
Le programme calcule vos chances de gagner à une loterie. Si vous devez, par exemple, trouver 6 des
nombres de 1 à 50 pour gagner, il y a
( 50 × 49 × 48 × 47 × 46 × 45 )
-----------------------------------------------------------------------(1 × 2 × 3 × 4 × 5 × 6)
tirages possibles, et vous avez une chance sur 15 890 700. Bonne chance !
En général, si vous choisissez k nombres parmi n, il y a
n × ( n – 1 ) × ( n – 1 ) × ... × ( n – k + 1 )
-------------------------------------------------------------------------------------------1 × 2 × 3 × ... × k
tirages possibles. La boucle for qui suit calcule cette valeur :
int lotteryOdds = 1;
for (int i = 1; i <= k; i++)
lotteryOdds = lotteryOdds * (n - i + 1) / i;
Livre Java .book Page 88 Jeudi, 25. novembre 2004 3:04 15
88
Au cœur de Java 2 - Notions fondamentales
INFO
Voir un peu plus loin pour obtenir une description de la boucle for généralisée (aussi appelée boucle "for each")
ajoutée au JDK 5.0.
Exemple 3.5 : LotteryOdds.java
import java.swing.*;
public class LotteryOdds
{
public static void main(String[] args)
{
Scanner in = new Scanner(System.in);
System.out.print("How many numbers do you need to draw? ");
int k = in.nextInt();
System.out.print("What is the highest number you can draw? ");
int n = in.nextInt();
/*
calculer le binôme
n * (n - 1) * (n - 2) * . . . * (n - k + 1)
------------------------------------------1 * 2 * 3 * . . . * k
*/
int lotteryOdds = 1;
for (int i = 1; i <= k; i++)
lotteryOdds = lotteryOdds * (n - i + 1) / i;
System.out.println
("Your odds are 1 in " + lotteryOdds + ". Good luck!");
}
}
Sélections multiples — l’instruction switch
La construction if/else peut se révéler assez lourde quand vous devez traiter plusieurs sélections et
de multiples alternatives. Java dispose de l’instruction switch qui reproduit exactement celle de C et
C++, y compris ses défauts.
Par exemple, si vous créez un système de menu ayant quatre alternatives, comme celui de la
Figure 3.14, vous pouvez utiliser un code comparable à celui-ci :
Scanner in = new Scanner(System.in);
System.out.print("Select an option (1, 2, 3, 4)");
int choice = in.nextInt();
switch (choice)
{
case 1:
. . .
break;
case 2:
. . .
break;
case 3:
. . .
break;
case 4:
. . .
break;
default:
Livre Java .book Page 89 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
// entrée incorrecte
. . .
break;
}
Figure 3.14
Organigramme
de l’instruction switch.
choice = 1
YES
. . .
YES
. . .
YES
. . .
YES
. . .
NO
choice = 2
NO
choice = 3
NO
choice = 4
NO
(default)
bad input
89
Livre Java .book Page 90 Jeudi, 25. novembre 2004 3:04 15
90
Au cœur de Java 2 - Notions fondamentales
L’exécution commence à l’étiquette case dont la valeur correspond à la valeur de sélection, puis elle
se poursuit jusqu’à une instruction break ou jusqu’à la fin du bloc switch. Si aucune correspondance n’est trouvée, la clause default est exécutée, si elle existe.
Notez que les valeurs de case doivent être des entiers ou des constantes énumérées. Il n’est pas
possible de tester des chaînes. Par exemple, le code suivant est erroné :
String input = . . .;
switch (input) // ERREUR
{
case "A": // ERREUR
. . .
break;
. . .
}
ATTENTION
Il est possible d’exécuter plusieurs instructions case. Si vous oubliez d’ajouter la clause break à la fin d’une instruction case, l’exécution se poursuit avec le bloc case qui suit ! Ce comportement est très dangereux et est une cause
commune d’erreur. C’est pourquoi nous n’utilisons pas l’instruction switch dans nos programmes.
Interrompre le flux d’exécution
Bien que les concepteurs de Java aient conservé goto en tant que mot réservé, ils ne l’ont pas inclus
dans le langage. En général, l’emploi d’instructions goto est considéré comme inélégant et maladroit. Certains programmeurs pensent néanmoins que les forces anti-goto sont allées trop loin (un
fameux article de Donald Knuth s’intitule "La programmation structurée avec des goto"). Ils avancent que si l’utilisation fréquente de goto est dangereuse, quitter immédiatement une boucle peut
parfois être utile. Les concepteurs de Java ont admis cette thèse et ont même ajouté une nouvelle
instruction pour ce mécanisme : l’interruption "étiquetée" (labeled break).
Observons d’abord l’instruction break normale. La même instruction qui est employée pour sortir
d’un bloc switch permet de quitter une boucle. Par exemple :
while (years <= 100)
{
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
if (balance >= goal) break;
years++;
}
L’exécution de programme quitte la boucle si years > 100 au début de la boucle, ou si balance
>= goal au milieu de la boucle. Bien entendu, vous auriez pu calculer la même valeur pour years
sans ajouter break, de la façon suivante :
while (years <= 100 && balance < goal)
{
balance += payment;
double interest = balance * interestRate / 100;
balance += interest;
if (balance < goal)
years++;
}
Livre Java .book Page 91 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
91
Notez toutefois que le test balance < goal est reproduit deux fois dans cette version. Pour éviter
cette redondance, certains programmeurs préfèrent l’instruction break.
Contrairement à C++, Java propose également une instruction d’interruption étiquetée permettant de
quitter des boucles imbriquées. Il arrive qu’un événement particulier survienne au sein d’une boucle
profondément imbriquée. Dans ce cas, il est souhaitable de quitter l’ensemble des boucles, et pas
seulement celle qui a vu surgir cet événement. Il ne serait pas simple de programmer cette situation
en ajoutant des conditions supplémentaires aux diverses boucles.
Nous allons présenter un exemple qui montre le fonctionnement de ce mécanisme. Remarquez que
l’étiquette doit précéder la plus externe des boucles que vous souhaitez quitter. Elle doit être suivie
de deux-points (:) :
Scanner in = new Scanner(System.in);
int n;
read_data:
while (. . .) // cette instruction de boucle est étiquetée
{
. . .
for (. . .) // cette boucle interne n’est pas étiquetée
{
System.out.print("Enter a number >= 0");
n = in.nextInt();
if (n < 0) // Ne doit pas se produire, impossible de continuer
break read_data;
// sortir de la boucle de lecture
. . .
}
}
// cette instruction est exécutée immédiatement après break
if (n < 0) // vérifier si situation anormale
{
// traiter situation anormale
}
else
{
// poursuivre le traitement normal
}
Si l’entrée est invalide, l’instruction break étiquetée saute après le bloc étiqueté. Comme avec toute
utilisation de break, il vous faut alors effectuer un test pour savoir si la boucle s’est terminée normalement ou si elle a été interrompue.
INFO
Curieusement, vous pouvez attribuer une étiquette à n’importe quelle instruction, y compris à une instruction if ou
à un bloc d’instructions. Par exemple :
étiquette:
{
. . .
if (condition) break étiquette; // sortie du bloc
. . .
}
// saut ici lors de l’exécution de l’instruction break
Livre Java .book Page 92 Jeudi, 25. novembre 2004 3:04 15
92
Au cœur de Java 2 - Notions fondamentales
Si l’instruction goto vous manque vraiment, et que vous puissiez placer un bloc qui se termine juste avant l’endroit
où vous voulez sauter, une instruction break fera l’affaire ! Cette approche n’est bien entendu pas conseillée. Notez
aussi que vous ne pouvez sauter qu’en dehors d’un bloc, jamais dans un bloc.
Il existe enfin une instruction continue qui, comme l’instruction break, interrompt le flux normal
d’exécution. L’instruction continue transfère le contrôle en tête de la boucle englobante la plus
interne. En voici un exemple :
Scanner in = new Scanner(System.in);
while (sum < goal)
{
System.out.print("Enter a number: ");
n = in.nextInt();
if (n < 0) continue;
sum += n; // n’est pas exécuté si n < 0
}
Si n < 0, l’instruction continue saute immédiatement en tête de boucle, et n’exécute pas le reste de
l’itération en cours.
Si l’instruction continue est employée dans une boucle for, elle provoque un saut vers la partie
"mise à jour" de la boucle for. Par exemple :
for (count = 1; count < 100; count++)
{
System.out.print("Enter a number, -1 to quit: ");
n = in.nextInt();
if (n < 0) continue;
sum += n; // n’est pas exécuté si n < 0
}
Si n < 0, l’instruction continue provoque un saut vers l’instruction count++.
Il existe aussi une forme étiquetée de l’instruction continue qui provoque un saut vers l’en-tête de
la boucle portant l’étiquette correspondante.
ASTUCE
De nombreux programmeurs trouvent les instructions break et continue peu claires. Ces instructions sont absolument facultatives ; vous pouvez toujours exprimer la même logique sans y avoir recours. Dans cet ouvrage, nous
n’utilisons jamais ces instructions.
Grands nombres
Si la précision des types de base entier et flottant n’est pas suffisante, vous pouvez avoir recours à des
classes très utiles du package java.math, appelées BigInteger et BigDecimal. Ces classes permettent de manipuler des nombres comprenant une longue séquence arbitraire de chiffres. La classe
BigInteger implémente une arithmétique de précision arbitraire pour les entiers, et BigDecimal
fait la même chose pour les nombres à virgule flottante.
Utilisez la méthode statique valueOf pour transformer un nombre ordinaire en grand nombre :
BigInteger a = BigInteger.valueOf(100);
Livre Java .book Page 93 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
93
Il n’est malheureusement pas possible d’utiliser les opérateurs mathématiques habituels tels que + et
* pour combiner des grands nombres. Vous devez, à la place, avoir recours à des méthodes telles que
add et multiply dans les classes des grands nombres :
BigInteger c = a.add(b); // c = a + b
BigInteger d = c.multiply(b.add(BigInteger.valueOf(2))); // d = c * (b + 2)
INFO C++
Contrairement à C++, Java ne prévoit pas de surcharge programmable des opérateurs. Il n’est pas possible au
programmeur de la classe BigInteger de redéfinir les opérateurs + et * pour leur attribuer les opérations add et
multiply des classes BigInteger. Les concepteurs ont en fait surchargé l’opérateur + pour indiquer la concaténation de chaînes, mais ils ont choisi de ne pas surcharger les autres opérateurs, sans donner au programmeur la possibilité de le faire lui-même.
L’Exemple 3.6 montre le programme lotteryOdds de l’Exemple 3.5 modifié, afin de fonctionner
avec les grands nombres. Par exemple, si vous êtes invité à participer à une loterie pour laquelle vous
devez choisir 60 nombres parmi 490 possibles, ce programme vous dira que vous avez une chance sur
716395843461995557415116222540092933411717612789263493493351013459481104668848.
Bonne chance !
Le programme de l’Exemple 3.5 comprenait l’instruction de calcul suivante :
lotteryOdds = lotteryOdds * (n - i + 1) / i;
Avec l’utilisation des grands nombres, l’instruction équivalente devient :
lotteryOdds = lotteryOdds.multiply(BigInteger.valueOf(n - i + 1))
.divide(BigInteger.valueOf(i));
Exemple 3.6 : BigIntegerTest.java
import java.math.*;
import java.util.*;
public class BigIntegerTest
{
public static void main(String[] args)
{
Scanner in = new Scanner(System.in);
System.out.print("How many numbers do you need to draw? ");
int k = in.nextInt();
System.out.print("What is the highest number you can draw? ");
int n = in.nextInt();
/*
Calculer le binôme
n * (n - 1) * (n - 2) * . . . * (n - k + 1)
------------------------------------------1 * 2 * 3 * . . . * k
*/
BigInteger lotteryOdds = BigInteger.valueOf(1);
Livre Java .book Page 94 Jeudi, 25. novembre 2004 3:04 15
94
Au cœur de Java 2 - Notions fondamentales
for (int i = 1; i <= k; i++)
lotteryOdds = lotteryOdds
.multiply(BigInteger.valueOf(n - i + 1))
.divide(BigInteger.valueOf(i));
System.out.println("Your odds are 1 in " + lotteryOdds +
". Good luck!");
}
}
java.math.BigInteger 1.1
•
•
•
•
•
BigInteger add(BigInteger other)
BigInteger subtract(BigInteger other)
BigInteger multiply(BigInteger other)
BigInteger divide(BigInteger other)
BigInteger mod(BigInteger other)
Renvoient respectivement la somme, la différence, le produit, le quotient et le reste de BigInteger
et de other.
•
int compareTo(BigInteger other)
Renvoie 0 si BigInteger est égal à other, un résultat négatif s’il est inférieur à other et un
résultat positif sinon.
•
static BigInteger valueOf(long x)
Renvoie un grand entier dont la valeur est égale à x.
java.math.BigDecimal 1.1
•
•
•
•
BigDecimal add(BigDecimal other)
BigDecimal subtract(BigDecimal other)
BigDecimal multiply(BigDecimal other)
BigDecimal divide(BigDecimal other, roundingMode mode) 5.0
Renvoient respectivement la somme, la différence, le produit ou le quotient de BigDecimal et de
other. Pour calculer le quotient, vous devez fournir un mode d’arrondi. Le mode RoundingMode.HALF_UP est celui que vous avez étudié à l’école (c’est-à-dire arrondi au chiffre inférieur si
0 à 4, arrondi au chiffre supérieur si 5 à 9). Cela convient pour les calculs de routine. Consultez
la documentation API en ce qui concerne les autres modes d’arrondi.
•
int compareTo(BigDecimal other)
Renvoie 0 si BigDecimal est égal à other, un résultat négatif s’il est inférieur à other et un
résultat positif sinon.
•
•
static BigDecimal valueOf(long x)
static BigDecimal valueOf(long x, int scale)
Renvoient un grand décimal dont la valeur est égale à x or x/10scale.
Tableaux
Un tableau est une structure de données qui stocke une série de valeurs du même type. Vous accédez
à chaque valeur individuellement à l’aide d’un entier indice. Par exemple, si a est un tableau
d’entiers, a[i] est le ième entier du tableau.
Livre Java .book Page 95 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
95
Vous déclarez une variable tableau en spécifiant son type — qui est le type d’élément suivi de [] —
et le nom de la variable tableau. Voici, par exemple, la déclaration d’un tableau a d’entiers :
int[] a;
Cette instruction ne déclare toutefois que la variable a. Elle n’initialise pas a comme un tableau.
L’opérateur new crée le tableau
int[] a = new int[100];
Cette instruction crée un tableau qui peut stocker 100 entiers.
INFO
Vous pouvez définir une variable de tableau soit sous la forme
int[] a;
soit sous la forme
int a[];
La plupart des programmeurs Java préfèrent le premier style car il sépare nettement le type int[] (tableau
d’entiers) du nom de la variable.
Les éléments du tableau sont numérotés de 0 à 99 (et non de 1 à 100). Une fois que le tableau est
créé, vous pouvez remplir ses éléments, par exemple à l’aide d’une boucle :
int[] a = new int[100];
for (int i = 0; i < 100; i++)
a[i] = i; // remplit le tableau avec les valeurs de 0 à 99
ATTENTION
Si vous construisez un tableau de 100 éléments et que vous essayiez d’accéder à l’élément a[100] (ou à tout autre
indice en dehors de la plage 0 à 99), votre programme se terminera avec une exception "array index out of bounds"
(indice de tableau hors limites).
Vous pouvez trouver le nombre des éléments d’un tableau à l’aide de nomTableau.length. Par
exemple,
for (int i = 0; i < a.length; i++)
System.out.println(a[i]);
Une fois un tableau créé, vous ne pouvez pas modifier sa taille (mais vous pouvez changer un
élément individuel du tableau). Si vous devez modifier souvent la taille d’un tableau pendant
l’exécution d’un programme, vous pouvez avoir recours à une structure de données différente appelée
liste de tableaux (voir le Chapitre 5 pour plus d’informations à ce sujet).
La boucle "for each"
Le JDK 5.0 a introduit une construction de boucle performante qui vous permet de parcourir chaque
élément d’un tableau (ainsi que d’autres collections d’éléments) sans avoir à vous préoccuper des
valeurs d’indice.
Livre Java .book Page 96 Jeudi, 25. novembre 2004 3:04 15
96
Au cœur de Java 2 - Notions fondamentales
La boucle for améliorée
for (variable : collection) instruction
définit la variable donnée sur chaque élément de la collection, puis exécute l’instruction (qui, bien
sûr, peut être un bloc). L’expression collection doit être un tableau ou un objet d’une classe qui
implémente l’interface Iterable, comme ArrayList. Nous verrons les listes de tableaux au Chapitre 5 et l’interface Iterable au Chapitre 2 du Volume 2.
Par exemple,
for (int element : a)
System.out.println(element);
affiche chaque élément du tableau a sur une ligne séparée.
Il est conseillé de lire cette boucle sous la forme "pour chaque élément dans a". Les concepteurs du
langage Java ont envisagé d’utiliser des mots clés comme foreach et in. Mais cette boucle a été
ajoutée avec un peu de retard au langage Java et, au final, personne n’a voulu casser un ancien code
qui contenait déjà des méthodes ou des variables avec les mêmes noms (comme System.in).
Bien entendu, vous pourriez obtenir le même effet avec une boucle for traditionnelle :
for (int i = 0; i < a.length; i++)
System.out.println(a[i]);
Toutefois, la boucle "for each" est plus concise et moins sujette à erreur (vous n’avez pas à vous
inquiéter des valeurs d’indice de début et de fin, qui sont souvent pénibles).
INFO
La variable loop de la boucle "for each" parcourt les éléments d’un tableau, et non les valeurs d’indice.
La boucle "for each" est une amélioration agréable de la boucle traditionnelle si vous devez traiter
tous les éléments d’une collection. Il y a toutefois de nombreuses opportunités d’utiliser la boucle
for traditionnelle. Vous ne voudrez peut-être pas, par exemple, parcourir la totalité de la collection
ou pourriez avoir besoin de la valeur d’indice à l’intérieur de la boucle.
Initialiseurs de tableaux et tableaux anonymes
Java propose un raccourci pour créer un objet tableau et l’initialiser simultanément. Voici un exemple de
la syntaxe à employer :
int[] smallPrimes = { 2, 3, 5, 7, 11, 13 };
Remarquez qu’il n’est pas nécessaire d’appeler new lorsque vous utilisez cette syntaxe.
Il est même possible d’initialiser un tableau anonyme :
new int[] { 17, 19, 23, 29, 31, 37 }
Cette expression alloue un nouveau tableau et le remplit avec les valeurs spécifiées entre les accolades. Elle détermine le nombre de valeurs fournies et affecte au tableau le même nombre d’éléments.
Cette syntaxe est employée pour réinitialiser un tableau sans créer une nouvelle variable. L’exemple
smallPrimes = new int[] { 17, 19, 23, 29, 31, 37 };
Livre Java .book Page 97 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
97
est un raccourci pour
int[] anonymous = { 17, 19, 23, 29, 31, 37 };
smallPrimes = anonymous;
INFO
Il est légal d’avoir des tableaux de longueur 0. Un tel tableau peut être utile si vous écrivez une méthode qui calcule
un résultat de tableau et que ce résultat puisse être vide. Un tableau de longueur 0 se construit de la façon suivante :
new typeElément[0]
Notez qu’un tableau de longueur 0 est différent de null (voir le Chapitre 4 pour plus d’informations concernant
null).
Copie des tableaux
Il est possible de copier une variable tableau dans une autre, mais les deux variables feront alors
référence au même tableau :
int[] luckyNumbers = smallPrimes;
luckyNumbers[5] = 12; // smallPrimes[5] vaut maintenant 12
La Figure 3.15 montre le résultat. Si vous voulez effectivement copier toutes les valeurs d’un tableau
dans un autre, il faut employer la méthode arraycopy de la classe System. Sa syntaxe est la
suivante :
System.arraycopy(from, fromIndex, to, toIndex, count);
Le tableau to doit disposer de suffisamment d’espace pour contenir les éléments copiés.
Figure 3.15
Copie d’une
variable tableau.
smallPrimes =
luckyNumbers =
2
3
5
7
11
12
Par exemple, les instructions suivantes créent deux tableaux, puis copient les quatre derniers
éléments du premier tableau dans le second tableau. La copie débute à la position 2 du tableau
source ; quatre éléments sont copiés en partant de la position 3 du tableau cible. Le résultat est donné
à la Figure 3.16 :
int[] smallPrimes = {2, 3, 5, 7, 11, 13};
int[] luckyNumbers = {1001, 1002, 1003, 1004, 1005, 1006, 1007};
System.arraycopy(smallPrimes, 2, luckyNumbers, 3, 4);
for (int i = 0; i < luckyNumbers.length; i++)
System.out.println(i + ": " + luckyNumbers[i]);
On obtient en sortie :
0:
1:
2:
3:
1001
1002
1003
5
Livre Java .book Page 98 Jeudi, 25. novembre 2004 3:04 15
98
Au cœur de Java 2 - Notions fondamentales
4: 7
5: 11
6: 13
Figure 3.16
smallPrimes =
Copie des valeurs
vers un tableau.
luckyNumbers =
2
3
5
7
11
13
1001
1002
1003
5
7
11
13
INFO C++
Un tableau Java est assez différent d’un Tableau C/C++ dans la pile (stack). Il peut cependant être comparé à un pointeur
sur un tableau alloué dans le tas (segment heap de la mémoire). C’est-à-dire que
int[] a = new int[100]; // en Java
n’est pas la même chose que
int a[100]; // en C++
mais plutôt
int* a = new int[100]; // en C++
En Java, l’opérateur [] est prédéfini pour effectuer une vérification de limites. De plus, l’arithmétique de pointeur
n’est pas possible — vous ne pouvez pas incrémenter a pour qu’il pointe sur l’élément suivant du tableau.
Paramètres de ligne de commande
Vous avez déjà vu plusieurs exemples de tableaux Java. Chaque programme Java a une méthode
main avec un paramètre String[] args. Celui-ci indique que la méthode main reçoit un tableau de
chaînes, qui sont les arguments spécifiés sur la ligne de commande.
Examinez, par exemple, ce programme :
public class Message
{
public static void main(String[] args)
{
if (args[0].equals("-h"))
System.out.print("Hello,");
else if (args[0].equals("-g"))
System.out.print("Goodbye,");
Livre Java .book Page 99 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
99
// afficher les autres arguments de ligne de commande
for (int i = 1; i < args.length; i++)
System.out.print(" " + args[i]);
System.out.println("!");
}
}
Si le programme est appelé de la façon suivante :
java Message -g cruel world
le tableau args a le contenu suivant :
args[0]: "-g"
args[1]: "cruel"
args[2]: "world"
Le programme affiche le message :
Goodbye, cruel world!
INFO C++
Dans la méthode main d’un programme Java, le nom du programme n’est pas stocké dans le tableau args. Si, par
exemple, vous lancez le programme ainsi :
java Message -h world
à partir de la ligne de commande, args[0] vaudra "-h" et non "Message" ou "java".
Tri d’un tableau
Si vous voulez trier un tableau de nombres, utilisez une des méthodes sort de la classe Arrays :
int[] a = new int[10000];
. . .
Arrays.sort(a)
Cette méthode utilise une version adaptée de l’algorithme QuickSort qui se révèle très efficace sur la
plupart des ensembles de données. La classe Arrays fournit plusieurs autres méthodes de gestion
des tableaux ; vous trouverez leur description dans les notes API situées à la fin de cette section.
Le programme de l’Exemple 3.7 montre le fonctionnement des tableaux. Il choisit une combinaison
aléatoire de nombres pour une loterie. S’il s’agit d’une loterie où il faut choisir 6 nombres sur 49, le
programme peut afficher :
Bet the following combination. It’ll make you rich!
4
7
8
19
30
44
Pour sélectionner une telle série aléatoire de nombres, il faut d’abord remplir un tableau numbers
avec les valeurs 1, 2, . . ., n :
int[] numbers = new int[n];
for (int i = 0; i < numbers.length; i++)
numbers[i] = i + 1;
Un second tableau contient les numéros à tirer :
Livre Java .book Page 100 Jeudi, 25. novembre 2004 3:04 15
100
Au cœur de Java 2 - Notions fondamentales
int[] result = new int[k];
Nous tirons maintenant k numéros. La méthode Math.random renvoie un nombre aléatoire à virgule
flottante entre 0 (inclus) et 1 (exclu). En multipliant le résultat par n, nous obtenons un nombre aléatoire entre 0 et n - 1 :
int r = (int)(Math.random() * n);
Nous définissons le ième résultat comme le numéro de cet indice. Au départ, il s’agit simplement de
r lui-même, mais vous allez voir qu’en fait, le contenu du tableau numbers change après chaque
tirage :
result[i] = numbers[r];
Nous devons maintenant nous assurer que nous ne tirerons pas deux fois le même numéro — tous les
numéros d’un tirage doivent être différents. Nous écrasons donc numbers[r] avec le dernier numéro
du tableau et décrémentons n de 1 :
numbers[r] = numbers[n - 1];
n--;
A chaque tirage, nous extrayons en fait un indice, et non la valeur réelle. L’indice pointe sur un
tableau qui contient les valeurs qui n’ont pas encore été tirées.
Après avoir tiré k numéros, le tableau result est trié pour que la sortie soit plus parlante :
Arrays.sort(result);
for (int r : result)
System.out.println(r);
Exemple 3.7 : LotteryDrawing.java
import java.util.*;
public class LotteryDrawing
{
public static void main(String[] args)
{
Scanner in = new Scanner(System.in);
System.out.print("How many numbers do you need to draw? ");
int k = in.nextInt();
System.out.print("What is the highest number you can draw? ");
int n = in.nextInt();
// remplir un tableau avec les nombres 1 2 3 . . . n
int[] numbers = new int[n];
for (int i = 0; i < numbers.length; i++)
numbers[i] = i + 1;
// tirer k nombres et les mettre dans un second tableau
int[] result = new int[k];
for (int i = 0; i < result.length; i++)
{
// créer un indice aléatoire entre 0 et n - 1
int r = (int)(Math.random() * n);
// choisir l’élément à cet emplacement aléatoire
result[i] = numbers[r];
Livre Java .book Page 101 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
101
// déplacer le dernier élément vers l’emplac. aléatoire
numbers[r] = numbers[n - 1];
n--;
}
// imprimer le tableau trié
Arrays.sort(result);
System.out.println
("Bet the following combination It’ll make you rich!");
for (int r : result)
System.out.println(r);
}
}
java.lang.System 1.1
static void arraycopy(Object from, int fromIndex, Object to, int toIndex, int
count)
•
Copie les éléments du premier tableau dans le second.
Paramètres :
from
Un tableau de n’importe quel type (le Chapitre 5 explique
pourquoi il s’agit d’un paramètre de type Object).
fromIndex
Indice à partir duquel des éléments seront lus.
to
Tableau du même type que from.
toIndex
Premier indice vers lequel les éléments seront copiés.
count
Nombre d’éléments à copier.
java.util.Arrays 1.2
• static void sort(Type[] a)
Trie le tableau en utilisant un algorithme QuickSort adapté.
Paramètres :
•
a
Tableau de type int, long, short, char, byte, boolean,
float ou double.
static int binarySearch(Type[] a, Type v)
Utilise l’algorithme BinarySearch pour rechercher la valeur v. Si elle la trouve, la méthode
renvoie l’indice de v. Sinon elle renvoie une valeur r négative ; -r - 1 désigne la position à
laquelle v devrait être insérée pour que le tableau reste trié.
Paramètres :
•
a
Tableau trié de type int, long, short, char, byte,
boolean, float ou double.
v
Valeur de même type que les éléments de a.
static void fill(Type[] a, Type v)
Affecte la valeur de v à tous les éléments du tableau.
Paramètres :
•
a
Tableau de type int, long, short, char, byte, boolean,
float ou double.
v
Valeur de même type que les éléments de a.
static boolean equals(Type[] a, Type[] b)
Renvoie true si les tableaux ont la même longueur et les éléments d’indices correspondants
possèdent la même valeur.
Livre Java .book Page 102 Jeudi, 25. novembre 2004 3:04 15
102
Au cœur de Java 2 - Notions fondamentales
Paramètres :
a, b
float ou double.
Tableau de type int, long, short, char, byte, boolean,
Tableaux multidimensionnels
Les tableaux multidimensionnels utilisent plusieurs indices pour accéder aux éléments du tableau.
Ils sont utilisés pour les tables et autres organisations plus complexes. Vous pouvez sauter cette
section jusqu’à ce que vous ayez besoin d’un tel mécanisme de stockage.
Supposons que vous désiriez créer un tableau de nombres qui montre la manière dont un investissement de 10 000 euros croît selon divers taux d’intérêt, lorsque les intérêts sont payés annuellement
et réinvestis. Le Tableau 3.8 illustre ce scénario.
Tableau 3.8 : Accroissement d’un investissement en fonction de différents taux d’intérêt
10 %
11 %
12 %
13 %
14 %
15 %
10 000,00
10 000,00
10 000,00
10 000,00
10 000,00
10 000,00
11 000,00
11 100,00
11 200,00
11 300,00
11 400,00
11 500,00
12 100,00
12 321,00
12 544,00
12 769,00
12 996,00
13 225,00
13 310,00
13 676,31
14 049,28
14 428,97
14 815,44
15 208,75
14 641,00
15 180,70
15 735,19
16 304,74
16 889,60
17 490,06
16 105,10
16 850,58
17 623,42
18 424,35
19 254,15
20 113,57
17 715,61
18 704,15
19 738,23
20 819,52
21 949,73
23 130,61
19 487,17
20 761,60
22 106,81
23 526,05
25 022,69
26 600,20
21 435,89
23 045,38
24 759,63
26 584,44
28 525,86
30 590,23
23 579,48
25 580,37
27 730,79
30 040,42
32 519,49
35 178,76
La manière évidente de stocker cette information est un tableau à deux dimensions (ou matrice) que
nous appellerons balance.
Il est très facile de déclarer une matrice :
double[][] balance;
Comme toujours en Java, vous ne pouvez pas utiliser le tableau avant de l’avoir initialisé par un
appel à new. L’initialisation peut se faire par une instruction comme celle-ci :
balances = new double[NYEARS][NRATES] ;
Si vous connaissez les éléments du tableau, vous pouvez utiliser un raccourci pour initialiser les
tableaux à plusieurs dimensions sans avoir recours à new. Par exemple :
int[][] magicSquare =
{
Livre Java .book Page 103 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
103
{16, 3, 2, 13},
{5, 10, 11, 8},
{9, 6, 7, 12},
{4, 15, 14, 1}
};
Lorsque le tableau est initialisé, vous pouvez accéder à ses éléments individuellement, à l’aide de
deux indices entre crochets, par exemple balance[i][j].
L’exemple de programme stocke un tableau à une dimension interest, pour les taux d’intérêt, et un
tableau à deux dimensions balance, pour les soldes des comptes, pour chaque année et chaque taux
d’intérêt. La première ligne du tableau est initialisée avec le solde initial :
for (int j = 0; j < balance[0].length; j++)
balances[0][j] = 10000;
Puis les autres lignes sont calculées de la façon suivante :
for (int i = 1; i < balances.length; i++)
{
for (int j = 0; j < balances[i].length; j++)
{
double oldBalance = balances[i - 1][j];
double interest = . . .;
balance[i][j] = oldBalance + interest;
}
}
L’Exemple 3.8 présente le programme qui calcule l’ensemble des valeurs du tableau.
INFO
Une boucle "for each" ne traverse pas automatiquement toutes les entrées d’un tableau bidimensionnel. Il parcourt
plutôt les lignes, qui sont elles-mêmes des tableaux à une dimension. Pour visiter tous les éléments d’un tableau bidimensionnel, imbriquez deux boucles, comme ceci :
for (double[] row : balances)
for (double b : row)
faire quelque chose avec b
Exemple 3.8 : CompoundInterest.java
public class CompoundInterest
{
public static void main(String[] args)
{
final int STARTRATE = 10;
final int NRATES = 6;
final int NYEARS = 10;
// définir les taux d’intérêt de 10 à 15%
double[] interestRate = new double[NRATES];
for (int j = 0; j < interestRate.length; j++)
interestRate[j] = (STARTRATE + j) / 100.0;
double[][] balance = new double[NYEARS][NRATES];
// définir les soldes initiaux à 10 000
for (int j = 0; j < balance[0].length; j++)
Livre Java .book Page 104 Jeudi, 25. novembre 2004 3:04 15
104
Au cœur de Java 2 - Notions fondamentales
balance[0][j] = 10000;
// calculer l’intérêt des années à venir
for (int i = 1; i < balance.length; i++)
{
for (int j = 0; j < balance[i].length; j++)
{
// récup. solde année précédente de la ligne précédente
double oldBalance = balance[i - 1][j];
// calculer l’intérêt
double interest = oldBalance * interestRate[j];
// calculer le solde de l’année
balances[i][j] = oldBalance + interest;
}
}
// imprimer une ligne de taux d’intérêt
for (int j = 0; j < interestRate.length; j++)
System.out.printf("%9.0f%%", 100 * interestRate[j]));
System.out.println();
// imprimer la table des soldes
for (double[] row : balances)
{
// imprimer une ligne de la table
for (double b : row)
System.out.printf("%10.2f", b);
System.out.println();
}
}
}
Tableaux irréguliers
Pour l’instant, ce que nous avons vu ne s’éloigne pas trop des autres langages de programmation.
Mais en réalité il se passe en coulisse quelque chose de subtil que vous pouvez parfois faire tourner
à votre avantage : Java ne possède pas de tableaux multidimensionnels, mais uniquement des
tableaux unidimensionnels. Les tableaux multidimensionnels sont en réalité des "tableaux de
tableaux".
Livre Java .book Page 105 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
105
Ainsi, dans l’exemple précédent, balances est en fait un tableau de dix éléments, chacun d’eux
étant un tableau de six nombres réels (voir Figure 3.17).
Figure 3.17
10000.0
10000.0
10000.0
10000.0
10000.0
10000.0
balances =
Un tableau à deux
dimensions.
balances[1] =
balances[1][2] =
11000.0
11100.0
11200.0
11300.0
11400.0
11500.0
.
.
.
23579.48
25580.37
27730.79
30040.42
32519.49
35178.76
L’expression balances[i] se réfère au sous-tableau d’indice i, autrement dit à la rangée i ; et
balance[i] [j] fait référence à l’élément j de ce sous-tableau.
Comme les rangées de tableau sont accessibles individuellement, vous pouvez aisément les intervertir !
double[] temp = balance[i];
balances[i] = balances[i + 1];
balances[i + 1] = temp;
De plus, il est facile de créer des tableaux "irréguliers", c’est-à-dire des tableaux dont les différentes
rangées ont des longueurs différentes. Pour illustrer ce mécanisme, créons un tableau dont les
rangées i et les colonnes j représentent le nombre de tirages possibles pour une loterie où il fait
"choisir j nombres parmi i nombres" :
1
1
1
1
1
1
1
1
2
3
4
5
6
1
3
6
10
15
1
4
10
20
1
5
15
1
6
1
Comme j ne peut jamais être plus grand que i, nous obtenons une matrice triangulaire. La ième
rangée possède i + 1 éléments (nous admettons un choix de 0 élément ; et il n’y a qu’une manière
Livre Java .book Page 106 Jeudi, 25. novembre 2004 3:04 15
106
Au cœur de Java 2 - Notions fondamentales
d’effectuer un tel choix). Pour créer ce tableau irrégulier, commençons par allouer le tableau qui
contient les rangées :
int[][] odds = new int[NMAX + 1][];
Créons ensuite les rangées elles-mêmes :
for (int n = 0; n <= NMAX; n++)
odds[n] = new int[n + 1];
Une fois le tableau alloué, nous pouvons accéder normalement à ses éléments, à condition de ne pas
dépasser les limites de chaque sous-tableau :
for (int n = 0; n < odds.length; n++)
for (k = 0; k < odds[n].length; k++)
{
// calculer lotteryOdds
. . .
odds[n][k] = lotteryOdds;
}
L’Exemple 3.9 vous montre le programme complet.
INFO C++
La déclaration Java
double[][] balances = new double[10][6]; // Java
n’est pas équivalente à
double balances[10][6]; // C++
ni même à
double (*balances)[6] = new double[10][6]; // C++
En fait, en C++, un tableau de 10 pointeurs est alloué :
double** balances = new double*[10]; // C++
A chaque élément du tableau de pointeurs est ensuite affecté un tableau de 6 nombres :
for (i = 0; i < 10; i++)
balances[i] = new double[6];
Cette boucle est heureusement exécutée automatiquement par l’instruction new double[10][6]. Si vous désirez
créer un tableau irrégulier, il faut allouer les rangées séparément.
Exemple 3.9 : LotteryArray.java
public class LotteryArray
{
public static void main(String[] args)
{
final int NMAX = 10;
// allouer un tableau triangulaire
int[][] odds = new int[NMAX + 1][];
for (int n = 0; n <= NMAX; n++)
odds[n] = new int[n + 1];
Livre Java .book Page 107 Jeudi, 25. novembre 2004 3:04 15
Chapitre 3
Structures fondamentales de la programmation Java
// remplir le tableau triangulaire
for (int n = 0; n < odds.length; n++)
for (int k = 0; k < odds[n].length; k++)
{
/*
calculer le binôme
n * (n - 1) * (n - 2) * . . . * (n - k + 1)
------------------------------------------1 * 2 * 3 * . . . * k
*/
int lotteryOdds = 1;
for (int i = 1; i <= k; i++)
lotteryOdds = lotteryOdds * (n - i + 1) / i;
odds[n][k] = lotteryOdds;
}
// imprimer le tableau triangulaire
for (int[] row : odds)
{
for (int odd : row)
System.out.printf("%4d", odd);
System.out.println();
}
}
}
107
Livre Java .book Page 108 Jeudi, 25. novembre 2004 3:04 15
Livre Java .book Page 109 Jeudi, 25. novembre 2004 3:04 15
4
Objets et classes
Au sommaire de ce chapitre
✔ Introduction à la programmation orientée objet
✔ Utilisation des classes prédéfinies
✔ Construction de vos propres classes
✔ Champs et méthodes statiques
✔ Paramètres des méthodes
✔ Construction d’un objet
✔ Packages
✔ Commentaires pour la documentation
✔ Conseils pour la conception de classes
L’objectif de ce chapitre est :
m
de vous présenter la programmation orientée objet ;
m
de vous montrer comment créer des objets appartenant à des classes de la bibliothèque Java
standard ;
m
de vous montrer comment rédiger vos propres classes.
Si vous n’avez pas d’expérience en matière de programmation orientée objet, nous vous conseillons
de lire attentivement ce chapitre. La POO (programmation orientée objet) demande une approche
différente de celle des langages procéduraux. La transition n’est pas toujours facile, mais il est
nécessaire de vous accoutumer au concept d’objet avant d’approfondir Java.
Pour les programmeurs C++ expérimentés, ce chapitre, comme le précédent, présentera des informations familières ; malgré tout, il existe des différences entre les deux langages, et nous vous
conseillons de lire les dernières sections de ce chapitre (en vous concentrant sur les infos relatives
à C++).
Livre Java .book Page 110 Jeudi, 25. novembre 2004 3:04 15
110
Au cœur de Java 2 - Notions fondamentales
Introduction à la programmation orientée objet
De nos jours, la programmation orientée objet constitue le principal paradigme de la programmation ; elle a remplacé les techniques de programmation procédurale, "structurée", qui ont été
développées au début des années 1970. Java est totalement orienté objet, et il est impossible de
programmer avec ce langage dans le style procédural que vous avez peut-être appris. Nous espérons
que cette section — combinée avec les exemples fournis dans le texte et sur le site Web — vous fournira assez d’informations sur la POO pour vous permettre de travailler en Java d’une manière
productive.
Commençons par une question qui, à première vue, n’a rien à voir avec la programmation : comment
certaines sociétés de l’industrie informatique sont-elles devenues si importantes, et aussi rapidement ? Nous faisons allusion à Compaq, à Dell, à Gateway et à d’autres constructeurs d’ordinateurs
personnels. Certains répondront qu’elles fabriquaient en général de bonnes machines, vendues à bas
prix, dans une période où la demande atteignait des sommets. Mais allons plus loin : comment ontelles pu construire tant de modèles aussi rapidement et comment ont-elles répondu aussi vite aux
changements qui ont bouleversé le marché de la micro-informatique ?
La réponse réside essentiellement dans le fait que ces sociétés ont sous-traité une bonne part de leur
travail. Elles ont acheté des éléments à des vendeurs réputés et se sont chargées de l’assemblage des
machines. La plupart du temps, elles n’ont pas investi d’argent ni de temps dans la conception et la
fabrication des alimentations, des disques durs, des cartes mères et des autres composants. Ainsi,
elles ont pu produire rapidement des ordinateurs et s’adapter très vite aux nouveautés, tout en réduisant
leurs coûts de développement.
Ces constructeurs de micro-ordinateurs achetaient en fait des "fonctionnalités préconditionnées".
Par exemple, lorsqu’elles achetaient une alimentation, elles acquéraient quelque chose qui possédait
certaines propriétés (la taille, le poids, etc.) et une certaine fonctionnalité (une sortie stabilisée, une
puissance électrique, etc.). Compaq représente un bon exemple de l’efficacité de cette méthode.
Lorsque la société Compaq a abandonné le développement complet de ses machines pour acheter la
plupart de ces éléments à des tiers, elle a pu améliorer de manière importante le bas de sa gamme.
La POO s’appuie sur la même idée. Votre programme est constitué d’objets possédant certaines
propriétés et pouvant accomplir certaines opérations. C’est votre budget et votre temps disponible
qui décideront du fait que vous construirez un objet ou que vous l’achèterez. Cependant, tant que les
objets en question satisfont à vos spécifications, vous ne vous préoccupez pas de savoir comment ils
ont été implémentés. En POO, vous n’êtes concerné que par ce que les objets vous révèlent. Ainsi,
tout comme les constructeurs d’ordinateurs ne s’intéressent pas au développement des alimentations,
la plupart des programmeurs Java ne se préoccupent pas de savoir comment un objet est implémenté,
pourvu qu’il exécute ce qu’ils souhaitent.
La programmation structurée traditionnelle consiste à concevoir un ensemble de fonctions (ou algorithmes) permettant de résoudre un problème. Après avoir déterminé ces fonctions, l’étape suivante
consistait traditionnellement à trouver la manière appropriée de stocker des données. C’est la raison
pour laquelle le concepteur du langage Pascal, Niklaus Wirth, a intitulé son fameux ouvrage de
programmation Algorithmes + Structures de données = Programmes. Remarquez que le terme algorithmes est placé en tête dans ce titre, devant l’expression structures de données. Cela montre bien la
manière dont les programmeurs travaillaient à cette époque. D’abord, vous décidiez de la manière
dont vous alliez manipuler les données ; ensuite seulement, vous choisissiez le genre de structures
Livre Java .book Page 111 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
111
que vous imposeriez aux données afin de faciliter cette manipulation. La POO inverse cet ordre et
place les données au premier plan avant de déterminer l’algorithme qui sera employé pour opérer sur
ces données.
En POO, la clé de la productivité consiste à rendre chaque objet responsable de l’accomplissement
de quelques tâches associées. Si un objet dépend d’une tâche qui n’est pas de sa responsabilité, il
doit avoir accès à un autre objet capable d’accomplir cette tâche. Le premier objet demande alors au
second d’exécuter la tâche en question. Cela s’effectue grâce à une version plus généralisée des
appels de fonctions auxquels vous êtes habitués en programmation traditionnelle (notez qu’en Java
ces appels de fonctions sont appelés en général des appels de méthodes).
Il faut remarquer qu’un objet ne doit jamais manipuler directement les données internes d’un autre
objet, pas plus qu’il ne doit rendre accessibles directement les données aux autres objets. Toute
communication se fait par l’intermédiaire d’appels de méthodes. Par l’encapsulation des données
d’un objet, vous facilitez leur réutilisation, vous réduisez la dépendance aux données et vous minimisez le temps de débogage.
Bien entendu, comme c’est le cas pour les modules d’un langage procédural, il ne faut pas qu’un
objet accomplisse trop de choses. La conception et le débogage sont simplifiés lorsque l’on construit
de petits objets spécialisés au lieu d’énormes objets contenant des données complexes et possédant
des centaines de fonctions pour les manipuler.
Le vocabulaire de la POO
Avant d’aller plus loin, il faut comprendre certains termes de la POO. Le plus important est le mot
classe, que vous avez déjà rencontré dans les exemples du Chapitre 3. Une classe est le modèle ou la
matrice de l’objet effectif. Cela nous amène à la comparaison habituelle en ce qui concerne les classes :
des moules à biscuits, les objets représentant les biscuits proprement dits. Lorsqu’on construit un
objet à partir d’une classe, on dit que l’on crée une instance de cette classe.
Comme vous avez pu le constater, tout le code que vous écrivez en Java se trouve dans une classe. La
bibliothèque Java standard fournit plusieurs milliers de classes répondant à de multiples besoins
comme la conception de l’interface utilisateur, les dates et les calendriers, ou la programmation
réseau. Quoi qu’il en soit, il vous faut quand même créer vos propres classes Java, décrire les objets
des domaines de problèmes appartenant à vos applications et adapter les classes existantes à vos
propres besoins.
L’encapsulation (appelée parfois dissimulation des données, ou isolement des données) est un
concept clé pour l’utilisation des objets. L’encapsulation consiste tout bonnement à combiner des
données et un comportement dans un emballage et à dissimuler l’implémentation des données aux
utilisateurs de l’objet. Les données d’un objet sont appelées ses champs d’instance ; les fonctions
qui agissent sur les données sont appelées ses méthodes. Un objet spécifique, qui est une instance
d’une classe, a des valeurs spécifiques dans ses champs d’instance. Le jeu de ces valeurs est l’état
actuel de l’objet. Chaque fois que vous appelez un message sur un objet, son état peut changer.
Il faut insister sur le fait que l’encapsulation ne fonctionne correctement que si les méthodes n’ont
jamais accès directement aux champs d’instance dans une classe autre que la leur propre. Les
programmes doivent interagir avec les données d’un objet uniquement par l’intermédiaire des
méthodes de l’objet. L’encapsulation représente le moyen de donner à l’objet son comportement de
"boîte noire" ; c’est sur elle que reposent la réutilisation et la sécurité de l’objet. Cela signifie qu’une
Livre Java .book Page 112 Jeudi, 25. novembre 2004 3:04 15
112
Au cœur de Java 2 - Notions fondamentales
classe peut complètement modifier la manière dont elle stocke ses données, mais tant qu’elle continue à utiliser les mêmes méthodes pour les manipuler, les autres objets n’en sauront rien et ne s’en
préoccuperont pas.
Lorsque vous commencez réellement à écrire vos propres classes en Java, un autre principe de la
POO facilite cette opération : les classes peuvent être construites sur les autres classes. On dit qu’une
classe construite à partir d’une autre l’étend. Java est en fait fourni avec une "superclasse cosmique"
appelée Object. Toutes les autres classes étendent cette classe. Vous en apprendrez plus concernant
la classe Object dans le prochain chapitre.
Lorsque vous étendez une classe existante, la nouvelle classe possède toutes les propriétés et méthodes de la classe que vous étendez. Vous fournissez les nouvelles méthodes et les champs de données
qui s’appliquent uniquement à votre nouvelle classe. Le concept d’extension d’une classe pour en
obtenir une nouvelle est appelé héritage. Reportez-vous au prochain chapitre pour plus de détails
concernant la notion d’héritage.
Les objets
Pour bien travailler en POO, vous devez être capable d’identifier trois caractéristiques essentielles
des objets. Ce sont :
m
Le comportement de l’objet. Que pouvez-vous faire avec cet objet, ou quelles méthodes pouvezvous lui appliquer ?
m
L’état de l’objet. Comment l’objet réagit-il lorsque vous appliquez ces méthodes ?
m
L’identité de l’objet. Comment l’objet se distingue-t-il des autres qui peuvent avoir le même
comportement et le même état ?
Tous les objets qui sont des instances d’une même classe partagent le même comportement. Celui-ci
est déterminé par les méthodes que l’objet peut appeler.
Ensuite, chaque objet stocke des informations sur son aspect actuel. C’est l’état de l’objet. L’état
d’un objet peut changer dans le temps, mais pas spontanément. Une modification dans l’état d’un
objet doit être la conséquence d’appels de méthodes (si l’état de l’objet change sans qu’un appel de
méthode soit intervenu, cela signifie que la règle de l’encapsulation a été violée).
Néanmoins, l’état d’un objet ne décrit pas complètement celui-ci, car chaque objet possède une identité spécifique. Par exemple, dans un système de traitement de commandes, deux commandes sont
distinctes même si elles désignent des produits identiques. Remarquez que des objets individuels —
instances d’une même classe — ont toujours une identité distincte et généralement un état distinct.
Chacune de ces caractéristiques essentielles peut avoir une influence sur les autres. Par exemple,
l’état d’un objet peut altérer son comportement. Si une commande est "expédiée" ou "payée", elle
peut refuser un appel de méthode qui demanderait d’ajouter ou de supprimer un élément. Inversement, si une commande est "vide" — autrement dit, si aucun produit n’a encore été commandé —
elle ne doit pas pouvoir être expédiée.
Dans un programme procédural traditionnel, le processus commence par une fonction principale
main. Lorsqu’on travaille dans un système orienté objet, il n’y a pas de "début", et les programmeurs
débutants en POO se demandent souvent par où commencer. La réponse est la suivante : trouvez
d’abord les classes appropriées, puis ajoutez des méthodes à ces classes.
Livre Java .book Page 113 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
113
ASTUCE
Une règle simple dans l’identification des classes consiste à rechercher des noms quand vous analysez le problème.
En revanche, les méthodes sont symbolisées par des verbes.
Par exemple, voici quelques noms dans un système de gestion de commandes :
m
produit ;
m
commande ;
m
adresse de livraison ;
m
règlement ;
m
compte.
Ces noms permettent de rechercher les classes Item, Order, et ainsi de suite.
On cherche ensuite les verbes. Les produits (ou articles) sont ajoutés aux commandes. Les commandes sont expédiées ou annulées. Les règlements sont appliqués aux commandes. Tous ces verbes,
"ajouter", "expédier", "annuler" et "appliquer", permettent d’identifier l’objet qui aura la principale
responsabilité de leur exécution. Par exemple, lorsqu’un nouveau produit est ajouté à une
commande, c’est l’objet Order (commande) qui doit être responsable de cet ajout, car il sait
comment stocker et trier ses propres éléments. Autrement dit, dans la classe Order, add (ajouter)
doit être une méthode qui reçoit un objet Item (produit) comme paramètre.
Bien entendu, la "règle des noms et des verbes" n’est qu’une règle mnémonique, et seule l’expérience vous apprendra à déterminer quels noms et quels verbes sont importants pour le développement
de vos propres classes.
Relations entre les classes
Les relations les plus courantes entre les classes sont :
m
dépendance ("utilise") ;
m
agrégation ("possède") ;
m
héritage ("est").
La relation de dépendance ou "utilise" est la plus évidente et la plus courante. Par exemple, la classe
Order utilise la classe Account, car les objets Order doivent pouvoir accéder aux objets Account
pour vérifier que le compte est crédité. Mais la classe Item ne dépend pas de la classe Account, car
les objets Item n’ont pas à se préoccuper de l’état du compte d’un client. Une classe dépend d’une
autre classe si ses méthodes manipulent des objets de cette classe.
ASTUCE
Efforcez-vous de réduire le nombre de classes qui dépendent mutuellement les unes des autres. L’avantage est le
suivant : si une classe A ignore l’existence d’une classe B, elle n’aura pas à se préoccuper des modifications apportées
éventuellement à B ! (Et cela signifie qu’une modification dans la classe B n’introduira pas de bogues dans la classe A.)
Dans la terminologie logicielle, on dit vouloir réduire le couplage entre les classes.
Livre Java .book Page 114 Jeudi, 25. novembre 2004 3:04 15
114
Au cœur de Java 2 - Notions fondamentales
La relation d’agrégation ou "possède" est facile à comprendre, car elle est concrète ; par exemple, un
objet Order contient des objets Item. Cette relation signifie que des objets d’une classe A contiennent
des objets d’une classe B.
INFO
Certains dédaignent le concept d’agrégation et préfèrent parler de relation "d’association". Du point de vue de la
modélisation, cela peut se comprendre. Mais pour les programmeurs, la relation "possède" semble évidente. Nous
préférons personnellement le terme agrégation pour une seconde raison : la notation standard pour les associations
est moins claire. Voir le Tableau 4.1.
La relation d’héritage ou "est" exprime une relation entre une classe plus spécifique et une, plus
générale. Par exemple, une classe RushOrder (commande urgente) hérite d’une classe Order. La
classe spécialisée RushOrder dispose de méthodes particulières pour gérer la priorité et d’une
méthode différente pour calculer le coût de livraison, mais ses autres méthodes — par exemple,
ajouter des éléments ou facturer — sont héritées de la classe Order. En général, si la classe A étend
la classe B, la classe A hérite des méthodes de la classe B, mais possède des fonctionnalités supplémentaires (l’héritage sera décrit plus en détail au chapitre suivant).
De nombreux programmeurs ont recours à la notation UML (Unified Modeling Language) pour
représenter les diagrammes de classe qui décrivent la relation entre les classes. Un exemple est
montré à la Figure 4.1. Vous dessinez les classes sous la forme de rectangles, et les relations sont
représentées par des flèches ayant différents aspects. Le Tableau 4.1 montre les styles de flèches les
plus couramment utilisés.
Figure 4.1
Diagramme d’une classe.
INFO
Plusieurs outils permettent de dessiner ce type de diagramme. Les fournisseurs proposent souvent des outils performants (et chers) censés être le point central de la procédure de développement. On trouve entre autres Rational Rose
Livre Java .book Page 115 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
115
(http://www.ibm.com/software/awdtools/developer/modeler) et Together (http://www.borland.com/together).
Vous pouvez également opter pour le programme à source libre ArgoUML (http://argouml.tigris.org). Une version
commerciale est disponible chez GentleWare (http://gentleware.com). Pour dessiner des diagrammes simples sans
trop de problèmes, testez Violet (http://horstmann.com/violet).
Tableau 4.1 : Notation UML pour représenter la relation entre classes
Relation
Connecteur UML
Héritage
Héritage d’interface
Dépendance
Agrégation
Association
Association dirigée
Comparaison entre POO et programmation procédurale traditionnelle
Nous terminerons cette courte introduction à la POO en comparant celle-ci avec le modèle procédural qui doit vous être familier. En programmation procédurale, on identifie d’abord les tâches à
accomplir, puis :
m
En procédant par étapes, on réduit chaque tâche en sous-tâches, puis celles-ci en sous-tâches
plus petites, et ainsi de suite jusqu’à ce que les sous-tâches obtenues soient suffisamment simples
pour être implémentées directement (approche de haut en bas).
m
On écrit des procédures permettant de résoudre les tâches simples, puis on les combine en procédures plus sophistiquées jusqu’à ce que l’on obtienne les fonctionnalités souhaitées (approche de
bas en haut).
Bien entendu, la plupart des programmeurs emploient un mélange de ces deux stratégies pour résoudre un problème de programmation. La règle de base pour découvrir des procédures est identique à
celle que l’on utilise pour trouver les méthodes en POO : chercher les verbes ou les actions dans la
description du problème. La différence principale réside dans le fait qu’en POO on isole d’abord les
classes dans le projet. C’est ensuite seulement que l’on cherche les méthodes. Il existe une autre
distinction d’importance entre les procédures traditionnelles et les méthodes de la POO : chaque
méthode est associée à la classe qui est responsable de l’opération.
Pour de petits problèmes, la réduction en procédures fonctionne très bien. Pour des problèmes
plus importants, les classes et les méthodes offrent deux avantages. Les classes fournissent un
mécanisme de regroupement des méthodes, qui est très pratique. L’implémentation d’un simple
navigateur Web peut exiger soit 2 000 fonctions, soit 100 classes possédant en moyenne
20 méthodes. Cette deuxième structure est plus facile à maîtriser pour un programmeur et aussi à
répartir parmi les membres d’une équipe. L’encapsulation offre également une aide appréciable :
Livre Java .book Page 116 Jeudi, 25. novembre 2004 3:04 15
116
Au cœur de Java 2 - Notions fondamentales
les classes dissimulent la représentation de leurs données à tout le programme, excepté à leurs
propres méthodes. Comme le montre la Figure 4.2, cela signifie que, si un bogue altère des
données, il est plus aisé de rechercher le coupable parmi les 20 méthodes qui ont accès à ces
données que parmi 2 000 procédures.
Vous pourriez penser que tout cela ne semble pas très différent de la modularisation. Vous avez sans
doute écrit des programmes que vous avez divisés en modules qui communiquent à l’aide de procédures plutôt qu’en échangeant des données. Lorsque cette technique est bien appliquée, elle se
rapproche énormément de l’encapsulation. Néanmoins, dans la plupart des langages, la moindre
négligence vous permet d’accéder aux données d’un autre module — l’encapsulation est facile à
contourner.
Il existe un problème plus sérieux : alors que les classes représentent des usines capables de produire
de nombreux objets ayant le même comportement, il n’est pas possible d’obtenir de multiples copies
d’un module utile. Supposons que vous disposiez d’un module qui encapsule une collection de
commandes et d’un autre module contenant un arbre binaire bien équilibré pour accéder à ces
commandes. Supposons encore que vous ayez besoin de deux collections, une pour les commandes
en attente et l’autre pour les commandes terminées. Vous ne pouvez pas lier deux fois le module
d’arbre binaire. Et vous n’avez sûrement pas envie d’en faire une copie et de renommer toutes les
procédures pour permettre au lieur de fonctionner !
Les classes ne connaissent pas de telles limites. Lorsqu’une classe a été définie, il est très facile de
construire n’importe quel nombre d’instances de cette classe (alors qu’un module ne peut avoir
qu’une seule instance).
Figure 4.2
La programmation
procédurale comparée
à la POO.
procédure
méthode
méthode
procédure
procédure
Données
globales
méthode
méthode
procédure
procédure
méthode
méthode
Données
objet
Données
objet
Données
objet
Nous n’avons encore fait que gratter légèrement la surface. Vous trouverez à la fin de ce chapitre une
petite section contenant des astuces pour concevoir les classes. Toutefois, pour une meilleure
compréhension du processus de conception orientée objet, de nombreux ouvrages existent sur le
sujet.
Utilisation des classes existantes
On ne peut rien faire en Java sans les classes, et vous avez déjà vu plusieurs classes dans les chapitres
précédents. Malheureusement, la plupart d’entre elles ne correspondent pas à l’esprit de Java.
Livre Java .book Page 117 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
117
Un bon exemple de cette anomalie est constitué par la classe Math. Vous avez vu que l’on peut utiliser les méthodes de la classe Math, telles que Math.random, sans avoir besoin de savoir comment
elles sont implémentées — il suffit d’en connaître le nom et les paramètres (s’il y en a). C’est la
caractéristique de l’encapsulation, et ce sera vrai pour toutes les classes. Malheureusement, la classe
Math encapsule seulement une fonctionnalité ; elle n’a pas besoin de manipuler ou de cacher des
données. Comme il n’y a pas de données, vous n’avez pas à vous préoccuper de la création des objets
et de l’initialisation de leurs champs d’instance — il n’y en a pas !
Dans la prochaine section, nous allons étudier une classe plus typique, la classe Date. Vous verrez
comment construire les objets et appeler les méthodes de cette classe.
Objets et variables objet
Pour travailler avec les objets, le processus consiste à créer des objets et à spécifier leur état initial.
Vous appliquez ensuite les méthodes aux objets.
Dans le langage Java, on utilise des constructeurs pour construire de nouvelles instances. Un
constructeur est une méthode spéciale dont le but est de construire et d’initialiser les objets.
Prenons un exemple. La bibliothèque Java standard contient une classe Date. Ses objets décrivent des
moments précis dans le temps, tels que "31 décembre 1999, 23:59:59 GMT".
INFO
Vous vous demandez peut-être pourquoi utiliser des classes pour représenter des dates au lieu (comme dans certains
langages) d’un type intégré. Visual Basic, par exemple, possède un type de données intégré et les programmeurs
peuvent spécifier les dates au format #6/1/1995#. Cela semble apparemment pratique ; les programmeurs utilisent simplement ce type sans se préoccuper des classes. Mais en réalité, cette conception de Visual Basic convient-elle
dans tous les cas ? Avec certains paramètres locaux, les dates sont spécifiées sous la forme mois/jour/année, dans
d’autres sous la forme jour/mois/année. Les concepteurs du langage sont-ils vraiment armés pour prévoir tous ces cas
de figure ? S’ils font un travail incomplet, le langage devient désagréablement confus et le programmeur frustré est
impuissant. Avec les classes, la tâche de conception est déléguée à un concepteur de bibliothèque. Si une classe n’est
pas parfaite, les programmeurs peuvent facilement écrire la leur pour améliorer ou remplacer les classes du système.
Les constructeurs ont toujours le même nom que la classe. Par conséquent, le constructeur pour la
classe Date est appelé Date. Pour construire un objet Date, vous combinez le constructeur avec
l’opérateur new de la façon suivante :
new Date()
Cette expression construit un nouvel objet. L’objet est initialisé avec l’heure et la date courantes.
Vous pouvez aussi passer l’objet à une méthode :
System.out.println(new Date());
Une autre possibilité consiste à appliquer une méthode à l’objet que vous venez de construire. L’une
des méthodes de la classe Date est la méthode toString. Cette méthode permet d’obtenir une représentation au format chaîne de la date. Voici comment appliquer la méthode toString à un objet
Date nouvellement construit :
String s = new Date().toString();
Livre Java .book Page 118 Jeudi, 25. novembre 2004 3:04 15
118
Au cœur de Java 2 - Notions fondamentales
Dans ces deux exemples, l’objet construit n’est utilisé qu’une seule fois. Généralement, vous voulez
conserver les objets que vous construisez pour pouvoir continuer à les utiliser. Stockez simplement
l’objet dans une variable :
Date birthday = new Date();
La Figure 4.3 montre la variable objet birthday (anniversaire) faisant référence à l’objet qui vient
d’être construit.
Figure 4.3
birthday =
Création
d’un nouvel objet.
Date
Il existe une différence importante entre les objets et les variables objet. Par exemple, l’instruction
Date deadline; // deadline ne désigne pas un objet
définit une variable objet, deadline (date limite) qui peut référencer des objets de type Date. Il est
important de comprendre que la variable deadline n’est pas un objet et qu’en fait elle ne référence
encore aucun objet. Pour le moment, vous ne pouvez employer aucune méthode Date avec cette
variable. L’instruction
s = deadline.toString(); // pas encore
provoquerait une erreur de compilation.
Vous devez d’abord initialiser la variable deadline. Pour cela, deux possibilités. Vous pouvez, bien
entendu, initialiser la variable avec l’objet nouvellement construit :
deadline = new Date();
Ou bien définir la variable pour qu’elle fasse référence à un objet existant :
deadline = birthday;
Maintenant, les deux variables font référence au même objet (voir Figure 4.4).
Figure 4.4
Variables objet
faisant référence
au même objet.
birthday =
Date
deadline =
Il est important de comprendre qu’une variable objet ne contient pas réellement un objet. Elle fait
seulement référence à un objet.
En Java, la valeur de toute variable objet est une référence à un objet qui est stocké ailleurs. La valeur
renvoyée par l’opérateur new est aussi une référence. Une instruction telle que
Date deadline = new Date();
Livre Java .book Page 119 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
119
comprend deux parties. L’expression new Date() crée un objet du type Date, et sa valeur est une
référence à cet objet qui vient d’être créé. Cette référence est ensuite stockée dans la variable
deadline.
Il est possible de donner explicitement la valeur null à une variable objet afin d’indiquer qu’elle ne
référence actuellement aucun objet :
deadline = null;
. . .
if (deadline != null)
System.out.println(deadline);
Une erreur d’exécution se produit si vous appliquez une méthode à une variable ayant la valeur
null :
birthday = null;
String s = birthday.toString(); // erreur à l’exécution !
Les variables ne sont pas initialisées automatiquement à null. Vous devez les initialiser, soit en
appelant new, soit en leur affectant null.
INFO C++
De nombreuses personnes pensent à tort que les variables objet de Java se comportent comme les références de C++.
Mais en C++ il n’y a pas de références nulles et les références ne peuvent pas être affectées. Il faut plutôt penser aux
variables objet de Java comme aux pointeurs sur objets de C++. Par exemple,
Date birthday; // Java
est en fait identique à :
Date* birthday; // C++
Lorsque l’on fait cette association, tout redevient clair. Bien entendu, un pointeur Date* n’est pas initialisé tant que
l’on n’appelle pas new. La syntaxe est presque la même en C++ et en Java :
Date* birthday = new Date(); // C++
Si l’on copie une variable dans une autre, les deux variables font référence à la même date : ce sont des pointeurs sur
le même objet. L’équivalent d’une référence null de Java est le pointeur null de C++.
Tous les objets Java résident dans le tas (heap). Lorsqu’un objet contient une autre variable objet, cette variable ne
contient elle-même qu’un pointeur sur un autre objet qui réside dans le tas.
En C++, les pointeurs vous rendent nerveux, car ils sont responsables de nombreuses erreurs. Il est très facile de
créer des pointeurs incorrects ou d’altérer la gestion de la mémoire. En Java, ces problèmes ont tout bonnement
disparu. Si vous utilisez un pointeur qui n’est pas initialisé, le système d’exécution déclenchera une erreur d’exécution au lieu de produire des résultats aléatoires. Vous n’avez pas à vous préoccuper de la gestion de la mémoire, car
le récupérateur de mémoire (ou ramasse-miettes) s’en charge.
En prenant en charge les constructeurs de copie et les opérateurs d’affectation, le C++ a fait un bel effort pour
permettre l’implémentation d’objets qui se copient automatiquement. Par exemple, une copie d’une liste liée est
une nouvelle liste liée ayant un contenu identique, mais des liens indépendants. Ce mécanisme autorise la conception de classes ayant le même comportement que les classes prédéfinies. En Java, il faut utiliser la méthode clone
pour obtenir une copie complète d’un objet.
Livre Java .book Page 120 Jeudi, 25. novembre 2004 3:04 15
120
Au cœur de Java 2 - Notions fondamentales
La classe GregorianCalendar de la bibliothèque Java
Dans les exemples précédents, nous avons employé la classe Date qui fait partie de la bibliothèque
Java standard. Une instance de cette classe possède un état — une position dans le temps.
Bien qu’il ne soit pas indispensable de connaître ces détails pour utiliser la classe Date, l’heure est
représentée par le nombre de millièmes de seconde (positif ou négatif) à partir d’un point fixe
(appelé epoch ou époque), qui est le 1er janvier 1970 à 00:00:00 UTC. UTC est le temps universel
(Coordinated Universal Time), le standard scientifique qui est, pour des raisons pratiques, le même
que l’heure GMT (Greenwich Mean Time).
En réalité, la classe Date n’est pas très pratique pour manipuler les dates. Les concepteurs de la
bibliothèque Java considèrent qu’une description de date — telle que "31 décembre 1999, 23:59:59"
— est une convention arbitraire déterminée par un calendrier. Cette description correspond à celle
du calendrier grégorien utilisé dans de nombreux pays. Ce même repère temporel pourrait être décrit
différemment dans le calendrier chinois ou le calendrier lunaire hébreu, sans parler du calendrier de
nos clients martiens.
INFO
Au cours de l’histoire de l’humanité, les civilisations se sont débattues avec la conception de calendriers attribuant
des noms aux dates, et ont mis de l’ordre dans les cycles solaires et lunaires. L’ouvrage Calendrical Calculations, de
Nachum Dershowitz et Edward M. Reingold (Cambridge University Press, 1997), fournit une explication fascinante
des calendriers dans le monde, du calendrier révolutionnaire français à celui des Mayas.
Les concepteurs de la bibliothèque ont décidé de séparer le fait de conserver le temps et d’attacher
des noms à des points temporels. La bibliothèque Java standard contient donc deux classes distinctes : la classe Date qui représente un point temporel, et la classe GregorianCalendar qui exprime
les dates par rapport au calendrier. En fait, la classe GregorianCalendar étend une classe Calendar
plus générique, qui décrit les propriétés des calendriers en général. Théoriquement, vous pouvez
étendre la classe Calendar et implémenter le calendrier lunaire chinois ou un calendrier martien.
Quoi qu’il en soit, la bibliothèque standard ne contient pas d’autre implémentation de calendrier que
le calendrier grégorien.
Distinguer la mesure du temps de la notion de calendriers relève tout à fait de la conception orientée
objet. Il est généralement souhaitable d’utiliser des classes séparées pour exprimer des concepts
différents.
La classe Date ne dispose que d’un petit nombre de méthodes pour comparer deux points dans le
temps. Par exemple, les méthodes before et after vous indiquent si un moment donné vient avant
ou après un autre :
if (today.before(birthday))
System.out.println("I still have time to shop for a gift.");
INFO
En réalité, la classe Date dispose de méthodes telles que getDay, getMonth et getYear, mais ces méthodes sont
dépréciées. Cela signifie que le concepteur de la bibliothèque a admis qu’elles n’auraient jamais dû y figurer.
Livre Java .book Page 121 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
121
Ces méthodes faisaient partie de la classe Date avant que les concepteurs réalisent qu’il était plus logique de fournir
des classes de calendrier séparées. Lors de l’introduction de ces classes, les méthodes Date ont été dépréciées. Vous
pouvez toujours les employer dans vos programmes, mais vous obtiendrez alors des avertissements disgracieux du
compilateur. Il est préférable d’éviter l’utilisation de méthodes dépréciées, car elles peuvent très bien être supprimées
dans une version future de la bibliothèque.
La classe GregorianCalendar propose beaucoup plus de méthodes que la classe Date. Elle possède
en particulier plusieurs constructeurs utiles. L’expression
new GregorianCalendar()
construit un nouvel objet qui représente la date et l’heure de construction de l’objet.
Vous pouvez construire un objet calendrier pour minuit à une date spécifique en fournissant l’année,
le mois et le jour :
new GregorianCalendar(1999, 11, 31)
Assez curieusement, les mois sont comptés à partir de 0. Ainsi, le mois 11 est décembre. Pour
simplifier ces manipulations, il existe des constantes comme Calendar.DECEMBER :
new GregorianCalendar(1999, Calendar.DECEMBER, 31)
Vous pouvez aussi définir l’heure :
new GregorianCalendar(1999, Calendar.DECEMBER, 31, 23, 59, 59)
Vous stockez bien entendu l’objet une fois construit dans une variable objet :
GregorianCalendar deadline = new GregorianCalendar(. . .);
La classe GregorianCalendar a des champs d’instance encapsulés pour conserver la date à sa
valeur définie. Si l’on n’examine pas le code source, il est impossible de connaître la représentation
interne de ces données dans la classe. Evidemment, c’est justement là l’intérêt de la chose. Ce qui
importe, ce sont les méthodes mises à disposition par la classe.
Les méthodes d’altération et les méthodes d’accès
Vous vous demandez probablement comment obtenir le jour, le mois ou l’année de la date encapsulée dans un objet GregorianCalendar. Et comment modifier les valeurs si elles ne vous conviennent pas. Vous trouverez les réponses à ces questions en consultant la documentation en ligne ou les
infos API à la fin de cette section. Nous allons étudier ici les méthodes les plus importantes.
Le rôle d’un calendrier est de calculer les attributs, tels que la date, le jour de la semaine, le mois ou
l’année, d’un point donné dans le temps. La méthode get de la classe GregorianCalendar permet
d’extraire ces données. Pour sélectionner l’élément que vous souhaitez connaître, vous passez à la
méthode une des constantes définies dans la classe Calendar, comme Calendar.MONTH pour le
mois, ou Calendar.DAY_OF_WEEK pour le jour de la semaine :
GregorianCalendar now = new GregorianCalendar();
int month = now.get(Calendar.MONTH);
int weekday = now.get(Calendar.DAY_OF_WEEK);
Les infos API donnent la liste de toutes les constantes disponibles.
Livre Java .book Page 122 Jeudi, 25. novembre 2004 3:04 15
122
Au cœur de Java 2 - Notions fondamentales
Il est possible de changer l’état en appelant la méthode set :
deadline.set(Calendar.YEAR, 2001);
deadline.set(Calendar.MONTH, Calendar.APRIL);
deadline.set(Calendar.DAY_OF_MONTH, 15);
Il existe aussi une méthode pour définir l’année, le mois et le jour en un seul appel :
deadline.set(2001, Calendar.APRIL, 15);
Vous pouvez enfin ajouter un certain nombre de jours, de semaines, de mois, etc. à un objet de calendrier
donné :
deadline.add(Calendar.MONTH, 3); // décaler la date limite de 3 mois
Si vous ajoutez un nombre négatif, le déplacement dans le calendrier se fait en arrière.
Il existe une différence conceptuelle entre, d’une part, la méthode get et, d’autre part, les méthodes
set et add. La méthode get se contente d’examiner l’état de l’objet et de renvoyer une information.
En revanche, les méthodes set et add modifient l’état de l’objet. Les méthodes qui modifient des
champs d’instance sont appelées méthodes d’altération (mutator), celles qui se contentent d’accéder
aux champs de l’instance, sans les modifier, sont appelées méthodes d’accès (accessor).
INFO C++
En C++, le suffixe const est utilisé pour désigner les méthodes d’accès. Une méthode qui n’est pas déclarée comme
const est supposée être une méthode d’altération. Dans le langage Java, il n’existe cependant pas de syntaxe particulière pour distinguer les méthodes d’accès de celles d’altération.
Une convention couramment employée consiste à préfixer les méthodes d’accès à l’aide de get et
les méthodes d’altération à l’aide de set. Par exemple, la classe GregorianCalendar dispose des
méthodes getTime et setTime qui récupèrent et définissent le moment dans le temps qu’un objet
calendrier représente :
Date time = calendar.getTime();
calendar.setTime(time);
Ces méthodes sont particulièrement utiles pour réaliser des conversions entre les classes Date et
GregorianCalendar. Supposons, par exemple, que vous connaissiez l’année, le mois et le jour
et que vous vouliez définir un objet Date avec ces informations. Puisque la classe Date ne sait rien
des calendriers, nous allons d’abord construire un objet GregorianCalendar, puis appeler la
méthode getTime pour obtenir une date :
GregorianCalendar calendar
= new GregorianCalendar(year, month, day);
Date hireDay = calendar.getTime();
Inversement, si vous voulez trouver l’année, le mois ou le jour d’un objet Date, vous construisez un
objet GregorianCalendar, définissez l’heure, puis appelez la méthode get :
GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(hireDay);
int year = calendar.get(Calendar.YEAR);
Livre Java .book Page 123 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
123
Nous allons conclure cette section avec un programme qui tire parti de la classe GregorianCalendar.
Le programme affiche un calendrier pour le mois en cours de la façon suivante :
Sun Mon Tue Wed Thu Fri Sat
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
La date du jour est signalée par un *, et le programme sait comment calculer les jours de la semaine.
Examinons les étapes clés du programme. Tout d’abord, nous construisons un objet calendrier qui
est initialisé avec la date et l’heure courantes (en réalité, l’heure n’a pas d’importance pour cette
application) :
GregorianCalendar d = new GregorianCalendar();
Nous capturons le jour et le mois courants en appelant deux fois la méthode get :
int today = d.get(Calendar.DAY_OF_MONTH);
int month = d.get(Calendar.MONTH);
Nous affectons ensuite à d le premier jour du mois et récupérons le jour de la semaine correspondant
à cette date :
d.set(Calendar.DAY_OF_MONTH, 1);
int weekday = d.get(Calendar.DAY_OF_WEEK);
La variable weekday est définie à 1 (ou Calendar.SUNDAY) si le premier jour du mois est un dimanche,
à 2 (ou Calendar.MONDAY) s’il s’agit d’un lundi, etc.
Nous imprimons ensuite l’en-tête et les espaces pour l’indentation de la première ligne du calendrier.
Pour chaque jour, nous imprimons un espace si la date est < 10, puis la date, et un * si la date est
celle du jour. Chaque samedi, nous allons à la ligne.
Puis nous avançons d de un jour :
d.add(Calendar.DAY_OF_MONTH, 1);
Quand allons-nous nous arrêter ? Nous ne savons pas si le mois a 31, 30, 29 ou 28 jours. Nous poursuivons tant que d est dans le mois en cours :
do
{
. . .
}
while (d.get(Calendar.MONTH) == month);
Lorsque d se trouve dans le mois suivant, le programme se termine.
L’Exemple 4.1 montre le programme complet.
Vous pouvez constater que la classe GregorianCalendar simplifie l’écriture d’un programme de
calendrier qui prend en charge toute la complexité relative aux jours de la semaine et aux différentes
longueurs des mois. Vous n’avez pas besoin de savoir comment la classe GregorianCalendar
calcule les mois et les jours de la semaine. Vous utilisez simplement l’interface de la classe — les
méthodes get, set et add.
Livre Java .book Page 124 Jeudi, 25. novembre 2004 3:04 15
124
Au cœur de Java 2 - Notions fondamentales
L’intérêt de cet exemple de programme est de démontrer comment vous pouvez utiliser l’interface
d’une classe pour réaliser des tâches assez sophistiquées, sans jamais avoir à vous préoccuper des
détails de son implémentation.
Exemple 4.1 : CalendarTest.java
import java.util.*;
public class CalendarTest
{
public static void main(String[] args)
{
// construire d comme la date courante
GregorianCalendar d = new GregorianCalendar();
int today = d.get(Calendar.DAY_OF_MONTH);
int month = d.get(Calendar.MONTH);
// attribuer à d le premier jour du mois
d.set(Calendar.DAY_OF_MONTH, 1);
int weekday = d.get(Calendar.DAY_OF_WEEK);
// imprimer l’en-tête
System.out.println("Sun Mon Tue Wed Thu Fri Sat");
// indenter la première ligne du calendrier
for (int i = Calendar.SUNDAY; i < weekday; i++ )
System.out.print("
");
do
{
// imprimer la date
int day = d.get(Calendar.DAY_OF_MONTH);
System.out.printf("%3d", day);
// marquer la date du jour avec un *
if (day == today)
System.out.print("*");
else
System.out.print(" ");
// sauter à la ligne après chaque samedi
if (weekday == Calendar.SATURDAY)
System.out.println();
// incrémenter d
d.add(Calendar.DAY_OF_MONTH, 1);
weekday = d.get(Calendar.DAY_OF_WEEK);
}
while (d.get(Calendar.MONTH) == month);
// sortir de la boucle si d est le premier jour du mois suivant
// imprimer dernière fin de ligne si nécessaire
if (weekday != Calendar.SUNDAY)
System.out.println();
}
}
Livre Java .book Page 125 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
125
INFO
A des fins de simplicité, le programme de l’Exemple 4.1 affiche un calendrier contenant les noms anglais des jours de
la semaine, en supposant que la semaine commence un dimanche. Regardez la classe DateFormatSymbols pour
connaître les noms des jours de la semaine dans d’autres langues. La méthode Calendar.getFirstDayOfWeek
renvoie le premier jour de la semaine, par exemple dimanche aux Etats-Unis et lundi en Allemagne.
java.util.GregorianCalendar 1.1
• GregorianCalendar()
Construit un objet calendrier représentant l’heure courante, dans la zone horaire par défaut et
avec les paramètres locaux par défaut.
•
GregorianCalendar(int year, int month, int day)
Construit un calendrier grégorien à la date spécifiée.
Paramètres :
•
year
L’année de la date.
month
Le mois de la date, à base 0 (autrement dit : 0 pour janvier).
day
Le jour du mois.
GregorianCalendar(int year, int month, int day, int hour, int minutes,
int seconds)
Construit un calendrier grégorien à la date et à l’heure spécifiées.
Paramètres :
•
year
L’année de la date.
month
Le mois de la date, à base 0 (autrement dit : 0 pour janvier).
day
Le jour du mois.
hour
L’heure (de 0 à 23).
minutes
Les minutes (de 0 à 59).
seconds
Les secondes (de 0 à 59).
int get(int field)
Extrait la valeur du champ spécifié.
Paramètres :
field
Une des valeurs suivantes :
– Calendar.ERA
– Calendar.YEAR
– Calendar.MONTH
– Calendar.WEEK_OF_YEAR
– Calendar.WEEK_OF_MONTH
– Calendar.DAY_OF_MONTH
– Calendar.DAY_OF_YEAR
– Calendar.DAY_OF_WEEK
– Calendar.DAY_OF_WEEK_IN_MONTH
– Calendar.AM_PM
– Calendar.HOUR
Livre Java .book Page 126 Jeudi, 25. novembre 2004 3:04 15
126
Au cœur de Java 2 - Notions fondamentales
– Calendar.HOUR_OF_DAY
– Calendar.MINUTE
– Calendar.SECOND
– Calendar.MILLISECOND
– Calendar.ZONE_OFFSET
– Calendar.DST_OFFSET
•
– void set(int field, int value)
Définit la valeur d’un champ particulier.
Paramètres :
•
field
Une des constantes acceptées par get.
value
La nouvelle valeur.
void set(int year, int month, int day)
Définit une nouvelle date.
Paramètres :
•
year
L’année de la date.
month
Le mois de la date, à base 0 (autrement dit : 0 pour janvier).
day
Le jour du mois.
void set(int year, int month, int day, int hour, int minutes, int seconds)
Fournit de nouvelles valeurs pour la date et l’heure.
Paramètres :
•
year
L’année de la date.
month
Le mois de la date, à base 0 (autrement dit : 0 pour janvier).
day
Le jour du mois.
hour
L’heure (de 0 à 23).
minutes
Les minutes (de 0 à 59).
seconds
Les secondes (de 0 à 59).
void add(int field, int amount)
Est une méthode arithmétique. Elle ajoute la quantité spécifiée à un champ. Par exemple, pour
ajouter 7 jours à la date courante, utilisez c.add(Calendar.DAY_OF_MONTH, 7).
Paramètres :
•
field
Le champ à modifier (spécifié à l’aide d’une des constantes
acceptées par get).
amount
Quantité à ajouter au champ (peut être négative).
void setTime(Date time)
Définit le calendrier à cette position dans le temps.
Paramètres :
•
time
La position dans le temps.
Date getTime()
Détermine la position dans le temps représentée par la valeur de cet objet calendrier.
Livre Java .book Page 127 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
127
Construction de vos propres classes
Vous avez pu voir au Chapitre 3 comment construire des classes simples. Ces classes étaient toutes
constituées d’une unique méthode main. Il est temps maintenant d’étudier l’écriture de classes plus
complexes, nécessaires à des applications plus sophistiquées. Ces classes n’ont généralement pas de
méthode main. Elles possèdent en revanche leurs propres méthodes et champs d’instance. Pour
construire un programme complet, vous combinez plusieurs classes, dont l’une possède une
méthode main.
Une classe Employee
La syntaxe la plus simple d’une classe Java est la suivante :
class NomDeClasse
{
constructeur 1
constructeur 2
. . .
méthode1
méthode2
. . .
champ1
champ2
. . .
}
INFO
Nous avons adopté la règle qui consiste à définir les méthodes au début et à placer les champs d’instance à la fin
(d’une certaine manière, cela encourage peut-être l’idée que l’interface doit prendre le pas sur l’implémentation).
Considérons cette version très simplifiée d’une classe Employee qui pourrait être utilisée pour le
registre du personnel d’une entreprise :
class Employee
{
// constructeur
public Employee(String n, double s,
int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar
= new GregorianCalendar(year, month - 1, day);
}
// une méthode
public String getName()
{
return name;
}
// autres méthodes
. . .
Livre Java .book Page 128 Jeudi, 25. novembre 2004 3:04 15
128
Au cœur de Java 2 - Notions fondamentales
// champs d’instance
private String name;
private double salary;
private Date hireDay;
}
Nous analyserons l’implémentation de cette classe dans les sections suivantes. Examinez d’abord
l’Exemple 4.2, qui présente un programme permettant de voir comment on peut utiliser la classe
Employee.
Dans ce programme, nous construisons un Tableau Employee et le remplissons avec trois objets
employee :
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", . . .);
staff[1] = new Employee("Harry Hacker", . . .);
staff[2] = new Employee("Tony Tester", . . .);
Nous utilisons ensuite la méthode raiseSalary de la classe Employee pour augmenter de 5 % le
salaire de chaque employé :
for (Employee e : staff)
e.raiseSalary(5);
Enfin, nous imprimons les informations concernant chaque employé, en appelant les méthodes
getName, getSalary et getHireDay :
for (Employee e : staff)
System.out.println("name=" + e.getName()
+ ",salary=" + e.getSalary()
+ ",hireDay=" + e.getHireDay());
Remarquez que ce programme est constitué de deux classes : la classe Employee et une classe
EmployeeTest ayant un modificateur (ou spécificateur d’accès) public. La méthode main avec les
instructions que nous venons de décrire est contenue dans la classe EmployeeTest.
Le nom du fichier source est EmployeeTest.java, puisque le nom du fichier doit être identique à
celui de la classe public. Vous ne pouvez avoir qu’une classe publique dans un fichier source, mais
le nombre de classes non publiques n’est pas limité.
Quand vous compilez ce code source, le compilateur crée deux fichiers classe dans le répertoire :
EmployeeTest.class et Employee.class.
Vous lancez le programme en donnant à l’interpréteur de bytecode le nom de la classe qui contient la
méthode main de votre programme :
java EmployeeTest
L’interpréteur de bytecode démarre l’exécution par la méthode main de la classe EmployeeTest. A son
tour, le code de cette méthode construit trois nouveaux objets Employee et vous montre leur état.
Exemple 4.2 : EmployeeTest.java
import java.util.*;
public class EmployeeTest
{
public static void main(String[] args)
{
Livre Java .book Page 129 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
// remplir le tableau staff avec trois objets Employee
Employee[] staff = new Employee[3];
staff[0]
1987,
staff[1]
1989,
staff[2]
1990,
= new Employee("Carl Cracker", 75000,
12, 15);
= new Employee("Harry Hacker", 50000,
10, 1);
= new Employee("Tony Tester", 40000,
3, 15);
// augmenter tous les salaires de 5%
for (Employee e : staff)
e.raiseSalary(5);
// afficher les informations concernant
// tous les objets Employee
for (Employee e : staff)
System.out.println("name=" + e.getName()
+ ",salary=" + e.getSalary()
+ ",hireDay=" + e.getHireDay());
}
}
class Employee
{
public Employee(String n, double s,
int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar
= new GregorianCalendar(year, month - 1, day);
// Avec GregorianCalendar 0 désigne janvier
hireDay = calendar.getTime();
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
private String name;
private double salary;
private Date hireDay;
}
129
Livre Java .book Page 130 Jeudi, 25. novembre 2004 3:04 15
130
Au cœur de Java 2 - Notions fondamentales
Travailler avec plusieurs fichiers source
Le programme de l’Exemple 4.2 a deux classes dans un seul fichier source. Nombre de programmeurs préfèrent avoir un fichier source pour chaque classe. Vous pouvez, par exemple, placer la
classe Employee dans un fichier Employee.java et EmployeeTest dans EmployeeTest.java.
Si vous préférez cette organisation, deux possibilités vous sont offertes pour la compilation du
programme. Vous pouvez invoquer le compilateur Java par un appel générique :
javac Employee*.java
Tous les fichiers source correspondants seront compilés en des fichiers de classe. Ou vous pouvez
simplement taper :
javac EmployeeTest.java
Il peut vous paraître surprenant que la seconde possibilité fonctionne, puisque le fichier
Employee.java n’est jamais explicitement compilé. Pourtant, lorsque le compilateur Java verra que
la classe Employee est utilisée dans EmployeeTest.java, il recherchera un fichier
Employee.class. S’il ne le trouve pas, il recherchera automatiquement Employee.java et le
compilera. Mieux encore, si la date de la version de Employee.java qu’il trouve est plus récente que
celle existant dans le fichier Employee.class, le compilateur Java recompilera automatiquement le
fichier.
INFO
Si vous êtes habitué à la fonctionnalité make d’UNIX (ou l’un de ses cousins Windows comme nmake), vous pouvez
imaginer le compilateur Java comme possédant la fonctionnalité make intégrée.
Analyser la classe Employee
Nous allons disséquer la classe Employee dans les sections qui suivent. Commençons par les méthodes.
Comme vous pouvez le voir en examinant le code source, cette classe possède un constructeur et
quatre méthodes :
public
public
public
public
public
Employee(String n, double s, int year, int month, int day)
String getName()
double getSalary()
Date getHireDay()
void raiseSalary(double byPercent)
Toutes les méthodes de cette classe sont publiques. Le mot clé public signifie que les méthodes
peuvent être appelées par n’importe quelle méthode de n’importe quelle classe. Il existe quatre
niveaux d’accès (ou niveaux de visibilité), qui seront décrits dans une prochaine section ainsi qu’au
chapitre suivant.
Remarquez également que trois champs d’instance contiendront les données que nous manipulerons
dans une instance de la classe Employee :
private String name;
private double salary;
private Date hireDay;
Livre Java .book Page 131 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
131
Le mot clé private (privé) assure que les seules méthodes pouvant accéder à ces champs d’instance
sont les méthodes de la classe Employee elle-même. Aucune méthode externe ne peut lire ou écrire
dans ces champs.
INFO
Il est possible d’employer le mot clé public avec vos champs d’instance, mais ce serait une très mauvaise idée. Si des
champs de données sont publics, les champs d’instance peuvent être lus et modifiés par n’importe quelle partie du
programme. Une telle situation irait complètement à l’encontre du principe d’encapsulation. Toute méthode de
toute classe peut modifier les champs publics (et, à notre avis, certaines parties de code profiteront de ce privilège
d’accès au moment où vous vous y attendrez le moins). Nous insistons sur le fait que vos champs d’instance doivent
être privés.
Notez encore que deux des champs d’instance sont eux-mêmes des objets. Les champs name et
hireDay sont des références à des objets String et Date. Il s’agit là d’une situation courante : les
classes contiennent souvent des champs d’instance du type classe.
Premiers pas avec les constructeurs
Examinons le constructeur de la classe Employee :
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar
= new GregorianCalendar(year, month - 1, day);
hireDay = calendar.getTime();
}
Vous constatez que le nom du constructeur est le même que le nom de la classe. Ce constructeur
s’exécute lorsque vous construisez des objets de la classe Employee, et il attribue aux champs
d’instance l’état initial que vous voulez leur donner.
Par exemple, si vous créez une instance de la classe Employee avec des instructions de ce genre :
new Employee("James Bond", 100000, 1950, 1, 1);
les champs d’instance sont affectés de la manière suivante :
name = "James Bond";
salary = 100000;
hireDay = January 1, 1950;
Il existe une différence importante entre les constructeurs et les autres méthodes. Un constructeur
peut seulement être appelé en association avec l’opérateur new. Vous ne pouvez pas appliquer un
constructeur à un objet existant pour redéfinir les champs d’instance. Par exemple,
james.Employee("James Bond", 250000, 1950, 1, 1); // ERREUR
provoquera une erreur de compilation.
Nous reparlerons des constructeurs, mais gardez toujours à l’esprit les points suivants :
m
Un constructeur porte le même nom que la classe.
m
Une classe peut avoir plus d’un constructeur.
Livre Java .book Page 132 Jeudi, 25. novembre 2004 3:04 15
132
Au cœur de Java 2 - Notions fondamentales
m
Un constructeur peut avoir un ou plusieurs paramètres, ou éventuellement aucun.
m
Un constructeur ne renvoie aucune valeur.
m
Un constructeur est toujours appelé à l’aide de l’opérateur new.
INFO C++
Les constructeurs fonctionnent de la même manière en Java et en C++. Mais souvenez-vous que tous les objets Java
sont construits dans la mémoire heap et qu’un constructeur doit être combiné avec new. Les programmeurs C++
oublient facilement l’opérateur new :
Employee number007("James Bond", 100000, 1950, 1 1);
// OK en C++, incorrect en Java.
Cette instruction fonctionne en C++, mais pas en Java.
ATTENTION
Prenez soin de ne pas déclarer des variables locales ayant le même nom que des champs d’instance. Par exemple, le
constructeur suivant n’initialisera pas le salaire (salary) :
public Employee(String n, double s, . . .)
{
String name = n; // ERREUR
double salary = s; // ERREUR
. . .
}
Le constructeur déclare les variables locales name et salary. Ces variables ne sont accessibles que dans le constructeur. Elles éclipsent les champs d’instance de même nom. Certains programmeurs — comme les auteurs de ce livre —
écrivent ce type de code s’ils tapent plus vite qu’ils ne pensent, car leurs doigts ont l’habitude d’ajouter le type de
données. C’est une erreur sournoise qui peut se révéler difficile à détecter. Il faut donc faire attention, dans toutes
les méthodes, à ne pas utiliser des variables qui soient homonymes des champs d’instance.
Paramètres implicites et explicites
Les méthodes agissent sur les objets et accèdent à leurs champs d’instance.
Par exemple, la méthode
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
affecte une nouvelle valeur au champ d’instance salary de l’objet qui exécute cette méthode (celleci, en l’occurrence, ne renvoie aucune valeur). Ainsi, l’instruction
number007.raiseSalary(5);
accroît le salaire de number007 en augmentant la variable number007.salary de 5 %. Plus précisément,
l’appel exécute les instructions suivantes :
double raise = number007.salary * 5 / 100;
number007.salary += raise;
Livre Java .book Page 133 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
133
La méthode raiseSalary a deux paramètres. Le premier, appelé paramètre implicite, est l’objet de
type Employee qui apparaît devant le nom de la méthode lors d’un appel. Le second, situé entre
parenthèses derrière le nom de la méthode, est un paramètre explicite.
Comme vous pouvez le voir, les paramètres explicites sont spécifiés dans la déclaration de la
méthode. Par exemple, double byPercent est explicitement déclaré. Le paramètre implicite
n’apparaît pas dans la déclaration de la méthode.
Dans chaque méthode, le mot clé this fait référence au paramètre implicite. Si vous préférez, vous
pouvez écrire la méthode raiseSalary de la façon suivante :
public void raiseSalary(double byPercent)
{
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
Certains programmeurs préfèrent ce style d’écriture, car il fait clairement la distinction entre les
champs d’instance et les variables locales.
INFO C++
En C++, vous définissez généralement les méthodes en dehors de la classe :
void Employee::raiseSalary(double byPercent) // en C++, pas en Java
{
. . .
}
Si vous définissez une méthode au sein d’une classe, ce sera automatiquement une méthode en ligne :
class Employee
{
. . .
int getName() { return name; } // en ligne en C++
}
Dans le langage Java, toutes les méthodes sont définies dans la classe elle-même. Elles ne sont pas pour autant des
méthodes en ligne. C’est la responsabilité de la machine virtuelle Java de trouver des opportunités pour le remplacement en ligne. Le compilateur en "juste-à-temps" surveille les appels de méthodes courtes, souvent appelées, mais
pas écrasées, puis les optimise.
Avantages de l’encapsulation
Examinons plus attentivement les méthodes, assez simples, getName, getSalary et getHireDay :
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay;
}
Livre Java .book Page 134 Jeudi, 25. novembre 2004 3:04 15
134
Au cœur de Java 2 - Notions fondamentales
Ce sont des exemples manifestes de méthodes d’accès. Comme elles renvoient simplement les
valeurs des champs d’instance, elles sont appelées parfois méthodes d’accès au champ.
Ne serait-il pas plus simple de rendre les champs name, salary et hireDay publics, au lieu d’avoir
des méthodes d’accès séparées ?
Il est important de se rappeler que le champ name est "en lecture seule". Une fois qu’il est défini dans
le constructeur, aucune méthode ne peut le modifier. Nous savons donc que ce champ ne peut jamais
être corrompu.
Le champ salary n’est pas en lecture seule, mais il ne peut être modifié que par la méthode raiseSalary. En particulier, si la valeur du champ se révélait incorrecte, seule cette méthode devrait être
déboguée. Si le champ salary avait été public, le responsable de la corruption de cette valeur pourrait
être n’importe où.
Il peut arriver que vous vouliez lire ou modifier la valeur d’un champ d’instance ; vous devez alors
fournir trois éléments :
m
un champ de données privé ;
m
une méthode publique d’accès à ce champ ;
m
une méthode publique d’altération de ce champ.
Le développement de ces éléments exige plus de travail que la création d’un simple champ public,
mais les bénéfices sont considérables :
1. Il est possible de modifier l’implémentation interne sans affecter aucun autre code que celui des
méthodes de la classe.
Par exemple, si le stockage du nom devient :
String firstName;
String lastName;
la méthode getName peut être modifiée pour renvoyer :
firstName + " " + lastName
Cette modification est totalement invisible pour le reste du programme.
Bien entendu, les méthodes d’accès et d’altération peuvent parfois exiger un gros travail et une
conversion entre les anciennes et les nouvelles données. Mais cela nous amène au second avantage
de cette technique.
2. Les méthodes d’altération peuvent détecter des erreurs, ce que ne peuvent pas faire de simples
instructions d’affectation.
Par exemple, une méthode setSalary peut s’assurer que le salaire n’est jamais inférieur à 0.
ATTENTION
Prenez soin de ne pas écrire des méthodes d’accès qui renvoient des références à des objets altérables. Nous avons
violé cette règle dans notre classe Employee, dans laquelle la méthode gethireDay renvoie un objet de la classe
Date :
class Employee
{
Livre Java .book Page 135 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
135
. . .
public Date getHireDay()
{
return hireDay;
}
. . .
private Date hireDay;
}
Le principe d’encapsulation est violé ! Considérons le code suivant :
Employee harry = . . .;
Date d = harry.getHireDay();
double tenYearsInMilliSeconds = 10 * 365.25 * 24 * 60 * 60 * 1000;
d.setTime(d.getTime() – (long) tenYearsInMilliSeconds);
// ajoutons dix ans d’ancienneté à Harry
La cause du problème est subtile. d et harry.hireDay font référence au même objet (voir Figure 4.5). L’application
de méthodes d’altération à d change automatiquement l’état privé de l’objet employee !
Si vous devez renvoyer une référence à un objet altérable, vous devez d’abord le cloner. Un clone est une copie
conforme d’un objet et cette copie est stockée à un emplacement différent de celui de l’original. Nous étudierons le
clonage plus en détail au Chapitre 6. Voici le code correct :
class Employee
{
. . .
public Date getHireDay()
{
return (Date)hireDay.clone();
}
. . .
}
En résumé, souvenez-vous qu’il faut toujours utiliser clone lorsque vous devez retourner une copie d’un champ de
données altérable.
Figure 4.5
Renvoi d'une référence
Figureà4.5
un champ altérable.
Renvoi d'une
référence à un
champ altérable.
harry =
d=
Employee
name =
salary =
hireDay =
Date
Livre Java .book Page 136 Jeudi, 25. novembre 2004 3:04 15
136
Au cœur de Java 2 - Notions fondamentales
Privilèges d’accès fondés sur les classes
Vous savez qu’une méthode peut accéder aux données privées de l’objet par lequel elle est invoquée.
Certaines personnes trouvent surprenant qu’une méthode puisse accéder aux données privées de tous
les objets de sa classe. Examinons par exemple une méthode equals qui compare deux employés :
class Employee
{
. . .
boolean equals(Employee other)
{
return name.equals(other.name);
}
}
Voici un appel typique :
if (harry.equals(boss)) . . .
Cette méthode accède aux champs privés de harry, ce qui n’est pas surprenant. Elle accède également aux champs privés de boss. C’est une opération parfaitement légale, car boss est un objet
de type Employee, et une méthode de la classe Employee a un droit d’accès aux champs privés de
n’importe quel objet de type Employee.
INFO C++
La même règle existe en C++. Une méthode peut accéder aux caractéristiques privées de n’importe quel objet de sa
classe, et pas seulement à celles du paramètre implicite.
Méthodes privées
Lorsque nous implémentons une classe, nous spécifions que la visibilité de tous les champs de données
est privée, car les données publiques sont d’utilisation risquée. Mais qu’en est-il des méthodes ? Bien
que la plupart des méthodes soient déclarées public, on rencontre fréquemment des méthodes
private. Vous voudrez parfois décomposer le code pour calculer diverses méthodes séparées. Généralement, ces méthodes d’aide (helper) ne doivent pas faire partie de l’interface publique : elles peuvent
être trop proches de l’implémentation actuelle, exiger un protocole spécial ou un type d’appel particulier.
Il est préférable d’implémenter ces méthodes comme privées.
Pour implémenter une méthode privée en Java, il suffit de remplacer le mot clé public par private.
En rendant une méthode privée, nous ne sommes plus tenus de la conserver si nous modifions
l’implémentation de la classe. Si la représentation interne des données est modifiée, cette méthode
pourrait se révéler plus difficile à implémenter ou devenir inutile. Peu importe : tant que la méthode
est privée, les concepteurs de la classe savent qu’elle n’est jamais employée à l’extérieur de la classe
et qu’elle peut donc être supprimée. En revanche, si la méthode est publique, nous ne pouvons pas
simplement l’abandonner, car un autre code peut l’utiliser.
Champs d’instance final
Vous pouvez définir un champ d’instance comme final. Un tel champ doit être initialisé lorsque
l’objet est construit. C’est-à-dire qu’il doit être certain que la valeur du champ est définie après la fin
Livre Java .book Page 137 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
137
de chaque constructeur. Ensuite il ne peut plus être modifié. Par exemple, un champ name de la classe
Employee peut être déclaré comme final puisqu’il ne change jamais après la construction de
l’objet. Il n’y a pas de méthode setName :
class Employee
{
. . .
private final String name;
}
Le modificateur final est particulièrement utile pour les champs de type primitif ou pour une classe
inaltérable (une classe est dite inaltérable lorsque aucune de ses méthodes ne modifie ses objets. Par
exemple, la classe String est inaltérable). Pour les classes modifiables, le modificateur final
risque de jeter la confusion dans l’esprit du lecteur. Par exemple,
private final Date hiredate;
signifie simplement que la référence d’objet stockée dans la variable hiredate n’est pas modifiée
après la construction de l’objet. Cela ne signifie pas pour autant que l’objet est constant. Toute
méthode est libre d’appeler la méthode d’altération setTime sur l’objet auquel fait référence hiredate.
Champs et méthodes statiques
Dans tous les exemples de programmes que vous avez vus, la méthode main est qualifiée de static.
Nous allons maintenant étudier la signification de ce modificateur.
Champs statiques
Si vous définissez un champ comme static, il ne peut en exister qu’un seul par classe. En revanche,
chaque objet a sa propre copie de tous les champs d’instance. Supposons, par exemple, que nous
voulions affecter un numéro unique d’identification à chaque employé. Nous ajoutons un champ
d’instance id et un champ statique nextId à la classe Employee :
class Employee
{
. . .
private int id;
private static int nextId = 1;
}
Maintenant, chaque objet employé possède son propre champ id, mais un seul champ nextId est
partagé entre toutes les instances de la classe. On peut aussi dire qu’il y a à peu près un millier
d’objets de la classe Employee, et par conséquent mille champs d’instance id, un pour chaque objet.
Mais il n’y a qu’un seul champ statique nextId. Même s’il n’y a aucun objet Employee, le champ
statique nextId est présent. Il appartient à la classe, pas à un objet individuel.
INFO
Dans la plupart des langages de programmation orientée objet, les champs statiques sont appelés champs de la
classe. Le terme "static" est un reliquat sans signification de C++.
Livre Java .book Page 138 Jeudi, 25. novembre 2004 3:04 15
138
Au cœur de Java 2 - Notions fondamentales
Implémentons une méthode simple :
public void setId()
{
id = nextId;
nextId++;
}
Supposons que vous définissiez le numéro d’identification d’employé pour harry :
harry.setId();
Le champ id de harry est ensuite défini, et la valeur du champ statique nextId est incrémentée :
harry.id = . . .;
Employee.nextId++;
Constantes
Les variables statiques sont plutôt rares, mais les constantes statiques sont plus courantes. Par exemple,
la classe Math définit une constante statique :
public class Math
{
. . .
public static final double PI = 3.14159265358979323846;
. . .
}
Vous pouvez accéder à cette constante dans vos programmes à l’aide de Math.PI.
Si le mot clé static avait été omis, PI aurait été un champ d’instance de la classe Math. Vous auriez
dû avoir recours à un objet de la classe Math pour accéder à PI, et chaque objet Math aurait eu sa
propre copie de PI.
Une autre constante statique que vous avez souvent utilisée est System.out. Elle est déclarée dans
la classe System :
public class System
{
. . .
public static final PrintStream out = . . .;
. . .
}
Comme nous l’avons déjà mentionné, il n’est jamais souhaitable d’avoir des champs publics, car
tout le monde peut les modifier. Toutefois, les constantes publiques (c’est-à-dire les champs final)
conviennent. Puisque out a été déclaré comme final, vous ne pouvez pas lui réaffecter un autre flux
d’impression :
System.out = new PrintStream(. . .); // ERREUR--out est final
INFO
Si vous examinez la classe System, vous remarquerez une méthode setOut qui vous permet d’affecter System.out
à un flux différent. Vous vous demandez peut-être comment cette méthode peut changer la valeur d’une variable
final. Quoi qu’il en soit, setOut est une méthode native, non implémentée dans le langage de programmation
Java. Les méthodes natives peuvent passer outre les mécanismes de contrôle d’accès de Java. C’est un moyen très
inhabituel de contourner ce problème, et vous ne devez pas l’émuler dans vos propres programmes.
Livre Java .book Page 139 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
139
Méthodes statiques
Les méthodes statiques sont des méthodes qui n’opèrent pas sur les objets. Par exemple, la méthode
pow de la classe Math est une méthode statique. L’expression
Math.pow(x, a)
calcule la puissance xa. Elle n’utilise aucun objet Math pour réaliser sa tâche. Autrement dit, elle n’a
pas de paramètre implicite.
Vous pouvez imaginer les méthodes statiques comme des méthodes n’ayant pas de paramètre this
(dans une méthode non statique, le paramètre this fait référence au paramètre implicite de la
méthode, voir précédemment).
Puisque les méthodes statiques n’opèrent pas sur les objets, vous ne pouvez pas accéder aux champs
d’instance à partir d’une méthode statique. Cependant, les méthodes statiques peuvent accéder aux
champs statiques dans leur classe. En voici un exemple :
public static int getNextId()
{
return nextId; // renvoie un champ statique
}
Pour appeler cette méthode, vous fournissez le nom de la classe :
int n = Employee.getNextId();
Auriez-vous pu omettre le mot clé static pour cette méthode ? Oui, mais vous auriez alors dû avoir
une référence d’objet du type Employee pour invoquer la méthode.
INFO
Il est légal d’utiliser un objet pour appeler une méthode statique. Par exemple, si harry est un objet Employee, vous
pouvez appeler harry.getNextId() au lieu de Employee.getnextId(). Cette écriture peut prêter à confusion.
La méthode getNextId ne consulte pas du tout harry pour calculer le résultat. Nous vous recommandons d’utiliser
les noms de classes, et non les objets, pour invoquer des méthodes statiques.
Vous utilisez les méthodes statiques dans deux cas :
1. Si une méthode n’a pas besoin d’accéder à l’état de l’objet, car tous les paramètres nécessaires
sont fournis comme paramètres explicites (par exemple, Math.pow).
2. Si une méthode n’a besoin d’accéder qu’à des champs statiques de la classe (par exemple :
Employee.getNextId).
INFO C++
Les champs et méthodes statiques ont la même fonctionnalité en Java et en C++. La syntaxe est toutefois légèrement
différente. En C++, vous utilisez l’opérateur "::" pour accéder à un champ ou une méthode statique hors de sa
portée, par exemple Math::PI.
L’historique du terme "static" est curieux. Le mot clé static a été introduit d’abord en C pour indiquer des variables
locales qui ne disparaissaient pas lors de la sortie d’un bloc. Dans ce contexte, le terme "static" est logique : la variable reste et elle est toujours là lors d’une nouvelle entrée dans le bloc. Puis static a eu une autre signification en
C, il désignait des variables et fonctions globales non accessibles à partir d’autres fichiers. Le mot clé static a été
Livre Java .book Page 140 Jeudi, 25. novembre 2004 3:04 15
140
Au cœur de Java 2 - Notions fondamentales
simplement réutilisé pour éviter d’en introduire un nouveau. Enfin, C++ a repris le mot clé avec une troisième interprétation, sans aucun rapport, pour indiquer des variables et fonctions appartenant à une classe, mais pas à un objet
particulier de la classe. C’est cette même signification qu’a ce terme en Java.
Méthodes "factory"
Voici une autre utilisation courante des méthodes statiques. La classe NumberFormat utilise les
méthodes factory, qui produisent des objets de formatage pour divers styles :
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); //affiche $0.10
System.out.println(percentFormatter.format(x)); //affiche 10%
Pourquoi ne pas utiliser plutôt un constructeur ? Pour deux raisons.
m
Vous ne pouvez pas donner de nom aux constructeurs, le nom d’un constructeur est toujours
celui de la classe. Mais nous avons besoin de deux noms différents pour obtenir l’instance
currency et l’instance percent.
m
Lorsque vous utilisez un constructeur, vous ne pouvez pas modifier le type de l’objet construit.
Mais la méthode factory peut renvoyer un objet du type DecimalFormat ou une sous-classe qui
hérite de NumberFormat (voir le Chapitre 5 pour plus de détails sur l’héritage).
La méthode main
Notez que vous pouvez appeler des méthodes statiques sans avoir aucun objet. Par exemple, vous ne
construisez jamais aucun objet de la classe Math pour appeler Math.pow.
Pour la même raison, la méthode main est une méthode statique :
public class Application
{
public static void main(String[] args)
{
// construire les objets ici
. . .
}
}
La méthode main n’opère sur aucun objet. En fait, lorsqu’un programme démarre, il n’existe encore
aucun objet. La méthode statique main s’exécute et construit les objets dont le programme a besoin.
ASTUCE
Chaque classe peut avoir une méthode main. C’est une astuce pratique pour le test unitaire de classes. Vous pouvez
par exemple ajouter une méthode main à la classe Employee :
class Employee
{
public Employee(String n, double s,
int year, int month, int day)
{
name = n;
salary = s;
Livre Java .book Page 141 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
141
GregorianCalendar calendar
= new GregorianCalendar(year, month –1, day);
hireDay = calendar.getTime();
}
. . .
public static void main(String[] args) // test unitaire
{
Employee e = new Employee("Romeo", 50000, 2003, 3, 31);
e.raiseSalary(10);
System.out.println(e.getName() + " " + e.getSalary());
}
. . .
}
Si vous voulez tester isolément la classe Employee, vous exécutez simplement :
java Employee
Si la classe Employee fait partie d’une plus grande application, vous démarrez l’application avec :
java Application
et la méthode main de la classe Employee ne s’exécute jamais.
Le programme de l’Exemple 4.3 contient une version simple de la classe Employee avec un champ
statique nextId et une méthode statique getNextId. Un tableau est rempli avec trois objets
Employee et les informations concernant l’employé sont affichées. Enfin, nous affichons les numéros
d’identification attribués.
Notez que la classe Employee a aussi une méthode main statique pour le test unitaire. Essayez de
lancer les deux :
java Employee
et
java StaticTest
pour exécuter les deux méthodes main.
Exemple 4.3 : StaticTest.java
public class StaticTest
{
public static void main(String[] args)
{
// remplir le tableau staff avec 3 objets Employee
Employee[] staff = new Employee[3];
staff[0] = new Employee("Tom", 40000);
staff[1] = new Employee("Dick", 60000);
staff[2] = new Employee("Harry", 65000);
// imprimer les informations concernant
// tous les objets Employee
for (Employee e : staff)
Livre Java .book Page 142 Jeudi, 25. novembre 2004 3:04 15
142
Au cœur de Java 2 - Notions fondamentales
{
e.setId();
System.out.println("name=" + e.getName()
+ ",id=" + e.getId()
+ ",salary=" + e.getSalary());
}
int n = Employee.getNextId(); // appel méthode statique
System.out.println("Next available id=" + n);
}
}
class Employee
{
public Employee(String n, double s)
{
name = n;
salary = s;
id = 0;
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public int getId()
{
return id;
}
public void setId()
{
id = nextId; // définir id à prochain id disponible
nextId++;
}
public static int getNextId()
{
return nextId; // renvoie un champ statique
}
public static void main(String[] args) // test unitaire
{
Employee e = new Employee("Harry", 50000);
System.out.println(e.getName() + " " + e.getSalary());
}
Livre Java .book Page 143 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
private
private
private
private
Objets et classes
143
String name;
double salary;
int id;
static int nextId = 1;
}
Paramètres des méthodes
Revoyons les termes qui décrivent comment les paramètres peuvent être passés à une méthode
(ou une fonction) dans un langage de programmation. Le terme appel par valeur signifie que la
méthode récupère exactement la valeur fournie par l’appelant. En revanche, un appel par référence signifie que la méthode obtient l’emplacement de la variable fournie par l’appelant. Une
méthode peut donc modifier la valeur stockée dans une variable passée par référence, mais pas
celle d’une variable passée par valeur. Ces termes "appels par..." sont standard en informatique et
décrivent le comportement des paramètres des méthodes dans les différents langages de
programmation, pas seulement en Java (en fait, il existe aussi un appel par nom, dont le principal
intérêt est historique ; il était employé avec le langage Algol, l’un des plus anciens langages de
haut niveau).
Le langage Java utilise toujours l’appel par valeur. Cela signifie que la méthode obtient une copie de
toutes les valeurs de paramètre. En particulier, la méthode ne peut modifier le contenu d’aucun des
paramètres qui lui sont passés.
Par exemple, dans l’appel suivant :
double pourcent = 10;
harry.raiseSalary(percent);
Peu importe comment la méthode est implémentée, nous savons qu’après l’appel à la méthode, la
valeur de percent sera toujours 10.
Examinons cette situation d’un peu plus près. Supposons qu’une méthode tente de tripler la valeur
d’un paramètre de méthode :
public static void tripleValue(double x) // ne marche pas
{
x = 3 * x;
}
Appelons cette méthode :
double percent = 10;
tripleValue(percent);
Cela ne marche pas. Après l’appel à la méthode, la valeur de percent est toujours 10. Voici ce qui se
passe :
1. x est initialisé avec une copie de la valeur de percent (c’est-à-dire 10).
2. x est triplé : il vaut maintenant 30. Mais percent vaut toujours 10 (voir Figure 4.6).
Livre Java .book Page 144 Jeudi, 25. novembre 2004 3:04 15
144
Au cœur de Java 2 - Notions fondamentales
3. La méthode se termine et la variable paramètre x n’est plus utilisée.
Figure 4.6
La modification d’un
paramètre numérique
n’a pas d’effet durable.
Valeur copiée
percent =
10
x=
30
Valeur triplée
Il existe pourtant deux sortes de paramètres de méthode :
m
les types primitifs (nombres, valeurs booléennes) ;
m
les références à des objets.
Vous avez vu qu’il était impossible pour une méthode de modifier un paramètre de type primitif. La
situation est différente pour les paramètres objet. Vous pouvez facilement implémenter une méthode
qui triple le salaire d’un employé :
public static void tripleSalary(Employee x) // ça marche
{
x.raiseSalary(200);
}
Lorsque vous appelez
harry = new Employee(. . .);
tripleSalary(harry);
voici ce qui se passe :
1. x est initialisé avec une copie de la valeur de harry, c’est-à-dire une référence d’objet.
2. La méthode raiseSalary est appliquée à cette référence d’objet. L’objet Employee, auquel font
référence à la fois x et harry, voit son salaire augmenté de 200 %.
3. La méthode se termine et la variable paramètre x n’est plus utilisée. Bien entendu, la variable
objet harry continue à faire référence à l’objet dont le salaire a triplé (voir Figure 4.7).
Livre Java .book Page 145 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
145
Figure 4.7
La modification
d’un paramètre objet
a un effet durable.
Référence
copiée
harry =
Salaire triplé
Employee
x=
Vous avez pu voir qu’il était facile — et en fait très courant — d’implémenter des méthodes qui
changent l’état d’un paramètre objet. La raison en est simple. La méthode obtient une copie de la
référence d’objet, et à la fois l’original et la copie font référence au même objet.
De nombreux langages de programmation (en particulier, C++ et Pascal) disposent de deux méthodes pour le passage de paramètre : l’appel par valeur et l’appel par référence. Certains programmeurs (et malheureusement aussi certains auteurs de livres) affirment que le langage Java a recours
aux appels par référence pour les objets. C’est pourtant une erreur. Elle est d’ailleurs si fréquente que
cela vaut la peine de prendre le temps d’étudier un contre-exemple en détail.
Essayons d’écrire une méthode qui échange deux objets Employee :
public static void swap(Employee x, Employee y) // ne marche pas
{
Employee temp = x;
x = y;
y = temp;
}
Si le langage Java utilisait l’appel par référence pour les objets, cette méthode fonctionnerait :
Employee a = new Employee("Alice", . . .);
Employee b = new Employee("Bob", . . .);
swap(a, b);
// est-ce que a fait maintenant référence à Bob, et b à Alice ?
Cependant, la méthode ne modifie pas réellement les références d’objet qui sont stockées dans les
variables a et b. Les paramètres x et y de la méthode swap sont initialisés avec les copies de ces références. La méthode poursuit l’échange de ces copies :
// x fait référence à Alice, y à Bob
Employee temp = x;
x = y;
y = temp;
// maintenant x fait référence à Bob, y à Alice
Livre Java .book Page 146 Jeudi, 25. novembre 2004 3:04 15
146
Au cœur de Java 2 - Notions fondamentales
Mais, en fin de compte, c’est une perte de temps. Lorsque la méthode se termine, les variables paramètres x et y sont abandonnées. Les variables originales a et b font toujours référence aux mêmes
objets, comme avant l’appel à la méthode (voir Figure 4.8).
Figure 4.8
L’échange de paramètres
objet n’a pas d’effet
durable.
Références
copiées
Employee
alice =
bob =
x=
Employee
y=
Références
échangées
Cet exemple montre que le langage Java n’utilise pas l’appel par référence pour les objets. Les références d’objet sont passées par valeur.
Voici un récapitulatif de ce que vous pouvez faire et ne pas faire, avec les paramètres d’une méthode,
en langage Java :
m
Une méthode ne peut pas modifier un paramètre de type primitif (c’est-à-dire des nombres ou
des valeurs booléennes).
m
Une méthode peut modifier l’état d’un paramètre objet.
m
Une méthode ne peut pas modifier un paramètre objet pour qu’il fasse référence à un nouvel
objet.
Le programme de l’Exemple 4.4 en fait la démonstration. Il essaie d’abord de tripler la valeur d’un
paramètre numérique et n’y parvient pas :
Testing tripleValue:
Before: percent=10.0
End of method: x=30.0
After: percent=10.0
Il parvient ensuite à tripler le salaire d’un employé :
Testing tripleSalary:
Before: salary=50000.0
End of method: salary=150000.0
After: salary=150000.0
Livre Java .book Page 147 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
147
Après appel à la méthode, l’état de l’objet auquel harry fait référence a changé. Cela est possible,
car la méthode en a modifié l’état par l’intermédiaire d’une copie de la référence d’objet.
Enfin, le programme met en évidence l’échec de la méthode swap :
Testing swap:
Before: a=Alice
Before: b=Bob
End of method: x=Bob
End of method: y=Alice
After: a=Alice
After: b=Bob
Vous pouvez voir que les variables paramètres x et y sont échangées, mais que les variables a et b ne
sont pas affectées.
INFO C++
C++ réalise des appels à la fois par valeur et par référence. Les paramètres par référence sont balisés par &. Par exemple,
vous pouvez facilement implémenter des méthodes
void tripleValue(double& x)
ou
void swap(Employee& x, Employee& y)
qui modifient leurs paramètres de référence.
Exemple 4.4 : ParamTest.java
public class ParamTest
{
public static void main(String[] args)
{
/*
Test 1: les méthodes ne peuvent pas modifier
des paramètres numériques
*/
System.out.println("Testing tripleValue:");
double percent = 10;
System.out.println("Before: percent=" + percent);
tripleValue(percent);
System.out.println("After: percent=" + percent);
/*
Test 2: les méthodes peuvent changer l’état
des paramètres objets
*/
System.out.println("\nTesting tripleSalary:");
Employee harry = new Employee("Harry", 50000);
System.out.println("Before: salary=" + harry.getSalary());
tripleSalary(harry);
System.out.println("After: salary=" + harry.getSalary());
/*
Test 3: les méthodes ne peuvent pas attacher de
nouveaux objets aux paramètres objet
*/
System.out.println("\nTesting swap:");
Livre Java .book Page 148 Jeudi, 25. novembre 2004 3:04 15
148
Au cœur de Java 2 - Notions fondamentales
Employee a = new Employee("Alice", 70000);
Employee b = new Employee("Bob", 60000);
System.out.println("Before: a=" + a.getName());
System.out.println("Before: b=" + b.getName());
swap(a, b);
System.out.println("After: a=" + a.getName());
System.out.println("After: b=" + b.getName());
}
public static void tripleValue(double x) // ne marche pas
{
x = 3 * x;
System.out.println("End of method: x=" + x);
}
public static void tripleSalary(Employee x) // ça marche
{
x.raiseSalary(200);
System.out.println("End of method: salary="
+ x.getSalary());
}
public static void swap(Employee x, Employee y)
{
Employee temp = x;
x = y;
y = temp;
System.out.println("End of method: x=" + x.getName());
System.out.println("End of method: y=" + y.getName());
}
}
class Employee // classe Employee simplifiée
{
public Employee(String n, double s)
{
name = n;
salary = s;
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
private String name;
private double salary;
}
Livre Java .book Page 149 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
149
Construction d’un objet
Nous avons vu comment écrire des constructeurs simples qui définissent l’état initial de nos objets.
Cependant, comme la construction d’un objet est une opération primordiale, Java offre une grande
diversité de mécanismes permettant d’écrire des constructeurs. Nous allons maintenant étudier ces
mécanismes.
Surcharge
Nous avons vu que la classe GregorianCalendar possédait plusieurs constructeurs. Nous pouvons
employer :
GregorianCalendar today = new GregorianCalendar();
ou :
GregorianCalendar deadline
= new GregorianCalendar(2099, Calendar.DECEMBER, 31);
Cette fonctionnalité s’appelle surcharge. La surcharge s’effectue si plusieurs méthodes possèdent le
même nom (dans ce cas précis, la méthode du constructeur GregorianCalendar), mais des paramètres différents. Le compilateur Java se charge de déterminer laquelle il va employer. Il choisit la
méthode correcte en comparant le type des paramètres des différentes déclarations avec celui des
valeurs transmises lors de l’appel. Une erreur de compilation se produit si le compilateur se révèle
incapable d’apparier les paramètres ou si plusieurs correspondances sont possibles. Ce processus est
appelé résolution de surcharge.
INFO
Java permet de surcharger n’importe quelle méthode — pas seulement les constructeurs. Par conséquent, pour
décrire complètement une méthode, vous devez spécifier le nom de la méthode ainsi que les types de ses paramètres.
Cela s’appelle la signature de la méthode. Par exemple, la classe String a quatre méthodes appelées indexOf.
Leurs signatures sont :
indexOf(int)
indexOf(int, int)
indexOf(String)
indexOf(String, int)
Le type renvoyé ne fait pas partie de la signature de la méthode. C’est-à-dire que vous ne pouvez pas avoir deux
méthodes avec le même nom et les mêmes types de paramètres, et seulement des types renvoyés qui diffèrent.
Initialisation des champs par défaut
Si vous ne définissez pas un champ explicitement dans un constructeur, une valeur par défaut lui est
automatiquement attribuée : 0 pour les nombres, false pour les valeurs booléennes, et null pour les
références d’objet. On considère généralement que ce n’est pas une bonne pratique de compter aveuglément sur ce mécanisme. Il est bien évidemment plus difficile à un tiers de comprendre votre code
si les variables sont initialisées de manière invisible.
Livre Java .book Page 150 Jeudi, 25. novembre 2004 3:04 15
150
Au cœur de Java 2 - Notions fondamentales
INFO
Il existe une différence importante entre les champs et les variables locales. Vous devez toujours explicitement initialiser les variables locales dans une méthode, mais si vous n’initialisez pas un champ dans une classe, il prend automatiquement la valeur par défaut (0, false ou null).
Prenez, par exemple, la classe Employee. Supposons que vous ne précisiez pas comment initialiser
certains des champs dans un constructeur. Par défaut, le champ salary sera initialisé à 0 et les
champs name et hireDay auront la valeur null.
Cela n’est toutefois pas souhaitable, car si quelqu’un appelle la méthode getName ou getHireDay, il
obtiendra une référence null qu’il n’attendait certainement pas :
Date h = harry.getHireDay();
calendar.setTime(h); // lance une exception si h vaut null
Constructeurs par défaut
Un constructeur par défaut est un constructeur sans paramètres. Par exemple, voici un constructeur
par défaut pour la classe Employee :
public Employee()
{
name = "";
salary = 0;
hireDay = new Date();
}
Si vous écrivez une classe sans fournir de constructeur, Java en fournit automatiquement un par
défaut. Ce dernier affecte une valeur par défaut à tous les champs d’instance. Ainsi, toutes les
données numériques des champs prennent la valeur 0, tous les booléens reçoivent la valeur false et
toutes les variables objet sont initialisées avec la valeur null.
Si une classe fournit au moins un constructeur, mais ne fournit pas de constructeur par défaut, il est
illégal de construire des objets sans paramètres de construction. Par exemple, notre classe d’origine
Employee dans l’Exemple 4.2 fournissait un unique constructeur :
Employee(String name, double salary, int y, int m, int d)
Il n’est pas légal, avec cette classe, de construire des employés par défaut. C’est-à-dire que l’appel
e = new Employee();
provoquerait une erreur.
ATTENTION
Retenez bien qu’un constructeur par défaut est fourni uniquement si votre classe ne possède pas d’autre constructeur. Si vous écrivez même un seul constructeur pour votre classe et que vous vouliez que les utilisateurs de votre
classe puissent en créer une instance par un appel à
new NomDeClasse()
Livre Java .book Page 151 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
151
vous devez fournir un constructeur par défaut explicite (sans arguments). Bien entendu, si les valeurs par défaut vous
conviennent pour tous les champs, vous pouvez simplement écrire :
public NomDeClasse()
{
}
Initialisation explicite de champ
Puisque vous pouvez surcharger les méthodes du constructeur dans une classe, la construction peut
se faire de plusieurs façons pour définir l’état initial des champs d’instance de vos classes. Il est
toujours souhaitable de s’assurer que, indépendamment de l’appel au constructeur, chaque champ
d’instance est défini à une valeur significative.
Vous pouvez simplement affecter une valeur à tous les champs dans la définition de classe. Par
exemple :
class Employee
{
. . .
private String name = "";
}
Cette affectation est réalisée avant l’exécution du constructeur. Cette syntaxe est particulièrement
utile si tous les constructeurs d’une classe ont besoin de définir un champ d’instance particulier à la
même valeur.
L’initialisation n’est pas nécessairement une valeur constante. Voici un exemple de champ initialisé
à l’aide d’un appel de méthode. Il s’agit d’une classe Employee où chaque employé a un champ id.
Vous pouvez l’initialiser de la façon suivante :
class Employee
{
. . .
static int assignId()
{
int r = nextId;
nextId++;
return r;
}
. . .
private int id = assignId();
}
INFO C++
En C++, il n’est pas possible d’initialiser directement les champs d’instance d’une classe. Tous les champs doivent être
définis dans un constructeur. Toutefois, C++ dispose d’une syntaxe spéciale, la liste d’initialisation :
Employee::Employee(String n, double s,
int y, int m, int d) // C++
: name(n),
salary(s),
hireDay(y, m, d)
{
}
Livre Java .book Page 152 Jeudi, 25. novembre 2004 3:04 15
152
Au cœur de Java 2 - Notions fondamentales
C++ utilise cette syntaxe spéciale pour appeler des constructeurs de champ. En Java, c’est inutile, car les objets n’ont
pas de sous-objets, seulement des pointeurs sur d’autres objets.
Noms de paramètres
Si vous écrivez des constructeurs très triviaux (et vous en écrirez beaucoup), la question des noms de
paramètres peut devenir fastidieuse.
Nous avons, en général, opté pour des noms de paramètres d’un seul caractère :
public Employee(String n, double s)
{
name = n;
salary = s;
}
L’inconvénient est qu’il faut lire le code pour savoir ce que signifient les paramètres n et s.
Certains programmeurs préfixent chaque paramètre avec un "a" :
public Employee(String aName, double aSalary)
{
name = aName;
salary = aSalary;
}
C’est très clair, n’importe quel lecteur peut immédiatement savoir ce que les paramètres désignent.
Une autre astuce est couramment utilisée. Elle s’appuie sur le fait que les variables paramètres éclipsent les champs d’instance de même nom. Si vous appelez un paramètre salary, ce nom salary fait
référence au paramètre, et non au champ d’instance. Mais vous pouvez toujours accéder au champ
d’instance à l’aide de this.salary. Souvenez-vous que this désigne le paramètre implicite, c’està-dire l’objet qui est en train d’être construit. Voici un exemple :
public Employee(String name, double salary)
{
this.name = name;
this.salary = salary;
}
INFO C++
En C++, il est courant de préfixer les champs d’instance avec un caractère de soulignement (_) ou une lettre fixe. Les
lettres "m" et "x" sont fréquemment choisies. Par exemple, le champ salary peut être appelé _salary, mSalary
ou xSalary. Ce n’est pas une pratique courante en langage Java.
Appel d’un autre constructeur
Le mot clé this fait référence au paramètre implicite d’une méthode. Il existe cependant une autre
signification pour ce mot clé.
Si la première instruction d’un constructeur a la forme this(...), ce constructeur appelle un autre
constructeur de la même classe. Voici un exemple caractéristique :
public Employee(double s)
{
Livre Java .book Page 153 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
153
// appelle Employee(String, double)
this("Employee #" + nextId, s);
nextId++;
}
Lorsque vous appelez new Employee(60000), le constructeur Employee(double) appelle le
constructeur Employee(String, double).
Cet emploi du mot clé this est pratique, vous n’avez besoin d’écrire le code de construction
commun qu’une seule fois.
INFO C++
L’objet this de Java est identique au pointeur this de C++. Néanmoins, en C++, il n’est pas possible pour un
constructeur d’en appeler un autre. Si vous souhaitez partager du code d’initialisation en C++, vous devez écrire une
méthode séparée.
Blocs d’initialisation
Nous avons déjà vu deux façons d’initialiser un champ de données :
m
en spécifiant une valeur dans un constructeur ;
m
en affectant une valeur initiale dans la déclaration.
Il existe en fait un troisième mécanisme, appelé bloc d’initialisation. Une déclaration de classe peut
contenir des blocs de code arbitraires qui sont exécutés chaque fois qu’un objet de cette classe est
construit. Par exemple :
class Employee
{
public Employee(String n, double s)
{
name = n;
salary = s;
}
public Employee()
{
name = "";
salary = 0;
}
. . .
private static int nextId;
private int id;
private String name;
private double salary;
...
// bloc d’initialisation d’objet
{
id = nextId;
nextId++;
}
}
Livre Java .book Page 154 Jeudi, 25. novembre 2004 3:04 15
154
Au cœur de Java 2 - Notions fondamentales
Dans cet exemple, le champ id est initialisé dans le bloc d’initialisation d’objet, peu importe le
constructeur utilisé pour construire un objet. Le bloc d’initialisation s’exécute en premier, avant
le corps du constructeur.
Ce mécanisme n’est jamais nécessaire et il n’est pas élégant. Il est généralement plus clair de placer
le code d’initialisation à l’intérieur d’un constructeur.
INFO
La définition des champs dans les blocs d’initialisation est autorisée, même s’ils ne sont définis que plus loin dans la
classe. Certaines versions du compilateur Java de Sun géraient incorrectement cette situation (bogue n˚ 4459133). Ce
bogue avait été résolu dans le JDK 1.4.1. Or, pour éviter des définitions circulaires, vous ne pouvez pas lire à partir de
champs qui ne soient initialisés que par la suite. Les règles exactes sont énumérées dans la section 8.3.2.3 des caractéristiques du langage Java (http://java.sun.com/docs/books/jls). Ces règles étant suffisamment complexes pour
tromper l’implémenteur, nous vous conseillons de placer les blocs d’initialisation après les définitions de champs.
Avec toutes ces techniques d’initialisation, il est difficile d’indiquer toutes les manières d’effectuer
une construction d’objet. Voici en détail ce qui se passe lorsqu’un constructeur est appelé :
1. Tous les champs de données sont initialisés à leurs valeurs par défaut (0, false ou null).
2. Tous les initialiseurs de champ et les blocs d’initialisation sont exécutés, dans l’ordre de leur
apparition à l’intérieur de la déclaration de classe.
3. Si la première ligne du constructeur appelle un second constructeur, le corps du second constructeur
est exécuté.
4. Le corps du constructeur est exécuté.
Naturellement, il est toujours judicieux d’organiser le code d’initialisation de sorte que l’on puisse
aisément le comprendre sans être un théoricien du langage. Par exemple, il serait curieux et dangereux de créer une classe dont les constructeurs dépendent de l’ordre dans lequel sont déclarés les
champs de données.
Un champ statique est initialisé, soit en spécifiant une valeur initiale, soit en utilisant un bloc
d’initialisation statique. Vous avez déjà vu le premier de ces mécanismes :
static int nextId = 1;
Si les champs statiques de votre classe requièrent un code d’initialisation complexe, utilisez un bloc
d’initialisation statique.
Placez le code dans un bloc balisé à l’aide du mot clé static. Voici un exemple. Les numéros d’ID
d’employés doivent débuter à un nombre entier inférieur à 10 000 :
// bloc d’initialisation statique
static
{
Random generator = new Random();
nextId = generator.nextInt(10000);
}
L’initialisation statique est exécutée au premier chargement de la classe. Comme les champs
d’instance, les champs statiques valent 0, false ou null à moins que vous ne les ayez explicitement
Livre Java .book Page 155 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
155
définis à une autre valeur. Tous les initialiseurs de champs statiques et les blocs d’initialisation statiques
sont exécutés dans l’ordre de leur apparition dans la déclaration de classe.
INFO
Voici une petite fantaisie de Java qui pourra étonner vos collègues programmeurs : il est possible de créer un
programme "Hello, World" en Java sans même écrire une méthode main :
public class Hello
{
static
{
System.out.println("Hello, World");
}
}
Lorsque vous invoquez la classe avec l’instruction java Hello, la classe est chargée, le bloc d’initialisation statique affiche "Hello, World", et c’est seulement ensuite que vous obtenez un affreux message d’erreur vous signalant que
main n’est pas définie. Vous pouvez l’éviter en appelant System.exit(0) à la fin du bloc d’initialisation statique.
Le programme de l’Exemple 4.5 montre plusieurs des fonctionnalités abordées dans cette section :
m
la surcharge de constructeurs ;
m
l’appel d’un autre constructeur à l’aide de this(...) ;
m
un constructeur par défaut ;
m
un bloc d’initialisation d’objet ;
m
un bloc d’initialisation statique ;
m
l’initialisation de champ d’instance.
Exemple 4.5 : ConstructorTest.java
import java.util.*;
public class ConstructorTest
{
public static void main(String[] args)
{
// remplir le tableau staff avec 3 objets Employee
Employee[] staff = new Employee[3];
staff[0] = new Employee("Harry", 40000);
staff[1] = new Employee(60000);
staff[2] = new Employee();
// afficher les informations concernant
// tous les objets Employee
for (Employee e : staff)
System.out.println("name=" + e.getName()
+ ",id=" + e.getId()
+ ",salary=" + e.getSalary());
}
}
class Employee
Livre Java .book Page 156 Jeudi, 25. novembre 2004 3:04 15
156
Au cœur de Java 2 - Notions fondamentales
{
// trois constructeurs surchargés
public Employee(String n, double s)
{
name = n;
salary = s;
}
public Employee(double s)
{
// appelle le constructeur Employee(String, double)
this("Employee #" + nextId, s);
}
// le constructeur par défaut
public Employee()
{
// name initialisé à ""--voir plus bas
// salary non défini explicitement--initialisé à 0
// id initialisé dans le bloc d’initialisation
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public int getId()
{
return id;
}
private static int nextId;
private int id;
private String name = ""; // initialisation champ d’instance
private double salary;
// bloc d’initialisation statique
static
{
Random generator = new Random();
// définir nextId à un nombre aléatoire
// entre 0 et 9999
nextId = generator.nextInt(10000);
}
// bloc d’initialisation d’objet
{
id = nextId;
nextId++;
}
}
java.util.Random 1.0
•
Random()
Construit un nouveau générateur de nombres aléatoires.
Livre Java .book Page 157 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
•
Objets et classes
157
int nextInt(int n) 1.2
Renvoie un nombre aléatoire entre 0 et n - 1.
Destruction des objets et méthode finalize
De nombreux langages, tels que C++, disposent de destructeurs explicites permettant d’accomplir
les opérations de nettoyage nécessaires à la libération de la mémoire allouée aux objets. Puisque
Java, par l’intermédiaire du garbage collector (ou ramasse-miettes), effectue un nettoyage automatique, la récupération manuelle de la mémoire n’est pas nécessaire et, par conséquent, Java ne prend
pas en charge les destructeurs.
Bien entendu, certains objets utilisent d’autres ressources que la mémoire, par exemple un fichier ou
un handle sur un autre objet qui opère sur des ressources système. Dans ce cas, il est important de
récupérer et de libérer ces ressources lorsqu’elles ne sont plus utilisées.
Java permet d’ajouter une méthode finalize à n’importe quelle classe. Cette méthode sera appelée
avant que le ramasse-miettes ne détruise l’objet. En pratique, ne comptez pas sur la méthode finalize pour récupérer les ressources qui sont en quantité limitée, car vous ne pouvez pas savoir exactement à quel moment elle est appelée.
INFO
Il existe une méthode System.runFinalizersOnExit(true) pour vous assurer que les méthodes de finalisation
sont appelées avant la fermeture de Java. Cette méthode n’est toutefois pas sûre et est maintenant dépréciée.
A la place, vous pouvez ajouter des "crochets de fermeture" avec la méthode Runtime.addShutdownHook
(voir la documentation API pour en savoir plus).
Si une ressource doit être libérée dès que vous avez fini de l’utiliser, vous devez effectuer cette libération manuellement. Ajoutez une méthode dispose que vous appellerez pour nettoyer ce qui doit
l’être. De même, si vous utilisez une classe qui possède une méthode dispose, appelez cette
méthode dès que vous n’avez plus besoin de l’objet.
Packages
Java permet de regrouper des classes dans un ensemble (ou un paquet) appelé package. Les packages
se révèlent pratiques pour l’organisation de votre travail et pour effectuer une séparation entre vos
créations et les bibliothèques fournies par des tiers.
La bibliothèque standard de Java est distribuée dans un certain nombre de packages, y compris
java.lang, java.util, java.net, etc. Les packages standard de Java constituent des exemples de
packages hiérarchiques. Tout comme les répertoires d’un disque dur, les packages peuvent être organisés suivant plusieurs niveaux d’imbrication. Tous les packages standard de Java se trouvent au sein
des hiérarchies de package java et javax.
L’utilisation des packages permet de s’assurer que le nom de chaque classe est unique. Supposons
que deux programmeurs aient la brillante idée de fournir une classe Employee. Tant qu’ils placent
leurs classes dans des packages différents, il n’y a pas de conflit. En fait, pour s’assurer vraiment que
le nom d’un package est unique, Sun recommande d’utiliser comme préfixe le nom du domaine
Livre Java .book Page 158 Jeudi, 25. novembre 2004 3:04 15
158
Au cœur de Java 2 - Notions fondamentales
Internet de votre société (a priori unique), écrit dans l’ordre inverse de ses éléments. Vous utilisez
ensuite des sous-packages pour les différents projets. Par exemple, horstmann.com est un domaine
enregistré par l’un des auteurs. Inversé, il devient le package com.horstmann. Ce package peut
encore être subdivisé en sous-packages tels que com.horstmann.corejava.
Le seul intérêt de l’imbrication de packages concerne la gestion de noms uniques. Du point de vue
du compilateur, il n’y a absolument aucune relation entre les packages imbriqués. Par exemple, les
packages java.util et java.util.jar n’ont rien à voir l’un avec l’autre. Chacun représente sa
propre collection indépendante de classes.
Importation des classes
Une classe peut utiliser toutes les classes de son propre package et toutes les classes publiques des
autres packages.
Vous pouvez accéder aux classes publiques d’un autre package de deux façons. La première consiste
simplement à ajouter le nom complet du package devant chaque nom de classe. Par exemple :
java.util.Date today = new java.util.Date();
C’est une technique plutôt contraignante. Il est plus simple d’avoir recours à import. La directive
import constitue un raccourci permettant de faire référence aux classes du package. Une fois que
cette directive est spécifiée, il n’est plus nécessaire de donner aux classes leur nom complet.
Vous pouvez importer une classe spécifique ou l’ensemble d’un package. Vous placez les instructions import en tête de vos fichiers source (mais au-dessous de toutes les instructions package). Par
exemple, vous pouvez importer toutes les classes du package java.util avec l’instruction :
import java.util.*;
Vous pouvez alors écrire
Date today = new Date();
sans le préfixe du package. Il est également possible d’importer une classe spécifique d’un package :
import java.util.Date;
La syntaxe java.util.* est moins compliquée. Cela n’entraîne aucun effet négatif sur la taille du
code.Si vous importez explicitement des classes, le lecteur de votre code connaît exactement les
classes utilisées.
ASTUCE
Dans Eclipse, vous pouvez sélectionner l’option de menu Source/Organize Imports. Les instructions des packages
comme import java.util.* sont automatiquement étendues en une liste d’importations spécifiques comme :
import java.util.ArrayList;
import java.util.Date;
C’est une fonctionnalité très commode.
Sachez toutefois que vous ne pouvez utiliser la notation * que pour importer un seul package. Vous
ne pouvez pas utiliser import java.* ni import java.*.* pour importer tous les packages ayant
le préfixe java.
Livre Java .book Page 159 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
159
Le plus souvent, vous importez simplement les packages dont vous avez besoin, sans autre préoccupation. La seule circonstance demandant une attention particulière est le conflit de noms. Par
exemple, à la fois les packages java.util et java.sql ont une classe Date. Supposons que vous
écriviez un programme qui importe les deux packages :
import java.util.*;
import java.sql.*;
Si vous utilisez maintenant la classe Date, vous obtiendrez une erreur de compilation :
Date today; // ERREUR--java.util.Date ou java.sql.Date?
Le compilateur ne peut déterminer de quelle classe Date vous avez besoin. Ce problème peut être
résolu par l’ajout d’une instruction import spécifique :
import java.util.*;
import java.sql.*;
import java.util.Date;
Mais si vous avez réellement besoin des deux classes Date ? Vous devez alors utiliser le nom
complet du package avec chaque nom de classe :
java.util.Date deadline = new java.util.Date();
java.sql.Date today = new java.sql.Date();
La localisation des classes dans les packages est le rôle du compilateur. Les bytecodes dans les
fichiers de classe utilisent toujours les noms complets de packages pour faire référence aux autres
classes.
INFO C++
Les programmeurs C++ font souvent une confusion entre import et #include. Ces deux directives n’ont rien de
commun. En C++, il faut employer #include pour inclure les déclarations des composants externes parce que le
compilateur C++ ne consulte aucun fichier à part celui qu’il compile et les fichiers d’en-têtes explicitement spécifiés.
Le compilateur Java, pour sa part, cherchera dans d’autres fichiers à condition que vous lui fournissiez une directive
de recherche.
En Java, il est possible d’éviter complètement le mécanisme import en nommant explicitement tous les packages,
comme java.util.Date. En C++, vous ne pouvez pas éviter les directives #include.
La directive import est purement une facilité du langage permettant de se référer à une classe en lui donnant un
nom plus court que celui complet du package. Par exemple, après une instruction import java.util.* (ou
import java.util.Date), vous pouvez faire référence à la classe java.util.Date en l’appelant simplement
Date.
En C++, une construction analogue au package est la fonctionnalité d’espace de nom (namespace). Songez aux mots
clés package et import de Java comme à des équivalents des directives namespace et using de C++.
Imports statiques
Depuis le JDK 5.0, l’instruction import a été améliorée de manière à permettre l’importation de
méthodes et de champs statiques, et non plus simplement des classes.
Par exemple, si vous ajoutez la directive
import static java.lang.System.*;
Livre Java .book Page 160 Jeudi, 25. novembre 2004 3:04 15
160
Au cœur de Java 2 - Notions fondamentales
en haut de votre fichier source, vous pouvez utiliser les méthodes et les champs statiques de la classe
Système, sans préfixe du nom de classe :
out.println("Goodbye, World!"); // c’est-à-dire System.out
exit(0); // c’est-à-dire System.exit
Vous pouvez également importer une méthode ou un champ spécifique :
import static java.lang.System.out;
Dans la pratique, il semble douteux que de nombreux programmeurs souhaitent abréger System.out
ou System.exit. Le résultat est moins clair. Mais il existe deux utilisations pratiques des imports
statiques.
1. Les fonctions mathématiques. Si vous utilisez un import statique pour la classe Math, vous
pouvez utiliser des fonctions mathématiques d’une manière naturelle. Par exemple,
sqrt(pow(x, 2) + pow(y, 2))
semble plus clair que
Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
2. Des constantes encombrantes. Si vous utilisez de nombreuses constantes avec des noms
compliqués, l’import statique vous ravira. Par exemple,
if (d.get(DAY_OF_WEEK) == MONDAY)
est plus agréable à l’œil que
if (d.get(Calendar.DAY_OF_WEEK) == Calendar.MONDAY)
Ajout d’une classe dans un package
Pour placer des classes dans un package, vous devez mettre le nom du package en haut de votre
fichier source, avant le code qui définit les classes dans le package. Par exemple, le fichier
Employee.java dans l’Exemple 4.7 commence ainsi :
package com.horstmann.corejava;
public class Employee
{
. . .
}
Si vous ne mettez pas une instruction package dans le fichier source, les classes dans ce fichier
source appartiendront au package par défaut qui n’a pas de nom de package. Jusqu’à présent, tous
nos exemples de classes se trouvaient dans le package par défaut.
Vous placez les fichiers d’un package dans un sous-répertoire correspondant au nom complet du
package. Par exemple, tous les fichiers de classe dans le package com.horstmann.corejava doivent
se trouver dans le sous-répertoire com/horstmann/corejava (com\horstmann\corejava sous
Windows).
Le programme des Exemples 4.6 et 4.7 est réparti sur deux packages : la classe PackageTest
appartient au package par défaut et la classe Employee au package com.horstmann.corejava.
Livre Java .book Page 161 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
161
Le fichier Employee.class doit donc se trouver dans un sous-répertoire com/horstmann/corejava.
En d’autres termes, la structure de répertoire est la suivante :
. (répertoire courant)
PackageTest.java
PackageTest.class
com/
horstmann/
corejava/
Employee.java
Employee.class
Pour compiler ce programme, positionnez-vous dans le répertoire de base et lancez la commande :
javac PackageTest.java
Le compilateur recherche automatiquement le fichier com/horstmann/corejava/Employee.java
et le compile.
Etudions un exemple plus réaliste, dans lequel nous n’utilisons pas le package par défaut, mais dont
les classes sont distribuées sur plusieurs packages (com.horstmann.corejava et com.mycompany) :
. (répertoire courant)
com/
horstmann/
corejava/
Employee.java
Employee.class
mycompany/
PayrollApp.java
PayrollApp.class
Dans cette situation, vous devez toujours compiler et exécuter des classes depuis le répertoire de
base, c’est-à-dire le répertoire contenant le répertoire com :
javac com/mycompany/PayrollApp.java
java com.mycompany.PayrollApp
N’oubliez pas que le compilateur fonctionne sur les fichiers (avec les séparateurs de fichiers et une
extension .java), tandis que l’interpréteur Java charge une classe (avec des séparateurs par point).
ATTENTION
Le compilateur ne vérifie pas les répertoires lorsqu’il compile les fichiers source. Supposons, par exemple, que vous
ayez un fichier source commençant par la directive :
package com.mycompany;
Vous pouvez compiler le fichier, même s’il ne se trouve pas dans un sous-répertoire com/mycompany. La compilation
se déroulera sans erreurs, s’il ne dépend pas des autres packages. Toutefois, la machine virtuelle ne trouvera pas les
classes résultantes lorsque vous tenterez d’exécuter le programme.
Exemple 4.6 : PackageTest.java
import com.horstmann.corejava.*;
// La classe Employee est définie dans ce package
import static java.lang.System.*;
public class PackageTest
{
Livre Java .book Page 162 Jeudi, 25. novembre 2004 3:04 15
162
Au cœur de Java 2 - Notions fondamentales
public static void main(String[] args)
{
// du fait de l’instruction import, nous n’utilisons pas
// com.horstmann.corejava.Employee ici
Employee harry = new Employee("Harry Hacker", 50000,
1989, 10, 1);
// augmenter le salaire de 5%
harry.raiseSalary(5);
// afficher les informations concernant harry
// utiliser java.lang.System.out ici
out.println("name=" + harry.getName()
+ ",salary=" + harry.getSalary());
}
}
Exemple 4.7 : Employee.java
package com.horstmann.corejava;
// les classes de ce fichier font partie de ce package
import java.util.*;
// les instructions import viennent après l’instruction package
public class Employee
{
public Employee(String n, double s,
int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar
= new GregorianCalendar(year, month - 1, day);
// GregorianCalendar utilise 0 pour janvier
hireDay = calendar.getTime();
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
private String name;
private double salary;
private Date hireDay;
}
Livre Java .book Page 163 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
163
Comment la machine virtuelle localise les classes
Vous avez vu que les classes étaient stockées dans des sous-répertoires du système de fichiers. Le
chemin d’accès de la classe doit correspondre au nom du package. Vous pouvez aussi utiliser l’utilitaire JAR pour ajouter des fichiers de classe à un archive. Un archive contient plusieurs fichiers de
classe et des sous-répertoires dans un seul fichier, ce qui économise l’espace et réduit le temps
d’accès (nous étudierons les fichiers JAR plus en détail au Chapitre 10).
Par exemple, les milliers de classes de la bibliothèque d’exécution sont toutes contenues dans le fichier
de la bibliothèque d’exécution rt.jar. Ce fichier se trouve dans le sous-répertoire jre/lib du JDK.
ASTUCE
Les fichiers JAR ont recours au format ZIP pour l’organisation des fichiers et sous-répertoires. Vous pouvez utiliser
n’importe quel utilitaire ZIP pour explorer rt.jar et les autres fichiers JAR.
Dans l’exemple de programme précédent, le répertoire du package com/horstmann/corejava était
un sous-répertoire du répertoire du programme. Cette organisation n’est cependant pas très souple.
Généralement de nombreux programmes doivent avoir accès aux fichiers de package. Pour rendre
vos packages accessibles aux programmes, vous devez :
1. Placer vos classes à l’intérieur d’un ou plusieurs répertoires spéciaux, disons /home/user/
classdir. Notez que ce répertoire est celui de base pour l’arborescence de package. Si vous
ajoutez la classe com.horstmann.corejava.Employee, le fichier de classe doit être localisé
dans le sous-répertoire /home/user/classdir/com/horstmann/corejava.
2. Définir le chemin de classe (classpath). Ce chemin est la collection de tous les répertoires de
base dont les sous-répertoires peuvent contenir des fichiers de classe.
La définition du chemin de classe dépend de votre environnement de compilation. Si vous utilisez le
JDK, deux choix s’offrent à vous : spécifier l’option -classpath pour le compilateur et l’interpréteur de bytecode, ou définir la variable d’environnement CLASSPATH.
Les détails dépendent de votre système d’exploitation. Sous UNIX, les éléments dans le chemin de
classe sont séparés par des caractères deux-points :
/home/user/classes:.:/home/user/archives/archive.jar
Sous Windows, ils sont séparés par des points-virgules :
c:\classes;.;c:\archives\archive.jar
Dans les deux cas, le point désigne le répertoire courant.
Ce chemin de classe contient :
m
le répertoire de base /home/user/classdir ou c:\classes ;
m
le répertoire courant (.) ;
m
le fichier JAR /home/user/archives/archive.jar ou c:\archives\archive.jar.
Livre Java .book Page 164 Jeudi, 25. novembre 2004 3:04 15
164
Au cœur de Java 2 - Notions fondamentales
Les classes sont toujours recherchées dans les fichiers de bibliothèque d’exécution (rt.jar et les
autres fichiers JAR dans les répertoires jre/lib et jre/lib/ext) ; vous ne les incluez pas explicitement dans le chemin de classe.
INFO
Un changement est intervenu par rapport aux versions 1.0 et 1.1 du kit Java Development Kit. Dans ces versions, les
classes système étaient stockées dans classes.zip qui devait faire partie du chemin d’accès aux classes.
Voici, par exemple, la façon de définir le chemin d’accès aux classes pour le compilateur :
javac -classpath /home/user/classdir:.:/home/user/archives/archive.jar
➥MyProg.java
(Toutes les instructions doivent être tapées sur une seule ligne. Sous Windows, utilisez le pointvirgule pour séparer les éléments du chemin de classes.)
ASTUCE
Vous pouvez également utiliser -cp au lieu de -classpath. Toutefois, avant le JDK 5.0, cette option ne fonctionnait qu’avec l’interpréteur de bytecode java, et vous deviez utiliser -classpath avec le compilateur
javac.
Le chemin de classe liste tous les répertoires et fichiers archive qui représentent des points de départ
pour la localisation des classes. Examinons l’exemple suivant :
/home/user/classdir:.:/home/user/archives/archive.jar
Supposons que l’interpréteur recherche le fichier de la classe com.horstmann.corejava.Employee.
Il va d’abord rechercher dans les fichiers de classe système qui sont stockés dans les archives des répertoires jre/lib et jre/lib/ext. Il ne trouvera pas le fichier de classes là, il va donc se tourner vers le
chemin de classe et rechercher les fichiers suivants :
m
/home/user/classdir/com/horstmann/corejava/Employee.class ;
m
com/horstmann/corejava/Employee.class en commençant par le répertoire courant ;
m
com/horstmann/corejava/Employee.class dans /home/user/archives/archive.jar.
INFO
La tâche du compilateur est plus difficile que celle de la machine virtuelle en ce qui concerne la localisation de
fichiers. Si vous faites référence à une classe sans spécifier son package, le compilateur doit d’abord trouver le
package qui contient la classe. Il consulte toutes les directives import en tant que sources possibles pour la classe.
Supposons par exemple que le fichier source contienne les directives
import java.util.*;
import com.horstmann.corejava.*;
Livre Java .book Page 165 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
165
et que le code source fasse référence à une classe Employee. Le compilateur essaiera alors de trouver
java.lang.Employee (car le package java.lang est toujours importé par défaut), java.util.Employee,
com.horstmann.corejava.Employee et Employee dans le package courant. Il recherche chacune de ces classes
dans tous les emplacements du chemin de classe. Une erreur de compilation se produit si plus d’une classe est trouvée
(les classes devant être uniques, l’ordre des instructions import n’a pas d’importance).
L’étape suivante pour le compilateur consiste à consulter les fichiers source pour voir si la source est plus récente que
le fichier de classe. Si oui, le fichier source est recompilé automatiquement. Souvenez-vous que vous ne pouvez
qu’importer des classes publiques des autres packages. Un fichier source peut seulement contenir une classe publique, et les noms du fichier et de la classe publique doivent correspondre. Le compilateur peut donc facilement localiser les fichiers source pour les classes publiques. Vous pouvez importer des classes non publiques à partir des
packages courants. Ces classes peuvent être définies dans des fichiers source avec des noms différents. Si vous importez une classe à partir du package courant, le compilateur examine tous les fichiers source de ce package pour vérifier
lequel définit la classe.
ATTENTION
Le compilateur javac recherche toujours les fichiers dans le répertoire courant, mais l’interpréteur java n’examine
le répertoire courant que si le répertoire "." fait partie du chemin d’accès de classe. En l’absence de définition de
chemin de classe, cela ne pose pas de problème, le chemin de classe par défaut est le répertoire ".". Mais, si vous
avez défini le chemin de classe et oublié d’inclure le répertoire ".", vos programmes seront compilés sans erreur,
mais ils ne pourront pas s’exécuter.
Définition du chemin de classe
Comme vous venez de le voir, vous pouvez définir le chemin de classe avec l’option -classpath
pour les programmes javac et java. Nous préférons cette option, mais certains programmeurs la
jugent ennuyeuse. Vous pouvez aussi définir la variable d’environnement CLASSPATH. Voici quelques
conseils pour définir la variable d’environnement CLASSPATH sous UNIX/Linux et Windows.
• Sous UNIX/Linux, modifiez le fichier de démarrage de votre shell.
Si vous utilisez le shell C, ajoutez la ligne suivante au fichier .cshrc de votre répertoire de base :
setenv CLASSPATH /home/user/classdir:.
• Si vous utilisez le shell Bourne Again ou bash, ajoutez la ligne suivante au fichier .bashrc ou
.bash_profile dans votre répertoire de base :
export CLASSPATH=/home/user/classdir:.
• Sous Windows 95/98/Me, modifiez le fichier autoexec.bat dans le lecteur racine (généralement
C:). Ajoutez la ligne :
SET CLASSPATH=c:\user\classdir;.
Vérifiez de ne pas placer d’espaces de l’un ou de l’autre côté du caractère =.
• Sous Windows NT/2000/XP, ouvrez le Panneau de configuration. Cliquez ensuite sur l’icône Système et choisissez l’onglet Environnement. Ajoutez une nouvelle variable d’environnement
nommée CLASSPATH ou modifiez la variable si elle existe déjà. Dans le champ de valeur, tapez le
Livre Java .book Page 166 Jeudi, 25. novembre 2004 3:04 15
166
Au cœur de Java 2 - Notions fondamentales
chemin de classe souhaité comme c:\user\classdir;. (voir Figure 4.9).
Figure 4.9
Définition du chemin de
classe sous Windows XP.
Visibilité dans un package
Nous avons déjà rencontré les modificateurs d’accès public et private. Les composants logiciels
déclarés public sont utilisables par n’importe quelle classe. Les éléments private ne sont accessibles que dans la classe qui les définit. Si vous ne spécifiez pas de modificateur public ou private,
un composant (classe, méthode ou variable) est accessible à toutes les méthodes du même package.
Revenons à l’Exemple 4.2. La classe Employee n’était pas définie en tant que classe publique.
Par conséquent, seules les autres classes de son package — en l’occurrence, le package par
défaut, comme EmployeeTest — peuvent y accéder. Pour les classes, il s’agit d’une situation
par défaut assez raisonnable. En revanche, ce fut un choix malheureux en ce qui concerne les
variables. Celles-ci doivent maintenant être explicitement spécifiées private si l’on ne souhaite
pas que leur visibilité s’étende par défaut à tout le package (ce qui serait en contradiction avec la
règle de l’encapsulation). Le problème naît du fait qu’il est très facile d’oublier de taper le mot
clé private. Voici un exemple de la classe Window du package java.awt, qui fait partie du code
source fourni avec le JDK :
public class Window extends Container
{
String warningString;
. . .
}
Notez que la variable warningString n’est pas privée ! Cela signifie que les méthodes de toutes les
classes du package java.awt ont accès à cette variable et peuvent lui affecter n’importe quelle
chaîne. En fait, les seules méthodes qui accèdent à cette variable se trouvent dans la classe Window,
et une déclaration private aurait donc été parfaitement appropriée. Nous pouvons supposer que le
programmeur était pressé en tapant le code et qu’il a tout bonnement oublié le modificateur
private.
Livre Java .book Page 167 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
167
INFO
Il est surprenant de constater que ce problème n’a jamais été corrigé, bien que nous l’ayons signalé dans sept
éditions de ce livre — il semble que les implémenteurs de bibliothèque ne lisent pas cet ouvrage. Par ailleurs, de
nouveaux champs ont été ajoutés à la classe au cours des années, et environ la moitié d’entre eux ne sont pas privés
non plus.
Est-ce réellement un problème ? Cela dépend. Par défaut, les packages ne sont pas des entités
fermées. C’est-à-dire que n’importe qui peut y ajouter des éléments. Des programmeurs malintentionnés peuvent donc ajouter du code qui modifie les variables dont la visibilité s’étend à la totalité
du package. Par exemple, dans des versions précédentes du langage de programmation Java, il était
très facile de pénétrer dans une autre classe du package java.awt, simplement en démarrant la
classe avec
package java.awt;
puis de placer le fichier de classe résultant dans un sous-répertoire java\awt quelque part dans le
chemin de classe, et vous pouviez avoir accès aux structures internes du package java.awt. Grâce à
ce subterfuge, il était possible de redéfinir le message d’avertissement (voir Figure 4.10).
Figure 4.10
Changement
du message d’avertissement
dans la fenêtre d’un applet.
A partir de la version 1.2, les concepteurs du JDK ont modifié le chargeur de classe pour explicitement désactiver le chargement de classes définies par l’utilisateur, et dont le nom de package
commencerait par "java." ! Bien entendu, vos propres classes ne bénéficient pas de cette protection. Vous pouvez utiliser à la place un autre mécanisme, le package sealing (plombage de package),
qui résout le problème de l’accès trop libre au package. Si vous plombez un package, aucune autre
classe ne peut lui être ajoutée. Vous verrez au Chapitre 10 comment produire un fichier JAR qui
contient des packages plombés.
Commentaires pour la documentation
Le JDK contient un outil très utile, appelé javadoc, qui génère une documentation au format
HTML à partir de vos fichiers source. En fait, la documentation API que nous avons décrite au
Chapitre 3 est simplement le résultat de l’exécution de javadoc sur le code source de la bibliothèque Java standard.
Livre Java .book Page 168 Jeudi, 25. novembre 2004 3:04 15
168
Au cœur de Java 2 - Notions fondamentales
Si vous ajoutez des commentaires commençant par le délimiteur spécial /** à votre code source,
vous pouvez générer facilement une documentation d’aspect très professionnel. Ce système est astucieux, car il vous permet de conserver votre code et votre documentation au même endroit. Si votre
documentation se trouve dans un fichier séparé, le code et les commentaires divergeront fatalement
à un moment donné. Mais puisque les commentaires de documentation sont dans le même fichier
que le code source, il est facile de les mettre à jour en même temps et d’exécuter javadoc.
Insertion des commentaires
L’utilitaire javadoc extrait les informations concernant les éléments suivants :
m
les packages ;
m
les classes publiques et les interfaces ;
m
les méthodes publiques et protégées ;
m
les champs publics et protégés.
Les fonctionnalités protégées seront examinées au Chapitre 5 ; les interfaces, au Chapitre 6.
Vous pouvez (et devez) fournir un commentaire pour chacune de ces fonctionnalités. Chaque
commentaire est placé immédiatement au-dessus de la fonctionnalité qu’il décrit. Un commentaire
commence par /** et se termine par */.
Chaque séquence /** . . . */ contient un texte au format libre suivi par des balises. Une balise
commence par un @, comme par exemple, @author ou @param.
La première phrase du commentaire au format libre doit être une instruction de sommaire. L’utilitaire javadoc génère automatiquement les pages de sommaire qui extraient ces phrases.
Dans le texte libre, vous pouvez utiliser des balises HTML telles que <em>...</em> pour insister,
<code>...</code> pour une police à espacement fixe, <strong>...</strong> pour des caractères
gras, et même <img ...> pour inclure une image. Evitez cependant les balises <h1> ou <hr> qui
peuvent interférer avec la mise en forme du document.
INFO
Si vos commentaires contiennent des liens vers d’autres fichiers comme des images (par exemple des diagrammes ou des images de composants de l’interface utilisateur), placez ces fichiers dans des sous-répertoires appelés doc-files. L’utilitaire javadoc copiera ces répertoires et les fichiers qu’ils contiennent vers le répertoire
documentation.
Commentaires de classe
Un commentaire de classe doit être placé après les instructions import, juste avant la définition
class.
Voici un exemple :
/**
Un objet <code>Card</code> représente une carte à jouer, telle
que "Dame de coeur". Une carte a une couleur (Pique, Coeur,
Trèfle ou Carreau) et une valeur (1 = As, 2 . . . 10, 11 = Valet,
12 = Dame, 13 = Roi).
Livre Java .book Page 169 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
169
*/
public class Card
{
. . .
}
INFO
De nombreux programmeurs commencent chaque ligne de documentation par un astérisque, comme ci-après :
/**
* Un objet <code>Card</code> représente une carte à jouer, telle
* que "Dame de cœur". Une carte a une couleur (Pique, Cœur,
* Trèfle ou Carreau) et une valeur (1 = As, 2 . . . 10, 11 = Valet,
* 12 = Dame, 13 = Roi)
*/
Cette méthode n’est pas recommandée, car elle décourage les programmeurs de mettre à jour les commentaires. Il
est fastidieux de réorganiser les * si la longueur de la ligne change. Certains éditeurs de texte prennent en charge
cette corvée. Si vous savez que les futurs mainteneurs de votre code vont utiliser un tel éditeur de texte, vous pouvez
ajouter ce style de détail qui délimite bien les commentaires.
Commentaires de méthode
Chaque commentaire de méthode doit immédiatement précéder la méthode qu’il décrit. En plus des
balises générales, vous pouvez utiliser les balises suivantes :
@param description de variable
Cette balise ajoute une entrée à la section des paramètres (parameters) de la méthode courante. La
description peut occuper plusieurs lignes et avoir recours aux balises HTML. Toutes les balises
@param pour une méthode doivent être regroupées :
@return description
Cette balise ajoute une section de renvoi (returns) à la méthode courante. La description peut occuper
plusieurs lignes et avoir recours aux balises HTML :
@throws description de classe
Cette balise ajoute une note concernant le déclenchement possible d’une exception par la méthode.
Les exceptions sont traitées au Chapitre 11.
Voici un exemple de commentaire de méthode :
/**
Augmente le salaire d’un employé.
@param byPercent le pourcentage d’augmentation du salaire
(par ex. 10 = 10%)
@return le montant de l’augmentation
*/
public double raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
return raise;
}
Livre Java .book Page 170 Jeudi, 25. novembre 2004 3:04 15
170
Au cœur de Java 2 - Notions fondamentales
Commentaires de champ
Seuls les champs publics doivent être documentés — c’est-à-dire généralement des constantes statiques.
Par exemple :
/**
La couleur "Coeur"
*/
public static final int HEARTS = 1;
Commentaires généraux
Les balises suivantes peuvent être employées dans les commentaires de documentation de classe :
@author nom
Cette balise crée une mention d’auteur (author). Il peut y avoir plusieurs balises @author, une pour
chaque auteur :
@version texte
Cette balise crée une entrée "version". Le texte décrit la version courante.
Les balises suivantes peuvent être employées dans tous les commentaires de documentation :
@since texte
Cette balise crée une entrée "depuis" (since). Le texte peut être toute description de la version ayant
introduit cette fonctionnalité. Par exemple, @since version 1.7.1 :
@deprecated texte
Cette balise ajoute un commentaire signalant que la classe, la méthode ou la variable est dépréciée et
ne doit plus être utilisée. Le texte doit suggérer une solution de remplacement. Par exemple :
@deprecated Utiliser <code>setVisible(true)</code> à la place
Vous pouvez ajouter des liens hypertexte vers d’autres parties connexes de la documentation javadoc, ou vers d’autres documents externes, à l’aide des balises @see et @link :
@see référence
Cette balise ajoute un lien hypertexte dans la section "see also" (voir aussi). Elle peut être employée
avec les classes et les méthodes. Ici, référence peut être l’un des choix suivants :
package.classe#fonctionnalité label
<a href="...">label</a>
"texte"
La première possibilité est la plus utile. Vous fournissez le nom d’une classe, d’une méthode ou
d’une variable, et javadoc insère un lien hypertexte vers la documentation. Par exemple,
@see com.horstmann.corejava.Employee#raiseSalary(double)
crée un lien vers la méthode raiseSalary(double) dans la classe com.horstmann.corejava.Employee. Vous pouvez omettre le nom du package ou à la fois les noms de package et de
classe. La fonctionnalité est alors recherchée dans le package ou la classe courants.
Notez que vous devez utiliser un #, et non un point, pour séparer la classe du nom de la méthode ou
de la variable. Le compilateur Java lui-même est assez futé pour déterminer la signification du caractère point, comme séparateur entre les packages, les sous-packages, les classes, les classes internes,
et les méthodes ou variables. L’utilitaire javadoc, lui, n’est pas aussi pointu, et vous devez l’aider.
Livre Java .book Page 171 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
171
Si la balise @see est suivie d’un caractère <, vous devez spécifier un lien hypertexte. Le lien peut
concerner n’importe quelle adresse URL. Par exemple :
@see <a href="www.horstmann.com/corejava.html">The Core Java home page</a>
Dans chaque cas, vous pouvez spécifier un label facultatif, qui apparaîtra en tant qu’ancre du lien.
Si vous omettez le label, le nom du code cible ou l’URL apparaîtront à l’utilisateur en tant qu’ancre.
Si la balise @see est suivie d’un caractère ", le texte s’affiche dans la section "see also". Par exemple :
@see "Core Java 2 volume 2"
Vous pouvez ajouter plusieurs balises @see pour une fonctionnalité, mais vous devez les regrouper
ensemble.
Vous pouvez aussi placer les liens hypertexte vers d’autres classes ou méthodes, n’importe où dans
vos commentaires. Vous insérez une balise spéciale sous la forme {@link package.classe#fonctionnalité label} n’importe où dans un commentaire. La description de la fonctionnalité suit les
mêmes règles que pour la balise @see.
Commentaires de package et d’ensemble
Vous placez les commentaires de classe, de méthode et de variable directement dans les fichiers
source Java, délimités par les commentaires de documentation /** . . . */. Toutefois, pour générer des commentaires de package, vous devez ajouter un fichier appelé package.html dans chaque
répertoire de package. Tout texte entre les balises <BODY>...</BODY> est extrait.
Vous pouvez aussi fournir un commentaire d’ensemble pour tous les fichiers source. Placez-le dans
un fichier appelé overview.html, situé dans le répertoire parent qui contient tous les fichiers
source. Tout le texte entre les balises <BODY>...</BODY> est extrait. Ce commentaire de vue
d’ensemble s’affiche lorsque l’utilisateur sélectionne "Overview" dans la barre de navigation.
Extraction des commentaires
Ici, docDirectory est le nom du répertoire où vous voulez stocker les fichiers HTML. Voici les
étapes à suivre :
1. Positionnez-vous dans le répertoire contenant les fichiers source à documenter. Si vous devez
documenter des packages imbriqués, tels que com.horstmann.corejava, vous devez vous trouver dans le répertoire qui contient le sous-répertoire com (le répertoire qui contient le fichier
overview.html, le cas échéant).
2. Exécutez la commande
javadoc -d docDirectory nomDuPackage
pour un package simple. Ou exécutez
javadoc -d docDirectory nomDuPackage1 nomDuPackage2...
pour documenter plusieurs packages. Si vos fichiers se trouvent dans le package par défaut,
exécutez
javadoc -d docDirectory *.java
à la place.
Livre Java .book Page 172 Jeudi, 25. novembre 2004 3:04 15
172
Au cœur de Java 2 - Notions fondamentales
Si vous avez omis l’option -d docDirectory, les fichiers HTML sont extraits du répertoire courant.
Cela peut devenir compliqué et n’est pas recommandé.
Le programme javadoc peut être personnalisé par de nombreuses options de ligne de
commande. Vous pouvez, par exemple, employer les options -author et -version pour inclure
les balises @author et @version dans la documentation (elles sont omises par défaut). Une autre
option utile est -link, pour inclure des liens hypertexte vers les classes standard. Par exemple, si
vous utilisez la commande
javadoc –link http://java.sun.com/j2se/5.0/docs/api *.java
toutes les classes de la bibliothèque standard sont automatiquement liées à la documentation du site
Web de Sun.
Pour découvrir d’autres options, vous pouvez consulter la documentation en ligne de l’utilitaire
javadoc à l’adresse http://java.sun.com/j2se/javadoc/.
INFO
Si vous avez besoin de personnalisation supplémentaire, par exemple pour produire une documentation sous un
format autre que HTML, vous pouvez fournir votre propre doclet pour générer la sortie sous la forme que vous
voulez. Il s’agit là d’une requête très particulière, et vous devez vous reporter à la documentation en ligne spécifique
à l’adresse suivante : http://java.sun.com/j2se/javadoc
ASTUCE
DocCheck est un doclet utile disponible à l’adresse http://java.sun.com/j2se/javadoc/doccheck/. Il analyse un jeu de
fichiers source à la recherche des commentaires de documentation manquants.
Conseils pour la conception de classes
Sans vouloir être exhaustif ou ennuyeux, nous allons terminer ce chapitre par quelques conseils qui
permettront à vos classes de faire bonne figure dans les cercles élégants de la POO.
1. Les données doivent être privées.
C’est une règle absolue : toute exception viole le principe d’encapsulation. Vous pouvez écrire
en cas de besoin une méthode d’accès ou une méthode d’altération, mais les champs proprement
dits doivent rester privés. L’expérience a montré que la représentation des données peut changer,
mais que la manière dont on les utilise change plus rarement. Lorsque les données sont privées,
une modification de leur représentation n’affecte pas l’utilisateur de la classe, et les bogues sont
plus facilement détectables.
2. Initialisez toujours les données.
Java n’initialise pas les variables locales à votre place, mais il initialise les champs
d’instance des objets. Ne vous fiez pas aveuglément aux valeurs par défaut ; initialisez
explicitement les variables en spécifiant leur valeur par défaut, soit dans la classe, soit dans
tous les constructeurs.
Livre Java .book Page 173 Jeudi, 25. novembre 2004 3:04 15
Chapitre 4
Objets et classes
173
3. N’abusez pas des types de base dans une classe.
Le principe consiste à remplacer plusieurs champs (apparentés) par une autre classe. Vos classes seront ainsi plus aisées à comprendre et à modifier. Par exemple, dans une classe Customer,
remplacez
private
private
private
private
String
String
String
String
street;
city;
state;
zip;
par une nouvelle classe nommée Address. De cette manière, vous pourrez facilement faire face
à une éventuelle modification de la présentation des adresses (pour le courrier international, par
exemple).
4. Tous les champs n’ont pas besoin de méthodes d’accès et de méthodes d’altération.
Vous pouvez avoir besoin de connaître et de modifier le salaire d’un employé. En revanche, une
fois l’objet construit, il n’est pas nécessaire de modifier sa date d’embauche. Bien souvent, les
objets contiennent des champs d’instance qui ne doivent pas être lus ni modifiés par d’autres —
par exemple, le tableau des codes postaux dans une classe Address.
5. Utilisez un format standard pour la définition de vos classes.
Nous présentons toujours le contenu des classes dans l’ordre suivant :
– éléments publics ;
– éléments accessibles au package ;
– éléments privés.
Chaque section est organisée ainsi :
– méthodes d’instance ;
– méthodes statiques ;
– champs d’instance ;
– champs statiques.
Après tout, les utilisateurs de votre classe sont davantage concernés par l’interface publique que
par les détails de l’implémentation privée. Et ils s’intéressent plus aux méthodes qu’aux
données.
En fait, il n’existe pas de convention universelle concernant le meilleur style. Le guide Sun du
langage de programmation Java recommande de lister d’abord les champs, puis les méthodes.
Quel que soit le style que vous adopterez, l’essentiel est de rester cohérent.
6. Subdivisez les classes ayant trop de responsabilités.
Ce conseil peut sembler vague, car le terme "trop" est relatif. En bref, chaque fois qu’il existe
une possibilité manifeste de diviser une classe complexe en deux classes conceptuellement plus
simples, profitez de l’occasion (mais n’exagérez pas, la complexité renaîtra si vous créez trop de
petites sous-classes).
Livre Java .book Page 174 Jeudi, 25. novembre 2004 3:04 15
174
Au cœur de Java 2 - Notions fondamentales
Voici un exemple de conception maladroite :
public class CardDeck // conception maladroite
{
public
public
public
public
public
CardDeck() { . . . }
void shuffle() { . . . }
int getTopValue() { . . . }
int getTopSuit() { . . . }
void draw() { . . . }
private int[] value;
private int[] suit;
}
Cette classe implémente en réalité deux concepts séparés : un jeu de cartes avec ses méthodes
shuffle et draw, et une carte avec les méthodes permettant d’inspecter la valeur et la couleur
d’une carte. Il est logique ici d’introduire une classe Card représentant une carte individuelle.
Vous avez maintenant deux classes, avec chacune ses propres responsabilités :
public class CardDeck
{
public CardDeck() { . . . }
public void shuffle() { . . . }
public Card getTop() { . . . }
public void draw() { . . . }
private Card[] cards;
}
public class Card
{
public Card(int aValue, int aSuit) { . . . }
public int getValue() { . . . }
public int getSuit() { . . . }
private int value;
private int suit;
}
7. Donnez des noms significatifs à vos classes et à vos méthodes.
Les variables doivent toujours avoir un nom représentatif de leur contenu. Il en va de même pour
les classes (il est vrai que la bibliothèque standard contient quelques exemples contestables,
comme la classe Date qui concerne l’heure).
Un bon principe consiste à nommer les classes avec un substantif (Order) ou un substantif associé à un adjectif (RushOrder). Pour les méthodes, respectez la convention standard en faisant
débuter leur nom par un préfixe en minuscules : get pour les méthodes d’accès (getSalary) et
set pour les méthodes d’altération (setSalary).
Livre Java .book Page 175 Jeudi, 25. novembre 2004 3:04 15
5
L’héritage
Au sommaire de ce chapitre
✔ Classes, superclasses et sous-classes
✔ Object : la superclasse cosmique
✔ Listes de tableaux génériques
✔ Enveloppes d’objets et autoboxing
✔ Réflexion
✔ Enumération de classes
✔ Conseils pour l’utilisation de l’héritage
Le chapitre précédent étudiait les classes et les objets. Celui-ci traite de l’héritage, essentiel dans la
programmation orientée objet. L’idée qui sous-tend ce concept est que vous pouvez créer de nouvelles classes basées sur des classes existantes. Lorsque vous héritez d’une classe, vous réutilisez (ou
héritez de) ses méthodes et champs, et ajoutez de nouveaux champs pour adapter votre classe à de
nouvelles situations. Cette technique est essentielle en programmation Java.
Si votre expérience concerne essentiellement les langages procéduraux, comme C, Visual Basic ou
COBOL, nous vous conseillons de lire attentivement ce chapitre. Les programmeurs C++ chevronnés, ainsi que ceux qui ont déjà expérimenté un langage orienté objet comme Smalltalk, seront ici en
territoire connu ; il existe néanmoins de nombreuses différences entre l’implémentation de l’héritage
en Java et son implémentation dans les autres langages orientés objet.
La dernière partie de ce chapitre couvre la réflexion, la capacité d’en savoir plus au sujet des classes
et de leurs propriétés dans un programme en cours d’exécution. La réflexion est une fonctionnalité
puissante, mais indéniablement complexe. Elle concerne plus les concepteurs d’outils que les
programmeurs d’application, et vous pouvez donc vous contenter de survoler cette section dans un
premier temps, pour y revenir plus tard.
Livre Java .book Page 176 Jeudi, 25. novembre 2004 3:04 15
176
Au cœur de Java 2 - Notions fondamentales
Classes, superclasses et sous-classes
Revenons à la classe Employee, déjà présentée au Chapitre 4. Supposons que vous travailliez pour
une entreprise au sein de laquelle les directeurs (managers) sont traités différemment des autres
employés. Les directeurs sont aussi des employés sous bien des aspects. Tout comme les employés,
ils reçoivent un salaire. Toutefois, tandis que les employés sont censés exécuter leurs tâches en
échange de leur salaire, les dirigeants reçoivent un bonus s’ils atteignent leurs objectifs. C’est le
genre de situation qui appelle fortement l’héritage. Pour quelle raison ? Parce qu’il faut définir une
nouvelle classe, Manager, et y ajouter des fonctionnalités. Vous pouvez cependant conserver une
partie de ce que vous avez déjà programmé dans la classe Employee, et tous les champs de la classe
d’origine seront préservés. D’une façon plus abstraite, disons qu’il existe une relation "est" entre
Manager et Employee. Tout directeur est un employé : cette relation d’état (ou d’appartenance) est le
flambeau de l’héritage.
Voici comment définir une classe Manager qui hérite de la classe Employee. Le mot clé extends est
employé en Java pour signifier l’héritage.
class Manager extends Employee
{
méthodes et champs ajoutés
}
INFO C++
L’héritage est comparable en Java et en C++. Java utilise le mot clé extends au lieu de :. Tout héritage en Java est
public ; il n’existe pas d’analogie avec les fonctionnalités C++ d’héritage privé et protégé.
Le mot clé extends signifie que vous créez une nouvelle classe qui dérive d’une classe
existante. La classe existante est appelée superclasse, classe de base ou encore classe parent
(voire classe ancêtre). La nouvelle classe est appelée sous-classe, classe dérivée ou classe
enfant. Les termes superclasse et sous-classe sont les plus courants en programmation Java,
bien que certains programmeurs préfèrent l’analogie parent/enfant, qui convient bien à la notion
d’héritage.
La classe Employee est une superclasse, mais ce n’est pas parce qu’elle serait supérieure à une sousclasse ou contiendrait plus de fonctionnalités. En fait, c’est le contraire : les sous-classes offrent plus
de fonctionnalités que leur superclasse. Par exemple, comme vous le verrez lors de l’examen du reste
de la classe Manager, cette dernière encapsule plus de données et possède plus de fonctionnalités que
sa superclasse Employee.
INFO
En anglais, les préfixes super et sub (sous) sont issus du langage des ensembles employé en informatique théorique
et en mathématiques. L’ensemble de tous les employés contient l’ensemble de tous les directeurs ; on dit qu’il s’agit
d’un superensemble de l’ensemble des directeurs. En d’autres termes, l’ensemble de tous les directeurs est un sousensemble de l’ensemble de tous les employés.
Livre Java .book Page 177 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
177
Notre classe Manager a un nouveau champ pour stocker le bonus, et une nouvelle méthode pour le
définir :
class Manager extends Employee
{
. . .
public void setBonus(double b)
{
bonus = b;
}
private double bonus;
}
Ces méthodes et champs n’ont rien de particulier. Si vous avez un objet Manager, vous pouvez
simplement appliquer la méthode setBonus :
Manager boss = . . .;
boss.setBonus(5000);
Vous ne pouvez pas appliquer la méthode setBonus à un objet Employee, elle ne fait pas partie des
méthodes définies dans la classe Employee.
Vous pouvez cependant utiliser des méthodes telles que getName et getHireDay avec les objets
Manager. Même si ces méthodes ne sont pas explicitement définies dans la classe Manager, celle-ci
en hérite automatiquement de la superclasse Employee.
De même, les champs name, salary et hireDay sont hérités de la superclasse. Chaque objet Manager
a quatre champs : name, salary, hireDay et bonus.
Lors de la définition d’une sous-classe par extension de sa superclasse, il vous suffit d’indiquer les
différences entre les sous-classes et la superclasse. Lors de la conception de classes, vous placez
les méthodes les plus générales dans la superclasse, et celles plus spécialisées dans la sous-classe.
La mise en commun de fonctionnalités dans une superclasse est très fréquente en programmation
orientée objet.
Cependant, certaines des méthodes de la superclasse ne conviennent pas pour la sous-classe Manager. En particulier, la méthode getSalary, qui doit renvoyer la somme du salaire de base et du
bonus. Vous devez fournir une nouvelle méthode pour remplacer celle de la superclasse :
class Manager extends Employee
{
. . .
public double getSalary()
{
. . .
}
. . .
}
Comment pouvez-vous implémenter cette méthode ? Au premier abord, cela paraît simple :
renvoyez simplement la somme des champs salary et bonus :
public double getSalary()
{
return salary + bonus; // ne marche pas
}
Livre Java .book Page 178 Jeudi, 25. novembre 2004 3:04 15
178
Au cœur de Java 2 - Notions fondamentales
Cela ne peut pas marcher. La méthode getSalary de la classe Manager n’a pas d’accès direct aux
champs privés de la superclasse. Cela signifie que la méthode getSalary de la classe Manager ne
peut pas accéder directement au champ salary, même si chaque objet Manager a un champ
appelé salary. Seules les méthodes de la classe Employee ont accès aux champs privés. Si les
méthodes de Manager veulent accéder à ces champs privés, elles doivent procéder comme les
autres méthodes : utiliser l’interface publique, c’est-à-dire la méthode publique getSalary de la
classe Employee.
Essayons à nouveau. Nous voulons donc appeler getSalary au lieu d’accéder simplement au champ
salary :
public double getSalary()
{
double baseSalary = getSalary(); // ne marche toujours pas
return baseSalary + bonus;
}
Le problème est que l’appel à getSalary réalise simplement un appel à lui-même, puisque la
classe Manager a une méthode getSalary (précisément la méthode que nous essayons d’implémenter). Il s’ensuit une série infinie d’appels à la même méthode, ce qui entraîne un plantage du
programme.
Nous devons préciser que nous voulons appeler la méthode getSalary de la superclasse Employee,
et non celle de la classe courante. Vous devez pour cela utiliser le mot clé super, l’appel
super.getSalary()
appelle la méthode getSalary de la classe Employee. Voici la version correcte de la méthode
getSalary pour la classe Manager :
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
INFO
Certains pensent que super est analogue à la référence à this. Cette analogie n’est toutefois pas exacte — super
n’est pas une référence à un objet. Par exemple, vous ne pouvez pas affecter la valeur super à une autre variable
objet. super est un mot clé spécial qui demande au compilateur d’invoquer la méthode de la superclasse.
Vous avez vu qu’une sous-classe pouvait ajouter des champs et qu’elle pouvait ajouter ou remplacer
des méthodes de la superclasse. Cependant, l’héritage ne peut pas ôter des champs ou des méthodes.
INFO C++
Java utilise le mot clé super pour appeler une méthode de superclasse. En C++, vous utilisez le nom de la superclasse
avec l’opérateur ::. Par exemple, la méthode getSalary de la classe Manager appellera Employee::getSalary
au lieu de super.getSalary.
Livre Java .book Page 179 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
179
Enfin, nous allons fournir un constructeur :
public Manager(String n, double s, int year, int month, int day)
{
super(n, s, year, month, day);
bonus = 0;
}
Ici, le mot clé super a une signification différente. L’instruction
super(n, s, year, month, day);
est un raccourci pour dire "appeler le constructeur de la superclasse Employee avec n, s, year,
month et day comme paramètres".
Puisque le constructeur de Manager ne peut pas accéder aux champs privés de la classe Employee,
ils doivent être initialisés par l’intermédiaire d’un constructeur. Le constructeur est invoqué à
l’aide de la syntaxe spéciale super. L’appel utilisant super doit être la première instruction dans
le constructeur pour la sous-classe.
Si le constructeur de la sous-classe n’appelle pas explicitement un constructeur de la superclasse, le
constructeur par défaut est appelé (sans paramètre). Au cas où la superclasse ne posséderait pas
de constructeur par défaut — et où aucun autre constructeur n’est appelé explicitement à partir du
constructeur de la sous-classe — le compilateur Java indique une erreur.
INFO
Souvenez-vous que le mot clé this a deux significations : définir une référence au paramètre implicite, et appeler
un autre constructeur de la même classe. Le mot clé super a également deux significations : invoquer une méthode
de superclasse, et invoquer un constructeur de superclasse. Lorsqu’ils sont utilisés pour invoquer des constructeurs,
les mots clés this et super sont très proches. Les appels de constructeur ne peuvent qu’être la première instruction
dans un autre constructeur. Les paramètres de construction sont passés, soit à un autre constructeur de la même
classe (this), soit à un constructeur de la superclasse (super).
INFO C++
Dans un constructeur C++, vous n’appelez pas super, mais vous utilisez la syntaxe de liste d’initialisation pour
construire la superclasse. Le constructeur Manager a l’aspect suivant en C++ :
Manager::Manager(String n, double s, int year, int month,
int day) // C++
{
bonus = 0;
}
La conséquence de la redéfinition de la méthode getSalary pour les objets Manager, est que pour
les directeurs, le bonus est automatiquement ajouté au salaire.
Pour comprendre ce fonctionnement, créons un nouveau directeur et définissons son bonus :
Manager boss = new Manager("Carl Cracker", 80000,
1987, 12, 15);
boss.setBonus(5000);
Livre Java .book Page 180 Jeudi, 25. novembre 2004 3:04 15
180
Au cœur de Java 2 - Notions fondamentales
Créons un tableau de trois employés :
Employee[] staff = new Employee[3];
Affectons à ce tableau le personnel de l’entreprise (employés et directeurs) :
staff[0]
staff[1]
1989,
staff[2]
1990,
= boss;
= new Employee("Harry Hacker", 50000,
10, 1);
= new Employee("Tony Tester", 40000,
3, 15);
Affichons le salaire de chacun :
for (Employee e : staff)
System.out.println(e.getName() + " "
+ e.getSalary());;
Cette boucle affiche les données suivantes :
Carl Cracker 85000.0
Harry Hacker 50000.0
Tommy Tester 40000.0
staff[1] et staff[2] affichent leur salaire de base, car ce sont des objets de la classe Employee.
En revanche, staff[0] est un objet Manager et sa méthode getSalary ajoute le bonus au salaire de
base.
Ce qui est important, c’est que l’appel
e.getSalary()
sélectionne la méthode getSalary correcte. Notez que le type déclaré de e est Employee, mais que
le type réel de l’objet auquel e fait référence peut être soit Employee (si i vaut 1 ou 2), soit Manager
(si i vaut 0).
Lorsque e fait référence à un objet Employee, l’appel e.getSalary() appelle la méthode
getSalary de la classe Employee. Si toutefois, e fait référence à un objet Manager, c’est la
méthode getSalary de la classe Manager qui est appelée à la place. La machine virtuelle
connaît le type réel de l’objet auquel e fait référence et invoque par conséquent la méthode qui
convient.
Cette possibilité pour une variable objet (comme la variable e) de pouvoir faire référence à plusieurs
types est appelée polymorphisme. La sélection automatique de la méthode appropriée lors de
l’exécution est appelée liaison dynamique. Nous reviendrons sur ces deux concepts plus en détail
dans ce chapitre.
INFO C++
En Java, vous n’avez pas besoin de déclarer une méthode comme virtuelle. La liaison dynamique est le comportement
par défaut. Si vous ne voulez pas qu’une méthode soit virtuelle, attribuez-lui le mot clé final (nous y reviendrons
dans ce chapitre).
L’Exemple 5.1 contient un programme qui montre la façon dont diffère le calcul du salaire pour les
objets Employee et Manager.
Livre Java .book Page 181 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
Exemple 5.1 : ManagerTest.java
import java.util.*;
public class ManagerTest
{
public static void main(String[] args)
{
// construire un objet Manager
Manager boss = new Manager("Carl Cracker", 80000,
1987, 12, 15);
boss.setBonus(5000);
Employee[] staff = new Employee[3];
// remplir le tableau staff avec des objets Manager et Employee
staff[0]
staff[1]
1989,
staff[2]
1990,
= boss;
= new Employee("Harry Hacker", 50000,
10, 1);
= new Employee("Tommy Tester", 40000,
3, 15);
// imprimer les infos concernant tous les objets Employee
for (Employee e : staff)
System.out.println("name=" + e.getName()
+ ",salary=" + e.getSalary());
}
}
class Employee
{
public Employee(String n, double s,
int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar
= new GregorianCalendar(year, month - 1, day);
hireDay = calendar.getTime();
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay;
}
181
Livre Java .book Page 182 Jeudi, 25. novembre 2004 3:04 15
182
Au cœur de Java 2 - Notions fondamentales
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
private String name;
private double salary;
private Date hireDay;
}
class Manager extends Employee
{
/**
@param n Nom de l’employé
@param s Le salaire
@param year L’année d’embauche
@param month Le mois d’embauche
@param day Le jour d’embauche
*/
public Manager(String n, double s,
int year, int month, int day)
{
super(n, s, year, month, day);
bonus = 0;
}
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double b)
{
bonus = b;
}
private double bonus;
}
Hiérarchie d’héritage
L’héritage n’est pas obligé de se limiter à une seule couche de classes dérivées. Nous pourrions, par
exemple, créer une classe Executive (Cadre) qui prolonge Manager. L’ensemble de toutes les classes dérivées d’une superclasse commune est appelé hiérarchie d’héritage ou hiérarchie des classes,
(voir Figure 5.1). Le chemin d’accès à une classe particulière vers ses ancêtres, dans la hiérarchie
d’héritage, se nomme la chaîne d’héritage.
Il existe généralement plusieurs chaînes descendant d’une même classe ancêtre. Vous pouvez former
une sous-classe Programmer ou Secretary qui prolonge la classe Employee : elles n’auront aucune
relation avec la classe Manager (ni entre elles). Le processus de création de classes dérivées n’est pas
limité.
Livre Java .book Page 183 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
183
INFO C++
Java ne prend pas en charge l’héritage multiple (pour savoir comment récupérer la majeure partie des fonctionnalités
de l’héritage multiple, voir la section sur les interfaces au prochain chapitre).
Figure 5.1
Hiérarchie d’héritage
de Employee.
Employee
Manager
Secretary
Programmer
Executive
Polymorphisme
Il existe une règle simple pour savoir si l’héritage est ou non le concept à envisager pour vos
données. La relation "est" dit que tout objet de la sous-classe est un objet de la superclasse. Par
exemple, chaque directeur est un employé. Il est par conséquent logique que la classe Manager soit
une sous-classe de la classe Employee. La réciproque n’est évidemment pas vraie — chaque
employé n’est pas un directeur.
Une autre façon de formuler cette relation est le principe de substitution. Il précise que vous pouvez
utiliser un objet d’une sous-classe chaque fois que le programme attend un objet d’une superclasse.
Vous pouvez ainsi affecter un objet d’une sous-classe à une variable de la superclasse :
Employee e;
e = new Employee(. . .);
// objet Employee attendu
e = new Manager(. . .); // OK, Manager peut aussi être utilisé
Dans le langage Java, les variables objet sont polymorphes. Une variable du type Employee peut
faire référence à un objet du type Employee ou à un objet de toute sous-classe de la classe Employee
(tel que Manager, Executive, Secretary, etc.).
Nous tirons parti de ce principe dans l’Exemple 5.1 :
Manager boss = new Manager(. . .);
Employee[] staff = new Employee[3];
staff[0] = boss;
Dans ce cas, les variables staff[0] et boss font référence au même objet. Cependant, staff[0] est
considéré par le compilateur comme seulement un objet Employee.
Cela signifie que vous pouvez appeler
boss.setBonus(5000); // OK
Livre Java .book Page 184 Jeudi, 25. novembre 2004 3:04 15
184
Au cœur de Java 2 - Notions fondamentales
mais pas
staff[0].setBonus(5000); // ERREUR
Le type déclaré de staff[0] est Employee, et la méthode setBonus n’est pas une méthode de la
classe Employee.
Vous ne pouvez toutefois pas affecter une référence de superclasse à une variable de sous-classe. Par
exemple, l’affectation suivante est incorrecte :
Manager m = staff[i]; // ERREUR
La raison de cette interdiction est simple : tous les employés ne sont pas directeurs. Si cette affectation réussissait et que m puisse faire référence à un objet Employee qui ne soit pas un directeur, il
serait possible d’appeler ultérieurement m.setBonus(...), et une erreur d’exécution s’ensuivrait.
ATTENTION
En Java, il est possible de transformer des tableaux de références de sous-classes en tableaux de références de superclasses, sans transtypage. Envisageons par exemple un tableau de directeurs
Manager[] managers = new Manager[10];
La conversion de ce tableau en tableau Employee[] est autorisée :
Employee[] staff = managers; // OK
Pourquoi pas, après tout ? Si manager[i] est un objet Manager, c’est également un objet Employee. En fait, un
événement surprenant survient. N’oubliez pas que les managers et staff font référence au même tableau. Etudiez
maintenant l’instruction
staff[0] = new Employee("Harry Hacker", ...);
Le compilateur autorisera cette instruction avec plaisir. Mais staff[0] et manager[0] constituent la même référence, on dirait donc que nous avons réussi à faire passer clandestinement un employé dans les rangs de la direction.
Ceci serait très déconseillé : l’appel à managers[0].setBonus(1000) tenterait d’accéder à une instance inexistante et corromprait la mémoire voisine.
Pour éviter toute corruption, tous les tableaux se souviennent du type d’élément créé, et ils surveillent que seules les
références compatibles y soient stockées. Par exemple, le tableau créé sous la forme new Manager[10] se souvient
qu’il s’agit uniquement d’un tableau de directeurs. Tenter de stocker une référence Employee entraîne une exception
de type ArrayStoreException.
Liaison dynamique
Il importe de bien comprendre ce qui se passe lorsqu’un appel de méthode est appliqué à un objet.
Voici les détails :
1. Le compilateur examine le type déclaré de l’objet et le nom de la méthode. Supposons que nous
appelions x.f(param), et que le paramètre implicite x soit déclaré comme étant un objet de la
classe C. Notez qu’il peut y avoir plusieurs méthodes, toutes avec le même nom f, mais avec des
types de paramètres différents. Il peut, par exemple, y avoir une méthode f(int) et une méthode
f(String). Le compilateur énumère toutes les méthodes appelées f dans la classe C et toutes
les méthodes public appelées f dans les superclasses de C.
Maintenant, le compilateur connaît tous les candidats possibles pour la méthode à appeler.
Livre Java .book Page 185 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
185
2. Le compilateur détermine ensuite les types des paramètres qui sont fournis dans l’appel de la
méthode. Si, parmi toutes les méthodes nommées f, il en existe une seule dont les types de paramètres correspondent exactement aux paramètres fournis, cette méthode est choisie pour l’appel.
Ce processus est appelé résolution de surcharge. Par exemple, dans un appel x.f("Hello"), le
compilateur choisira f(String) et non f(int). La situation peut devenir complexe du fait des
conversions de type (int vers double, Manager vers Employee, etc.). Si le compilateur ne peut
pas trouver de méthode avec les types de paramètres qui correspondent, ou s’il existe plusieurs
méthodes pouvant convenir après application des conversions, le compilateur renvoie une erreur.
Maintenant, le compilateur connaît le nom et le type des paramètres de la méthode devant être
appelée.
INFO
Souvenez-vous que le nom et la liste des types de paramètres pour une méthode sont appelés signature de la
méthode. Par exemple, f(int) et f(String) sont deux méthodes de même nom, mais présentant des signatures
différentes. Si vous définissez une méthode dans une sous-classe avec la même signature qu’une méthode d’une
superclasse, vous remplacez cette méthode. Le type renvoyé ne fait pas partie de la signature. Toutefois, lorsque vous
remplacez une méthode, vous devez conserver un type de retour compatible. Avant le JDK 5.0, les types de retours
devaient être identiques. Mais la sous-classe peut maintenant modifier le type de retour d’une méthode remplacée
en sous-type du type d’origine. Supposons par exemple que la classe Employee ait un
public Employee getBuddy() { ... }
la sous-classe Manager peut alors remplacer cette méthode par
public Manager getBuddy() { ... } // OK avec JDK 5.0
On dit que les deux méthodes getBuddy ont des types de retours covariants.
3. Si la méthode est private, static, final ou un constructeur, le compilateur sait exactement
quelle méthode appeler (le modificateur final est expliqué dans la section suivante). Cela
s’appelle une liaison statique. Sinon, la méthode à appeler dépend du type réel du paramètre
implicite, et la liaison dynamique doit être utilisée au moment de l’exécution. Dans notre exemple,
le compilateur générerait une instruction pour appeler f(String) avec liaison dynamique.
4. Lorsque le programme s’exécute et qu’il utilise la liaison dynamique pour appeler une méthode,
la machine virtuelle doit appeler la version de la méthode appropriée pour le type réel de l’objet
auquel x fait référence. Supposons que le type réel soit D, une sous-classe de C. Si la classe D définit une méthode f(String), celle-ci est appelée. Sinon, la superclasse de D est examinée pour
rechercher une méthode f(String), etc.
Exécuter cette recherche chaque fois qu’une méthode est appelée serait une perte de temps. La
machine virtuelle précalcule pour chaque classe une table de méthodes, qui liste toutes les signatures de méthodes et les méthodes réelles à appeler. Lorsqu’une méthode est réellement appelée,
la machine virtuelle fait une simple recherche dans la table. Dans notre exemple, la machine
virtuelle consulte la table de méthodes pour la classe D et recherche la méthode à appeler pour
f(String). Il peut s’agir de D.f(String) ou de X.f(String), où X est une superclasse
quelconque de D.
Il existe une variante à ce scénario. Si l’appel est super.f(param), le compilateur consulte la
table des méthodes de la superclasse du paramètre implicite.
Livre Java .book Page 186 Jeudi, 25. novembre 2004 3:04 15
186
Au cœur de Java 2 - Notions fondamentales
Nous allons examiner ce processus en détail dans l’appel e.getSalary() de l’Exemple 5.1. Le type
déclaré de e est Employee. La classe Employee possède une seule méthode appelée getSalary, et
elle n’a pas de paramètre. Dans ce cas, nous n’allons pas nous préoccuper de résolution de
surcharge.
Puisque la méthode getSalary n’est pas private, static ni final, elle est liée dynamiquement.
La machine virtuelle produit des tables de méthodes pour les classes Employee et Manager. La table
Employee montre que toutes les méthodes sont définies dans la classe Employee elle-même :
Employee:
getName() -> Employee.getName()
getSalary() -> Employee.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)
En réalité, ce n’est pas tout ; comme vous verrez plus loin dans ce chapitre, la classe Employee a une
superclasse Object de laquelle elle hérite un certain nombre de méthodes. Les méthodes de Object
sont ignorées pour l’instant.
La table de méthodes Manager est légèrement différente. Trois méthodes font partie de l’héritage,
une est redéfinie et une est ajoutée :
Manager:
getName() -> Employee.getName()
getSalary() -> Manager.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)
setBonus(double) -> Manager.setBonus(double)
A l’exécution, l’appel e.getSalary() est résolu de la façon suivante :
1. Tout d’abord, la machine virtuelle consulte la table de méthodes pour le type réel de e. Il peut
s’agir de la table des méthodes pour Employee, Manager ou une autre sous-classe de Employee.
2. Puis, la machine virtuelle recherche dans la classe déterminée la signature de getSalary().
Elle sait maintenant quelle méthode appeler.
3. Enfin, la machine virtuelle appelle la méthode.
La liaison dynamique possède une propriété très importante : elle rend les programmes extensibles sans qu’il soit nécessaire de modifier le code existant. Supposons qu’une nouvelle classe
Executive soit ajoutée, et qu’il soit possible que la variable e fasse référence à un objet de cette
classe. Le code contenant l’appel e.getSalary() n’a pas besoin d’être recompilé. La méthode
Executive.getSalary() est appelée automatiquement si e fait référence à un objet du type
Executive.
ATTENTION
Lorsque vous remplacez une méthode, la méthode de la sous-classe doit être au moins aussi visible que celle de la
superclasse. En particulier, si la méthode de la superclasse est public, la méthode de la sous-classe doit aussi être
déclarée comme public. Il est courant d’omettre accidentellement le spécificateur public pour la méthode de la
sous-classe. Le compilateur proteste alors et signale que vous essayez de fournir un privilège d’accès plus faible.
Livre Java .book Page 187 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
187
Empêcher l’héritage : les classes et les méthodes final
Dans certaines circonstances, vous souhaiterez interdire à d’autres programmeurs de former une
sous-classe à partir d’une des classes que vous avez créées. On emploie le modificateur final pour
spécifier qu’une classe ne peut pas être étendue (une telle classe est aussi appelée classe final).
Supposons que nous voulions empêcher la création de sous-classes à partir de la classe Executive.
Il suffit pour cela d’utiliser le modificateur final dans sa déclaration, de la façon suivante :
final class Executive extends Manager
{
. . .
}
Une méthode peut également être déclarée final. Dans ce cas, aucune sous-classe ne pourra
remplacer cette méthode (toutes les méthodes d’une classe final sont automatiquement des méthodes
final). Par exemple :
class Employee
{
. . .
public final String getName()
{
return name;
}
. . .
}
INFO
Souvenez-vous que les champs peuvent également être qualifiés de final. Un champ final ne peut pas être modifié
une fois que l’objet a été construit. Toutefois, si une classe est déclarée comme final, seules les méthodes, et non
les champs, sont automatiquement final.
Il n’existe qu’une bonne raison de rendre une méthode ou une classe final : vérifier que la sémantique ne peut pas être transformée en sous-classe. Par exemple, les méthodes getTime et setTime de
la classe Calendar sont final. Ceci montre que les concepteurs de la classe Calendar ont pris la
responsabilité de la conversion entre la classe Date et l’état du calendrier. Aucune sous-classe ne
doit être autorisée à bouleverser cet arrangement. De même, la classe String est une classe final.
Cela signifie que personne ne peut définir de sous-classe de String. En d’autres termes, si vous
voyez une référence String, vous savez qu’elle fait référence à une chaîne et à rien d’autre.
Certains programmeurs considèrent que vous devez déclarer toutes les méthodes sous la forme
final, à moins d’avoir une bonne raison pour vouloir utiliser le polymorphisme. En fait, en C++ et
C#, les méthodes n’utilisent le polymorphisme que si vous le demandez précisément. Ceci peut vous
sembler un peu extrême, mais il vaut certainement mieux penser soigneusement aux méthodes et aux
classes final lorsque vous concevez une hiérarchie de classe.
Aux premiers temps de Java, certains programmeurs utilisaient le mot clé final dans l’espoir
d’éviter les liaisons dynamiques. Lorsqu’une méthode n’est pas remplacée et qu’elle est courte, un
compilateur peut optimiser l’appel de méthode, une procédure appelée inlining. Par exemple, appliquer l’inlining à l’appel e.getName() le remplace par l’accès de champ e.name. Cette amélioration
est valable : les unités centrales détestent la dérivation, qui interfère avec leur stratégie de prélecture
Livre Java .book Page 188 Jeudi, 25. novembre 2004 3:04 15
188
Au cœur de Java 2 - Notions fondamentales
des instructions lors du traitement de l’instruction actuelle. Toutefois, si getName peut être remplacé
dans une autre classe, le compilateur ne peut pas procéder à l’inlining car il ne sait pas ce que peut
faire le code de remplacement.
Heureusement, le compilateur JIT de la machine virtuelle peut se révéler bien meilleur qu’un
compilateur traditionnel. Il connaît exactement les classes qui étendent une classe donnée et peut
vérifier si une classe remplace réellement une méthode donnée. Si une méthode est courte,
fréquemment appelée et qu’elle n’est pas réellement remplacée, le compilateur JIT peut procéder
à l’inlining de la méthode. Que se passe-t-il si la machine virtuelle charge une autre sous-classe
qui surcharge une méthode en ligne ? L’optimiseur doit annuler l’inlining. L’opération est lente
mais n’arrive que rarement.
INFO C++
En C++, une méthode n’est pas liée dynamiquement par défaut ; de plus, il est possible de spécifier la directive
inline pour que les appels à la méthode soient remplacés par le code source de la méthode. Cependant, aucun
mécanisme n’empêche une sous-classe de remplacer une méthode de superclasse. On peut écrire des classes C++ incapables d’avoir de descendance, mais cela exige une ruse complexe qui se justifie très rarement (cette astuce mystérieuse est laissée au lecteur en guise d’exercice. Indice : utilisez une classe de base virtuelle).
Transtypage
Nous avons appris, au Chapitre 3, que la conversion forcée d’un type en un autre s’appelle le transtypage, et que Java utilise une syntaxe spécifique pour désigner ce mécanisme. Par exemple,
double x = 3.405;
int nx = (int)x;
convertit la valeur de l’expression x en un entier (int), en supprimant la partie décimale.
Il est parfois nécessaire de convertir un nombre réel en nombre entier. Il peut aussi être nécessaire de
convertir un objet d’une classe en objet d’une autre classe. Pour effectuer un transtypage d’une référence d’objet, on emploie une syntaxe comparable à celle du transtypage d’une expression numérique. Le nom de classe cible est mis entre parenthèses et placé devant la référence d’objet que l’on
souhaite convertir. Voici un exemple :
Manager boss = (Manager)staff[0];
Il n’y a qu’une seule raison d’effectuer un transtypage d’objet — utiliser ce dernier à pleine capacité
lorsque son type réel a été temporairement occulté. Par exemple, dans la classe ManagerTest, le
tableau staff devait être un tableau d’objets Employee, car certains des éléments du tableau étaient
des employés réguliers (de type Employee). Les objets directeurs de ce tableau doivent être transtypés en Manager (leur type réel) si l’on souhaite accéder à leurs variables spécifiques. Remarquez que
dans l’exemple de la première section, nous nous sommes efforcés d’éviter le transtypage. Nous avons
initialisé la variable boss avec un objet Manager avant de la stocker dans le tableau. Il nous fallait
utiliser le type correct pour définir le bonus du directeur.
En Java, comme vous le savez, chaque variable objet appartient à un type donné, qui décrit le genre
d’objet auquel se réfère la variable et en détermine les capacités. Par exemple, staff[i] fait référence à un objet Employee (et peut donc référencer un objet Manager).
Livre Java .book Page 189 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
189
Le compilateur s’assure que l’on ne promet pas plus que l’on ne peut tenir lorsque vous stockez une
valeur dans une variable. Si vous affectez une référence d’une sous-classe à une variable de la superclasse, vous promettez moins, et le compilateur vous laisse faire. En revanche, si vous affectez une
référence de la superclasse à une variable d’une sous-classe, vous promettez plus. Vous devez donc
utiliser un transtypage pour que votre intention puisse être vérifiée au moment de l’exécution.
Que se passe-t-il si vous tentez de transtyper un objet dans un type dérivé et que vous mentiez par
conséquent sur le contenu de cet objet ?
Manager boss = (Manager)staff[1]; // ERREUR
Lorsque le programme s’exécute, Java remarque que le type est inapproprié et génère une exception
de type ClassCastException. Si vous n’interceptez pas cette exception, le programme se termine.
Il est par conséquent souhaitable de déterminer si un transtypage réussira avant de l’entreprendre.
A cette fin, employez l’opérateur instanceof, de la façon suivante :
if (staff[1] instanceof Manager)
{
boss = (Manager) staff[1];
. . .
}
Précisons que le compilateur ne vous laissera pas effectuer un transtypage si celui-ci est inévitablement
voué à l’échec. A titre d’exemple, le transtypage
Date c = (Date)staff[1];
provoque une erreur de compilation, car Date n’est pas une sous-classe de Employee.
En résumé :
m
Un transtypage d’objet ne peut s’appliquer qu’à des objets de la même hiérarchie de classes.
m
Utilisez instanceof avant de procéder à un transtypage d’une superclasse vers une sous-classe.
INFO
Le test : x instanceof C ne génère pas d’exception si x vaut null. Il renvoie simplement la valeur false. Cela est
logique, car null ne fait pas référence à un objet, en tout cas pas à un objet du type C.
En règle générale, il est préférable de ne pas convertir le type d’un objet par transtypage. Dans nos
exemples, il est rarement nécessaire de transtyper un objet Employee en objet Manager. La méthode
getSalary fonctionnera correctement avec les deux objets des deux classes. La répartition dynamique
permet de sélectionner automatiquement la méthode correcte (par polymorphisme).
L’unique raison d’avoir recours au transtypage est l’emploi d’une méthode spécifique aux directeurs,
comme setBonus. Si, pour quelque raison que ce soit, il apparaît important d’appeler setBonus
pour un objet de type Employee, demandez-vous si cela révèle une faille dans la conception de la
superclasse. Il peut être indiqué de revoir la conception de la superclasse et d’ajouter une méthode
setBonus. N’oubliez pas ceci : il suffit d’une exception ClassCastException non détournée pour
interrompre l’exécution du programme. En général, il est préférable de limiter autant que possible
l’utilisation de transtypages et de l’opérateur instanceof.
Livre Java .book Page 190 Jeudi, 25. novembre 2004 3:04 15
190
Au cœur de Java 2 - Notions fondamentales
INFO C++
Java emploie une syntaxe de transtypage issue du C, mais elle s’utilise comme l’opération dynamic_cast de C++.
Par exemple :
Manager boss = (Manager)staff[1]; // Java
correspond à :
Manager* boss = dynamic_cast<Manager*>(staff[1]); // C++
avec néanmoins une différence importante : si le transtypage échoue, il ne produit pas un objet null, mais déclenche une exception. Dans ce sens, il ressemble à un transtypage de références en C++. C’est dommage, car en C++ vous
pouvez effectuer la vérification de type et le transtypage en une seule opération.
Manager* boss = dynamic_cast<Manager*>(staff[1]); // C++
if (boss != NULL) . . .
En Java, il faut employer une combinaison de l’opérateur instanceof et du transtypage :
if (staff[1] instanceof Manager)
{
Manager boss = (Manager)staff[1];
. . .
}
Classes abstraites
A mesure que l’on remonte dans la hiérarchie des classes, celles-ci deviennent plus générales et
souvent plus abstraites. A un certain moment, la classe ancêtre devient tellement générale qu’on
la considère surtout comme un moule pour des classes dérivées et non plus comme une véritable
classe dotée d’instances. Prenons l’exemple d’une extension de la hiérarchie de notre classe
Employee. Un employé est une personne, tout comme l’est un étudiant. Etendons notre hiérarchie de classe pour inclure les classes Person et Student. La Figure 5.2 montre les relations
d’héritage entre ces classes.
Figure 5.2
Schéma de l’héritage
pour Person
et ses sous-classes.
Person
Employee
Student
Pourquoi construire une classe dotée d’un tel niveau d’abstraction ? Certains attributs, comme le
nom, concernent tout le monde. Les étudiants et les employés ont un nom, et le fait d’introduire une
superclasse commune permet de "factoriser" la méthode getName à un plus haut niveau dans la
hiérarchie d’héritage.
Livre Java .book Page 191 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
191
Ajoutons à présent une autre méthode, getDescription, dont le but est de renvoyer une brève
description de la personne, par exemple :
an employee with a salary of $50,000.00
a student majoring in computer science
Il est facile d’implémenter cette méthode pour les classes Employee et Student. Mais quelle information pouvez-vous fournir dans la classe Person ? Cette classe ne sait rien de la personne, excepté
son nom. Bien entendu, vous pouvez implémenter Person.getDescription() pour renvoyer une
chaîne vide. Mais il existe un meilleur moyen. Si vous employez le mot clé abstract, vous n’avez
pas besoin d’implémenter la méthode du tout :
public abstract String getDescription();
// aucune implémentation requise
Pour plus de clarté, une classe possédant une ou plusieurs méthodes abstraites doit elle-même être
déclarée abstraite :
abstract class Person
{ . . .
public abstract String getDescription();
}
En plus des méthodes abstraites, les classes abstraites peuvent posséder des données et des méthodes
concrètes. Par exemple, la classe Person peut stocker le nom de la personne et disposer d’une
méthode qui renvoie ce nom :
Person
{
public Person(String n)
{
name = n;
}
public abstract String getDescription();
public String getName()
{
return name;
}
private String name;
}
ASTUCE
De nombreux programmeurs pensent que les classes abstraites ne doivent avoir que des méthodes abstraites. Cette
vision est erronée. Il est toujours préférable de placer autant de fonctionnalités que possible dans une superclasse,
qu’elle soit ou non abstraite. En particulier, placez les méthodes et les champs communs (abstraits ou non) dans la
superclasse abstraite.
Les méthodes abstraites représentent en quelque sorte des emplacements pour les méthodes qui
seront implémentées dans les sous-classes. Lorsque vous étendez une classe abstraite, vous
avez deux possibilités. Vous pouvez laisser certaines ou toutes les méthodes abstraites indéfinies.
Livre Java .book Page 192 Jeudi, 25. novembre 2004 3:04 15
192
Au cœur de Java 2 - Notions fondamentales
La sous-classe doit ensuite être qualifiée d’abstraite également. Vous pouvez aussi définir toutes les
méthodes, la sous-classe n’est alors plus abstraite.
Nous allons par exemple, définir une classe Student qui étend la classe abstraite Person et implémente la méthode getDescription. Puisque aucune des méthodes de la classe Student n’est
abstraite, il n’est pas nécessaire de la déclarer comme classe abstraite.
Une classe peut être déclarée abstract même si elle ne possède pas de méthodes abstraites.
Les classes abstraites ne peuvent pas être instanciées. Autrement dit, si une classe est déclarée
abstract, il est impossible de créer un objet de cette classe. Par exemple, l’expression
new Person("Vince Vu")
est une erreur. Vous pouvez néanmoins créer des objets de sous-classes concrètes.
Notez cependant que vous pouvez toujours créer une variable objet d’une classe abstraite, mais cette
variable doit référencer un objet d’une sous-classe non abstraite. Par exemple :
Person p = new Student("Vince Vu", "Economics");
La variable p est ici une variable du type abstrait Person qui fait référence à une instance de la sousclasse non abstraite Student.
INFO C++
En C++, une méthode abstraite est appelée "fonction virtuelle pure" (pure virtual function) et sa déclaration est
terminée par = 0, de la façon suivante :
class Person // C++
{
public:
virtual string getDescription() = 0;
. . .
};
Une classe C++ est abstraite si elle possède au moins une fonction virtuelle pure. Il n’existe pas de mot clé particulier
en C++ pour désigner une classe abstraite.
Nous allons définir une sous-classe concrète Student qui étend la classe abstraite Person :
class Student extends Person
{
public Student(String n, String m)
{
super(n);
major = m;
}
public String getDescription()
{
return "a student majoring in " + major;
}
private String major;
}
Livre Java .book Page 193 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
193
La classe Student définit la méthode getDescription. Toutes les méthodes de la classe Student
sont donc concrètes, et la classe n’est plus désormais abstraite.
Le programme de l’Exemple 5.2 définit la superclasse abstraite Person et deux sous-classes concrètes Employee et Student. Un tableau de références Person est rempli avec les objets employé et
étudiant :
Person[] people = new Person[2];
people[0] = new Employee(. . .);
people[1] = new Student(. . .);
Nous affichons ensuite les noms et les descriptions de ces objets :
for (Person p = people)
System.out.println(p.getName() + ", " + p.getDescription());
Certains sont déconcertés par l’appel :
p.getDescription()
N’est-ce pas là un appel de méthode indéfinie ? N’oubliez pas que la variable p ne fait jamais référence à un objet Person puisqu’il est impossible de construire un objet de la classe abstraite Person.
La variable p fait toujours référence à un objet d’une sous-classe concrète comme Employee ou
Student. Pour ces objets, la méthode getDescription est définie.
Auriez-vous pu omettre totalement la méthode abstraite pour la superclasse Person et simplement
définir les méthodes getDescription dans les sous-classes Employee et Student ? Si vous l’aviez
fait, vous n’auriez alors pas pu invoquer la méthode getDescription sur la variable p. Le compilateur
s’assure que vous n’invoquez que les méthodes qui sont déclarées dans la classe.
Les méthodes abstraites sont un concept important dans le langage de programmation Java. Vous les
rencontrerez le plus souvent au sein des interfaces. Pour plus d’informations concernant les interfaces,
reportez-vous au Chapitre 6.
Exemple 5.2 : PersonTest.java
import java.text.*;
import java.util.*;
public class PersonTest
{
public static void main(String[] args)
{
Person[] people = new Person[2];
// remplir le tableau people avec des objets Student et Employee
people[0]
= new Employee("Harry Hacker", 50000, 1989, 10, 1);
people[1]
= new Student("Maria Morris", "computer science");
// afficher les noms et descriptions de tous les objets Person
for (Person p : people)
System.out.println(p.getName() + ", "
+ p.getDescription());
}
}
Livre Java .book Page 194 Jeudi, 25. novembre 2004 3:04 15
194
Au cœur de Java 2 - Notions fondamentales
abstract class Person
{
public Person(String n)
{
name = n;
}
public abstract String getDescription();
public String getName()
{
return name;
}
private String name;
}
class Employee extends Person
{
public Employee(String n, double s,
int year, int month, int day)
{
super(n);
salary = s;
GregorianCalendar calendar
= new GregorianCalendar(year, month - 1, day);
hireDay = calendar.getTime();
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay;
}
public String getDescription()
{
return String.format("an employee with a salary of $%.2f, salary);
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
private double salary;
private Date hireDay;
}
class Student extends Person
{
Livre Java .book Page 195 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
195
/**
@param n Nom de l’étudiant
@param m La spécialité de l’étudiant
*/
public Student(String n, String m)
{
// passer n au constructeur de la superclasse
super(n);
major = m;
}
public String getDescription()
{
return "a student majoring in " + major;
}
private String major;
}
Accès protégé
Comme vous le savez, les champs d’une classe sont généralement déclarés private et les méthodes
public. Tout élément private est invisible pour les autres classes. Nous avons expliqué au début de
ce chapitre que cette cécité sélective s’applique également aux sous-classes : une sous-classe n’a pas
accès aux champs privés de sa superclasse.
Il existe néanmoins des circonstances dans lesquelles vous voudrez limiter une méthode aux sousclasses seulement ou, plus généralement, permettre aux méthodes d’une sous-classe d’avoir accès à
un champ de superclasse. Dans ce cas, il faut déclarer cet élément protected (protégé). Par exemple, si la superclasse Employee déclare le champ hireDay comme protected plutôt que private,
les méthodes de la classe Manager pourront y accéder directement.
Cependant, les méthodes de la classe Manager ne pourront qu’accéder au champ hireDay des objets
Manager, et non des autres objets Employee. Cette restriction évite la violation du mécanisme de
protection et le risque que des sous-classes soient simplement créées pour obtenir l’accès aux
champs protégés.
Dans la pratique, le modificateur protected doit être utilisé avec une grande prudence. Supposons
que votre classe soit utilisée par d’autres programmeurs et que vous l’ayez dotée de champs protégés. A votre insu, d’autres programmeurs peuvent faire hériter de votre classe des sous-classes qui
accéderont à vos champs protégés. Ils risquent donc d’être contrariés si vous modifiez ensuite
l’implémentation de votre classe. Tout cela va à l’encontre de l’esprit de la programmation orientée
objet, qui recommande l’encapsulation des données.
En revanche, les méthodes protégées sont plus courantes. Une classe peut déclarer une méthode
protected si celle-ci est d’un usage délicat. Cela autorise les sous-classes (qui, a priori, connaissent
bien leur ancêtre) à utiliser cette méthode délicate, mais les autres classes sans lien de parenté n’en
ont pas le droit.
Un bon exemple de ce genre de méthode est la méthode clone de la classe Object — voir le Chapitre 6
pour plus de détails.
Livre Java .book Page 196 Jeudi, 25. novembre 2004 3:04 15
196
Au cœur de Java 2 - Notions fondamentales
INFO C++
En fait, les éléments protected de Java sont visibles par toutes les sous-classes, mais aussi par toutes les autres classes qui se trouvent dans le même package. La signification de protected est légèrement différente en C++, et la
notion de protection en Java est encore moins sûre qu’en C++.
Résumons les caractéristiques des quatre modificateurs de visibilité de Java :
1. Private (privé). Visible uniquement par la classe.
2. Public. Visible par toutes les classes.
3. Protected (protégé). Visible par le package et toutes les sous-classes.
4. Par défaut (hélas !) — aucun modificateur n’est spécifié. Visible par tout le package.
Object : la superclasse cosmique
La classe Object représente l’ancêtre ultime — toutes les classes Java héritent de Object. Néanmoins,
vous n’avez jamais à écrire :
class Employee extends Object
La superclasse Object est prise en compte par défaut si aucune superclasse n’est explicitement
spécifiée. Comme chaque classe Java dérive de Object, il importe de se familiariser avec les fonctionnalités de cette classe ancêtre. Nous en examinerons ici les caractéristiques de base ; les autres
aspects sont traités aux chapitres suivants et dans la documentation en ligne (plusieurs méthodes de
Object sont dédiées aux threads — voir le Chapitre 1 du Volume 2 pour plus d’informations sur les
threads).
Une variable de type Object peut référencer un objet de n’importe quel type :
Object obj = new Employee("Harry Hacker", 35000);
Bien entendu, une variable de type Object n’est utile qu’en tant que conteneur générique pour des
valeurs arbitraires. Pour pouvoir réellement en utiliser la valeur courante, il faut connaître le type
original (effectif) et appliquer un transtypage :
Employee e = (Employee)obj;
En Java, seuls les types primitifs (nombres, caractères et valeurs booléennes) ne sont pas des objets.
Tous les types de tableaux, qu’ils soient d’objets ou de types primitifs, sont des types de classes qui
étendent la classe Object :
Employee[] staff = new Employee[10];
obj = staff; // OK
obj = new int[10]; // OK
INFO C++
Il n’existe pas de classe racine cosmique en C++. Toutefois, en C++, tout pointeur peut être converti en pointeur
void*.
Livre Java .book Page 197 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
197
La méthode equals
La méthode equals de la classe Object détermine si deux objets sont égaux ou non. Telle qu’elle est
implémentée dans la classe Object, cette méthode vérifie que les deux références d’objet sont identiques. C’est un défaut assez raisonnable : si deux objets sont identiques, ils doivent certainement
être égaux. Cela suffit pour certaines classes. Il est peu logique, par exemple, de comparer deux
objets PrintStream à des fins d’égalité. Vous voudrez pourtant souvent implémenter le test d’égalité
dans lequel deux objets sont considérés égaux lorsqu’ils ont le même état.
Envisageons par exemple deux employés égaux s’ils ont le même nom, le même salaire et la même date
d’embauche (dans une vraie base de données d’employés, il serait plus logique de comparer les identifiants. Nous prenons cet exemple pour montrer le mécanisme de la mise en place de la méthode equals) :
class Employee
{ // . . .
public boolean equals(Object otherObject)
{
// tester rapidement si les objets sont identiques
if (this == otherObject) return true;
// doit renvoyer false si le paramètre explicite vaut null
if (otherObject == null) return false;
/* si les classes ne correspondent pas,
elles ne peuvent pas être égales */
if (getClass() != otherObject.getClass())
return false;
/* nous savons maintenant que otherObject
est un objet Employee non null */
Employee other = (Employee)otherObject;
// tester si les champs ont des valeurs identiques
return name.equals(other.name)
&& salary == other.salary
&& hireDay.equals(other.hireDay);
}
}
La méthode getClass renvoie la classe d’un objet — nous verrons cette méthode en détail plus loin
dans ce chapitre. Dans notre test, deux objets ne peuvent être égaux que s’ils sont de la même classe.
Lorsque vous définissez la méthode equals pour une sous-classe, appelez d’abord equals sur la
superclasse. Si ce test ne réussit pas, les objets ne peuvent pas être égaux. Si les champs de superclasse sont égaux, vous êtes prêt à comparer les champs d’instance de la sous-classe :
class Manager extends Employee{
. . .
public boolean equals(Object otherObject)
{
if (!super.equals(otherObject)) return false;
// super.equals vérifie que this et otherObject appartiennent à
// la même classe
Manager other = (Manager) otherObject;
return bonus == other.bonus;
}
}
Livre Java .book Page 198 Jeudi, 25. novembre 2004 3:04 15
198
Au cœur de Java 2 - Notions fondamentales
Test d’égalité et héritage
Comment devrait se comporter la méthode equals si les paramètres implicites et explicites n’appartiennent pas à la même classe ? Ce domaine a fait l’objet d’une certaine controverse. Dans l’exemple
précédent, la méthode equals renvoie false si les classes ne correspondent pas exactement. Mais
de nombreux programmeurs utilisent plutôt un test instanceof :
if (!(otherObject instanceof Employee)) return false;
Cela autorise l’éventualité que otherObject puisse appartenir à une sous-classe. Toutefois, cette
approche peut vous poser problème. Voici pourquoi. La spécification du langage Java requiert que la
méthode equals ait les propriétés suivantes :
1. Qu’elle soit réflective : pour toute référence x non nulle, x.equals(x) doit renvoyer true.
2. Qu’elle soit symétrique : pour toutes références x et y, x.equals(y) doit renvoyer true, si et
seulement si y.equals(x) renvoie true.
3. Qu’elle soit transitive : pour toutes références x, y et z, si x.equals(y) renvoie true et
y.equals(z) renvoie true, alors x.equals(z) doit renvoyer true.
4. Qu’elle soit cohérente : si les objets auxquels x et y font référence n’ont pas changé, des appels
successifs à x.equals(y) renvoient la même valeur.
5. Pour toute référence x non nulle, x.equals(null) doit renvoyer false.
Ces règles sont certainement raisonnables. Vous ne voudriez pas qu’un implémenteur de bibliothèque pondère s’il faut appeler x.equals(y) ou y.equals(x) lorsque vous localisez un élément dans
une structure de données.
Toutefois, la règle de symétrie implique des conséquences subtiles lorsque les paramètres appartiennent
à différentes classes. Envisageons un appel
e.equals(m)
où e est un objet Employee et m, un objet Manager, tous les deux se trouvant avoir le même nom,
salary et hireDate. Si Employee.equals utilise un test instanceof, cet appel renvoie true. Mais
cela signifie que l’appel inverse
m.equals(e)
doit aussi renvoyer true — la règle de symétrie ne permet pas de renvoyer false ni de lancer une
exception.
La classe Manager est ennuyée. Sa méthode equals doit être prête à se comparer à tout Employee,
sans prendre en compte les informations spécifiques aux directeurs ! Tout à coup, le test instanceof
apparaît moins attirant !
Certains auteurs ont prétendu que le test getClass était inadapté car il viole le principe de substitution. On cite souvent à cet égard la méthode equals de la classe AbstractSet, qui teste si deux
ensembles possèdent les mêmes éléments, dans le même ordre. La classe AbstractSet possède
deux sous-classes concrètes, TreeSet et HashSet, qui utilisent différents algorithmes pour localiser
des éléments de l’ensemble. Il est important de pouvoir comparer deux ensembles, quelle que soit
leur implémentation.
Livre Java .book Page 199 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
199
Toutefois, l’exemple des ensembles est plutôt spécialisé. Il serait logique de déclarer AbstractSet.equals sous la forme final, car personne ne doit redéfinir la sémantique de l’égalité du jeu (la
méthode n’est pas réellement final. Ceci permet à une sous-classe d’implémenter un algorithme
plus efficace pour le test d’égalité).
Comme nous le voyons, il existe deux scénarios distincts :
m
Si les sous-classes peuvent avoir leur propre notion de l’égalité, l’exigence de symétrie vous
oblige à utiliser le test getClass.
m
Si la notion d’égalité est fixée dans la superclasse, vous pouvez utiliser le test instanceof et
permettre aux objets de différentes sous-classes d’être égaux entre eux.
Dans l’exemple des employés et des directeurs, nous considérons que deux objets sont égaux
lorsqu’ils ont des champs concordants. Si nous avons deux objets Manager avec le même nom,
salaire et date d’embauche, mais avec des bonus différents, ils doivent donc être différents. Nous
avons par conséquent utilisé le test getClass.
Mais supposons que nous ayons utilisé un ID d’employé pour le test d’égalité. Cette notion de
l’égalité est logique pour toutes les sous-classes. Nous pourrions alors utiliser le test instanceof, et
nous déclarerions Employee.equals sous la forme final.
Notre recette pour l’écriture d’une méthode equals parfaite :
1. Nommez le paramètre explicite otherObject — vous devrez ultérieurement le transtyper en une
autre variable que vous appellerez other.
2. Vérifiez si this se trouve être identique à otherObject :
if (this == otherObject) return true;
Cette instruction est une simple optimisation. Dans la pratique, c’est très courant. Il est plus
économique de vérifier l’identité que de comparer les champs.
3. Testez si otherObject vaut null et renvoyez false dans ce cas. Ce test est obligatoire.
if (otherObject == null) return false;
Comparez les classes de this et otherObject. Si la sémantique de equals risque de changer
dans les sous-classes, utilisez le test getClass :
if (getClass() != otherObject.getClass()) return false;
Si la même sémantique vaut pour toutes les sous-classes, vous pouvez utiliser un test instanceof :
if (!(otherObject instanceof NomClasse)) return false;
4. Convertissez otherObject en une variable du type de votre classe :
nomDeClasse other = (nomDeClasse)otherObject
5. Comparez maintenant les champs, comme l’exige votre notion d’égalité. Utilisez == pour les
champs de type primitif, equals pour les champs d’objet. Renvoyez true si tous les champs
correspondent, false sinon :
return champ1 == other.champ1
&& champ2.equals(other.champ2)
&& . . .;
Livre Java .book Page 200 Jeudi, 25. novembre 2004 3:04 15
200
Au cœur de Java 2 - Notions fondamentales
Si vous redéfinissez equals dans une sous-classe, incluez un appel à super.equals(other).
INFO
La bibliothèque standard de Java contient plus de 150 implémentations des méthodes equals, avec une
disparité d’utilisations de instanceof, d’appels de getClass, de récupérations des exceptions
ClassCastException ou ne faisant rien du tout. Dès lors, il ne semble pas que tous les programmeurs
comprennent bien les subtilités de la méthode equals. Rectangle, par exemple, est une sous-classe de
Rectangle2D. Ces deux classes définissent une méthode equals avec un test instanceof. Si l’on compare
un Rectangle2D avec un Rectangle ayant les mêmes coordonnées, cela renvoie true ; échanger les paramètres
renvoie false.
ATTENTION
Il arrive souvent de se méprendre sur le moment où implémenter la méthode equals. Retrouverez-vous le
problème ?
public class Employee
{
public boolean equals(Employee other)
{
return name.equals(other.name)
&& salary == other.salary
&& hireDay.equals(other.hireDay);
}
...
}
Cette méthode déclare le type de paramètre explicite sous la forme Employee. En conséquence, il ne remplace pas
la méthode equals de la classe Object mais définit une méthode n’ayant aucun rapport.
Depuis le JDK 5.0, vous pouvez vous protéger de ce type d’erreur en balisant les méthodes qui remplacent les méthodes
de la superclasse avec @Override :
@Override public boolean equals(Object other)
Si vous avez fait une erreur et que vous définissiez une nouvelle méthode, le compilateur signale l’erreur. Supposons
par exemple que vous ajoutiez la déclaration suivante à la classe Employee.
@Override public boolean equals(Employee other)
L’erreur signalée car cette méthode ne remplace aucune méthode de la superclasse Object.
La balise @Override est une balise de métadonnées. Le mécanisme des métadonnées est très général et extensible,
il permet aux compilateurs et aux outils de réaliser des actions arbitraires. L’avenir nous dira si les concepteurs
d’outils en profiteront. Dans le JDK 5.0, les implémenteurs du compilateur ont décidé de se frayer un chemin à l’aide
de la balise @Override.
La méthode hashCode
Un code de hachage est un entier dérivé d’un objet. Les codes de hachage doivent être brouillés : si
x et y sont deux objets distincts, la probabilité devrait être forte que x.hashCode() et y.hashCode()
Livre Java .book Page 201 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
201
soient différents. Le Tableau 5.1 reprend quelques exemples de codes de hachage tirés de la méthode
hashCode de la classe String.
Tableau 5.1 : Codes de hachage tirés de la fonction hashCode
Chaîne
Code de hachage
Hello
140207504
Harry
140013338
Hacker
884756206
La classe String utilise l’algorithme suivant pour calculer le code de hachage :
int hash = 0;
for (int i = 0; i < length(); i++)
hash = 31 * hash + charAt(i);
La méthode hashCode est définie dans la classe Object. Chaque objet possède donc un code de
hachage par défaut, extrait de l’adresse mémoire de l’objet. Etudiez l’exemple suivant :
String s = "Ok";
StringBuffer sb = new StringBuffer(s);
System.out.println(s.hashCode() + " " + sb.hashCode());
String t = new String("Ok");
StringBuffer tb = new StringBuffer(t);
System.out.println(t.hashCode() + " " + tb.hashCode());
Le Tableau 5.2 en présente le résultat.
Tableau 5.2 : Codes de hachage des chaînes et tampons des chaînes
Objet
Code de hachage
s
3030
sb
20526976
t
3030
tb
20527144
Sachez que les chaînes s et t présentent le même code de hachage car, pour les chaînes, ces codes
sont dérivés de leur contenu. Les tampons de chaînes sb et tb disposent de codes de hachage différents car aucune méthode hashCode n’a été définie pour la classe StringBuffer et la méthode
hashCode par défaut de la classe Object dérive le code de hachage de l’adresse mémoire de l’objet.
Si vous redéfinissez la méthode equals, vous devrez aussi redéfinir la méthode hashCode pour les
objets que les utilisateurs pourraient insérer dans une table de hachage (nous avons traité des tables
de hachage au Chapitre 2 du Volume 2).
Livre Java .book Page 202 Jeudi, 25. novembre 2004 3:04 15
202
Au cœur de Java 2 - Notions fondamentales
La méthode hashCode devrait renvoyer un entier (qui peut être négatif). Associez simplement les
codes de hachage des champs d’instance, de sorte que les codes des différents objets soient plus
largement répartis.
Il existe, par exemple, une méthode hashCode pour la classe Employee :
class Employee
{
public int hashCode()
{
return 7 * name.hashCode()
+ 11 * new Double(salary).hashCode()
+ 13 * hireDay.hashCode();
}
. . .
}
Vos définitions de equals et hashCode doivent être compatibles : si x.equals(y) est vrai, alors
x.hashCode() doit avoir la même valeur que y.hashCode(). Si, par exemple, vous définissez
Employee.equals, de manière à comparer les ID des employés, la méthode hashCode devra hacher
les ID, et non les noms des employés ou les adresses mémoire.
java.lang.Object 1.0
•
int hashCode()
Renvoie un code de hachage pour cet objet. Il peut s’agir d’un entier, positif ou négatif. Les
objets égaux doivent renvoyer des codes de hachage identiques.
La méthode toString
Une autre méthode importante de la classe Object est toString, qui renvoie une chaîne représentant la valeur de l’objet. Voici un exemple typique. La méthode toString de la classe Point renvoie
une chaîne comme celle-ci :
java.awt.Point[x=10,y=20]
La plupart (mais pas toutes) des méthodes toString ont ce format : le nom de la classe, suivi des
valeurs de champs incluses entre crochets. Voici une implémentation de la méthode toString pour
la classe Employee :
public String toString()
{
return "Employee[name=" + name
+ ",salary=" + salary
+ ",hireDay=" + hireDay
+ "]";
}
En réalité, vous pouvez faire mieux. Au lieu de coder en dur le nom de la classe dans la méthode
toString, appelez getClass().getName() pour obtenir une chaîne avec le nom de la classe :
public String toString()
{
return getClass().getName()
+ "[name=" + name
+ ",salary=" + salary
Livre Java .book Page 203 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
203
+ ",hireDay=" + hireDay
+ "]";
}
La méthode toString s’applique alors également aux sous-classes.
Bien entendu, le programmeur de la sous-classe doit définir sa propre méthode toString et ajouter
les champs de la sous-classe. Si la superclasse utilise getClass().getName(), la sous-classe peut
simplement appeler super.toString(). Voici, par exemple, une méthode toString pour la classe
Manager :
class Manager extends Employee
{
. . .
public String toString()
{
return super.toString()
+ "[bonus=" + bonus
+ "]";
}
}
Maintenant, un objet Manager s’affiche de la façon suivante :
Manager[name=...,salary=...,hireDay=...][bonus=...]
La méthode toString est omniprésente pour une raison essentielle : chaque fois qu’un objet est
concaténé avec une chaîne à l’aide de l’opérateur "+", le compilateur Java invoque automatiquement
la méthode toString pour obtenir une représentation de l’objet sous forme de chaîne. Voici un
exemple :
Point p = new Point(10, 20);
String message = "La position actuelle est " + p;
// invoque automatiquement p.toString()
ASTUCE
Au lieu d’écrire x.toString(), vous pouvez écrire "" + x. Cette instruction concatène la chaîne vide avec la représentation textuelle de x, qui correspond exactement à la représentation obtenue par x.toString(). A la différence de toString, cette instruction fonctionne, même si x est de type primitif.
Si x est un objet quelconque et que vous appeliez
System.out.println(x);
la méthode println appelle simplement x.toString() et affiche la chaîne résultante.
La classe Object définit la méthode toString pour afficher le nom de classe et le code de hachage
de l’objet. Par exemple, l’appel
System.out.println(System.out)
produit une sortie ressemblant à ceci :
java.io.PrintStream@2f6684
La raison en est que l’implémenteur de la classe PrintStream n’a pas pris la peine de remplacer la
méthode toString.
Livre Java .book Page 204 Jeudi, 25. novembre 2004 3:04 15
204
Au cœur de Java 2 - Notions fondamentales
La méthode toString est un outil précieux de consignation. De nombreuses classes de la bibliothèque de classes standard définissent la méthode toString comme fournisseur d’informations utiles
sur l’état d’un objet. Ceci est particulièrement utile pour consigner des messages, par exemple :
System.out.println("Current position = " + position);
Comme nous l’expliquons au Chapitre 11, une solution encore meilleure consisterait à indiquer :
Logger.global.info("Current position = " + position);
ASTUCE
Il est fortement recommandé d’ajouter une méthode toString à chacune des classes que vous écrivez. Vous et tous
les programmeurs utilisant vos classes apprécierez le support de consignation.
Le programme de l’Exemple 5.3 implémente les méthodes equals, hashCode et toString pour les
classes Employee et Manager.
Exemple 5.3 : EqualsTest.java
import java.util.*;
public class EqualsTest
{
public static void main(String[] args)
{
Employee alice1 = new Employee("Alice Adams", 75000,
1987, 12, 15);
Employee alice2 = alice1;
Employee alice3 = new Employee("Alice Adams", 75000,
1987, 12, 15);
Employee bob = new Employee("Bob Brandson", 50000,
1989, 10, 1);
System.out.println("alice1 == alice2: "
+ (alice1 == alice2));
System.out.println("alice1 == alice3: "
+ (alice1 == alice3));
System.out.println("alice1.equals(alice3): "
+ alice1.equals(alice3));
System.out.println("alice1.equals(bob): "
+ alice1.equals(bob));
System.out.println("bob.toString(): " + bob);
Manager carl = new Manager("Carl Cracker", 80000,
1987, 12, 15);
Manager boss = new Manager("Carl Cracker", 80000,
1987, 12, 15);
boss.setBonus(5000);
System.out.println("boss.toString(): " + boss);
System.out.println("carl.equals(boss): "
+ carl.equals(boss));
Livre Java .book Page 205 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
System.out.println("alice1.hashCode(): " + alice1.hashCode());
System.out.println("alice3.hashCode(): " + alice3.hashCode());
System.out.println("bob.hashCode(): " + bob.hashCode());
System.out.println("carl.hashCode(): " + carl.hashCode());
}
}
class Employee
{
public Employee(String n, double s,
int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar
= new GregorianCalendar(year, month - 1, day);
hireDay = calendar.getTime();
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
public boolean equals(Object otherObject)
{
// test pour vérifier si les objets sont identiques
if (this == otherObject) return true;
// doit renvoyer false si le paramètre explicite vaut null
if (otherObject == null) return false;
/* si les classes ne correspondent pas,
elles ne peuvent pas être égales */
if (getClass() != otherObject.getClass())
return false;
/* nous savons maintenant que otherObject est
un objet Employee non null */
Employee other = (Employee)otherObject;
205
Livre Java .book Page 206 Jeudi, 25. novembre 2004 3:04 15
206
Au cœur de Java 2 - Notions fondamentales
// tester si les valeurs de champs sont identiques
return name.equals(other.name)
&& salary == other.salary
&& hireDay.equals(other.hireDay);
}
public int hashCode()
{
return 7 * name.hashCode()
+ 11 * new Double(salary).hashCode()
+ 13 * hireDay.hashCode();
}
public String toString()
{
return getClass().getName()
+ "[name=" + name
+ ",salary=" + salary
+ ",hireDay=" + hireDay
+ "]";
}
private String name;
private double salary;
private Date hireDay;
}
class Manager extends Employee
{
public Manager(String n, double s,
int year, int month, int day)
{
super(n, s, year, month, day);
bonus = 0;
}
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double b)
{
bonus = b;
}
public boolean equals(Object otherObject)
{
if (!super.equals(otherObject)) return false;
Manager other = (Manager)otherObject;
/* super.equals a vérifié que this et other
appartenaient à la même classe */
return bonus == other.bonus;
}
Livre Java .book Page 207 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
207
public int hashCode()
{
return super.hashCode()
+ 17 * new Double(bonus).hashCode();
}
public String toString()
{
return super.toString()
+ "[bonus=" + bonus
+ "]";
}
private double bonus;
}
java.lang.Object 1.0
•
Class getClass()
Renvoie un objet class contenant des informations sur l’objet. Comme vous le verrez dans ce
chapitre, la représentation des classes à l’exécution est encapsulée dans la classe Class.
•
boolean equals(Object otherObject)
Compare deux objets ; renvoie true si les objets pointent vers la même zone mémoire, et false
dans le cas contraire. Vous devez remplacer cette méthode dans vos propres classes.
•
String toString()
Renvoie une chaîne décrivant la valeur de l’objet. Vous devez remplacer cette méthode dans vos
propres classes.
•
Object clone()
Crée un clone de l’objet. Le système d’exécution de Java alloue de la mémoire pour la nouvelle
instance et y recopie la mémoire allouée à l’objet courant.
INFO
Le clonage constitue une opération importante, mais il s’agit aussi d’un processus assez délicat qui peut réserver de
mauvaises surprises aux imprudents. Nous y reviendrons lors de l’examen de la méthode clone, au Chapitre 6.
java.lang.Class 1.0
•
String getName()
Renvoie le nom de cette classe.
•
Class getSuperclass()
Renvoie la superclasse de cette classe en tant qu’objet Class.
Listes de tableaux génériques
Dans de nombreux langages de programmation — en particulier C —, la taille des tableaux doit être
fixée au moment de la compilation. Les programmeurs détestent cette contrainte : elle les oblige à de
pénibles acrobaties de conception. Combien d’employés y aura-t-il dans un service ? Probablement
Livre Java .book Page 208 Jeudi, 25. novembre 2004 3:04 15
208
Au cœur de Java 2 - Notions fondamentales
pas plus de 100. Et si un service important a 150 employés ? Allons-nous gâcher 90 entrées pour
chaque service n’ayant que 10 employés ?
La situation est bien plus simple en Java, car il est possible de spécifier la taille d’un tableau au
moment de l’exécution :
int actualSize = . . .;
Employee[] staff = new Employee[actualSize];
Bien entendu, ce genre de code ne résout pas complètement le problème de la modification dynamique des tableaux. Une fois que la taille d’un tableau est spécifiée, il n’est pas facile de la changer. La
manière la plus simple de gérer cette situation courante consiste à utiliser une autre classe Java, classée ArrayList. La classe ArrayList est semblable à un tableau, mais elle ajuste automatiquement
sa capacité à mesure que vous ajoutez et supprimez des éléments, sans que vous n’ayez rien à faire.
Depuis le JDK 5.0, ArrayList est une classe générique avec un paramètre de type. Pour spécifier le
type des objets d’éléments inclus dans la liste de tableau, vous annexez un nom de classe entre les
signes supérieur à et inférieur à, sous la forme ArrayList<Employee>. Vous verrez au Chapitre 13
comment définir votre propre classe générique, mais il est inutile de connaître les caractéristiques
techniques du type ArrayList pour l’utiliser.
Nous déclarons et construisons ici une liste de tableau qui contient des objets Employee :
ArrayList<Employee> staff = new ArrayList<Employee>();
INFO
Les classes génériques n’existaient pas avant le JDK 5.0. Il n’y avait qu’une seule classe ArrayList, une collection
"fourre-tout" dont les éléments étaient de type Object. Si vous devez utiliser une ancienne version de Java, abandonnez simplement tous les suffixes de type <...>. Vous pouvez continuer à utiliser ArrayList sans suffixe <...>
dans JDK 5.0 et versions ultérieures. On le considère comme un type "brut", dont le paramètre de type est effacé.
INFO
Dans les versions encore antérieures du langage Java, les programmeurs utilisaient la classe Vector pour les
tableaux dynamiques. La classe ArrayList est toutefois plus efficace, et il n’existe plus de raison valable d’utiliser
la classe Vector.
Employez la méthode add pour ajouter de nouveaux éléments à une liste de tableaux. Voici par
exemple comment remplir une liste de tableaux avec des objets Employee :
staff.add(new Employee("Harry Hacker", . . .));
staff.add(new Employee("Tony Tester", ...));
La classe ArrayList gère un tableau interne de références Object. Ce tableau finira par être saturé.
C’est là que les listes de tableaux sont magiques : si vous appelez add et que le tableau interne soit
plein, la liste de tableaux crée automatiquement un tableau plus grand et recopie tous les objets du
plus petit tableau vers le plus grand.
Si vous connaissez déjà le nombre d’éléments que contiendra le tableau, ou que vous puissiez
l’évaluer assez précisément, appelez la méthode ensureCapacity avant de remplir la liste de
tableaux :
staff.ensureCapacity(100);
Livre Java .book Page 209 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
209
Cet appel alloue un tableau interne de 100 objets. Les 100 premiers appels à add n’impliquent aucun
repositionnement coûteux.
Vous pouvez aussi passer une capacité initiale au constructeur de ArrayList :
ArrayList<Employee> staff = new ArrayList<Employee>(100);
ATTENTION
L’allocation d’une liste de tableaux de la façon suivante :
new ArrayList<Employee>(100) // la capacité est de 100
n’est pas la même chose que l’allocation d’un nouveau tableau :
new Employee[100] // la taille est de 100
Il faut établir une distinction entre la capacité d’une liste de tableaux et la taille d’un tableau. Si vous
allouez un tableau de 100 éléments, le tableau disposera de 100 emplacements, prêts à l’emploi. Une
liste de tableaux dont la capacité est de 100 éléments peut potentiellement contenir 100 éléments (et, en
réalité, plus de 100, au prix d’allocations supplémentaires). Mais au départ, juste après sa construction,
une liste de tableaux ne contient en fait aucun élément.
La méthode size renvoie le nombre réel d’éléments dans la liste de tableaux. Par exemple,
staff.size()
renvoie le nombre actuel d’éléments dans la liste de tableaux staff. Ce qui est l’équivalent de
a.length
pour un tableau a.
Une fois que vous êtes quasiment certain que la liste de tableaux a atteint sa taille définitive, vous
pouvez appeler la méthode trimToSize. Elle ajuste la taille du bloc de mémoire pour que la quantité
exacte nécessaire pour le nombre actuel d’éléments soit réservée. Le "ramasse-miettes" récupérera
toute mémoire en excès.
Une fois que vous avez ajusté la taille d’une liste de tableaux, l’ajout de nouveaux éléments va
déplacer de nouveau le bloc, ce qui prend du temps. N’utilisez trimToSize que si vous êtes certain
de ne plus ajouter d’éléments à la liste de tableaux.
INFO C++
La classe ArrayList est identique au modèle de vecteur C++. ArrayList et vector sont tous deux des types génériques. Mais le modèle vector de C++ surcharge l’opérateur [] pour faciliter l’accès aux éléments. Comme Java
n’autorise pas la surcharge des opérateurs, une opération équivalente exige un appel de méthode explicite. De plus,
les vecteurs de C++ sont copiés par valeur. Si a et b sont deux vecteurs, l’affectation a = b; fait de a un nouveau
vecteur, de la même longueur que b, et tous les éléments de b sont copiés vers a. En Java, la même affectation
amènera a et b à référencer la même liste de tableaux.
java.util.ArrayList<T> 1.2
•
ArrayList<T>()
Construit une liste de tableaux vide.
Livre Java .book Page 210 Jeudi, 25. novembre 2004 3:04 15
210
•
Au cœur de Java 2 - Notions fondamentales
ArrayList<T>(int initialCapacity)
Construit une liste de tableaux vide ayant la capacité spécifiée.
Paramètres :
•
initialCapacity La capacité de stockage initiale de la liste de tableaux.
boolean add(T obj)
Ajoute un élément à la fin de la liste de tableaux. Renvoie toujours true.
Paramètres :
•
obj
L’élément à ajouter.
int size()
Renvoie le nombre d’éléments actuellement contenus dans la liste de tableaux (cette valeur est
différente de la capacité. Bien entendu, elle est toujours inférieure ou égale à la capacité de la
liste de tableaux).
•
void ensureCapacity(int capacity)
Vérifie que la liste de tableaux a la capacité nécessaire pour contenir le nombre d’éléments
donné, sans nécessiter la réallocation de son tableau de stockage interne.
Paramètres :
•
capacity
La capacité de stockage souhaitée.
void trimToSize()
Réduit la capacité de stockage de la liste de tableaux à sa taille actuelle.
Accéder aux éléments d’une liste de tableaux
Malheureusement, rien n’est gratuit ; les avantages que procure l’accroissement automatique de
la taille d’une liste de tableaux exigent une syntaxe plus complexe pour accéder aux éléments, car la
classe ArrayList ne fait pas partie du langage Java ; il s’agit d’une classe utilitaire tiers ajoutée
à la bibliothèque standard.
Au lieu d’employer la syntaxe [], bien pratique pour accéder à un élément d’un tableau ou le modifier,
il faut appeler les méthodes get et set.
Par exemple, pour définir le ième élément, écrivez
staff.set(i, harry);
ce qui est équivalent à
a[i] = harry;
pour un tableau a (comme pour les tableaux, les valeurs d’index sont basées sur zéro).
Pour obtenir un élément de liste de tableau, utilisez
Employee e = staff.get(i);
ce qui est équivalent à :
Employee e = a[i];
Depuis le JDK 5.0, vous pouvez utiliser la boucle "for each" pour les listes de tableaux :
for (Employee e : staff)
// faire quelque chose avec e
Dans le code existant, la même boucle s’écrirait :
for (int i = 0; i < staff.size(); i++)
{
Livre Java .book Page 211 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
211
Employee e = (Employee) staff.get(i);
faire quelque chose avec e
}
INFO
Avant le JDK 5.0, les classes génériques n’existaient pas et la méthode get de la classe brute ArrayList n’avait
d’autre choix que de renvoyer un Object. Les éléments qui appelaient get devaient donc transtyper la valeur
renvoyée sur le type souhaité :
Employee e = (Employee) staff.get(i);
Le ArrayList brut est aussi un peu dangereux. Ses méthodes add et set acceptent des objets de n’importe quel
type. Un appel
staff.set(i, new Date());
se compile quasiment sans aucun avertissement et ne vous pose problème que lorsque vous récupérez l’objet et que
vous essayez de le transtyper. Si vous utilisez plutôt un ArrayList<Employee>, le compilateur détecte cette erreur.
ASTUCE
Une petite astuce permet de profiter d’un double avantage — une croissance flexible et un accès pratique aux
éléments. Créez d’abord une liste de tableaux et ajoutez-y tous ses éléments :
ArrayList<X> list = new ArrayList<X>();
while (. . .)
{
x = . . .;
list.add(x);
}
Utilisez ensuite la méthode toArray pour copier les éléments dans un tableau :
X[] a = new X[list.size()];
list.toArray(a);
ATTENTION
N’appelez pas list.set(i, x) tant que la taille de la liste de tableaux n’est pas supérieure à i. L’exemple suivant
est incorrect :
ArrayList<Employee> list = new ArrayList<Employee>(100); // capacité 100, taille 0
list.set(0, x); // pas encore d’élément 0
Appelez la méthode add au lieu de set pour remplir un tableau, et n’employez set que pour remplacer un élément
déjà ajouté.
Au lieu d’ajouter des éléments à la fin d’une liste de tableaux, vous pouvez aussi les insérer au
milieu :
int n = staff.size() / 2;
staff.add(n, e);
Livre Java .book Page 212 Jeudi, 25. novembre 2004 3:04 15
212
Au cœur de Java 2 - Notions fondamentales
Les éléments situés aux emplacements n et supérieurs sont décalés pour faire place au nouvel
élément. Si la nouvelle taille de la liste de tableaux après l’insertion excède la capacité, il est procédé
à une réallocation.
Il est également possible de supprimer un élément au milieu d’une liste de tableaux :
Employee e = staff.remove(n);
Les éléments situés au-dessus de l’élément supprimé sont décalés vers le bas, et la taille du tableau
est réduite de 1.
L’insertion et la suppression d’éléments ne sont pas très efficaces. On ne s’en préoccupe guère dans
le cas de petites listes de tableaux. Si vous devez ajouter ou supprimer fréquemment des éléments au
milieu d’une collection, il est préférable d’utiliser une liste liée. La programmation avec les listes
liées est étudiée au Chapitre 2 du Volume 2.
L’Exemple 5.4 est une modification du programme EmployeeTest du Chapitre 4. Le Tableau
Employee[] est remplacé par un ArrayList<Employee>. Remarquez les changements suivants :
m
Il n’est pas nécessaire de spécifier la taille du tableau.
m
Vous appelez add pour ajouter autant d’éléments que vous voulez.
m
Vous utilisez size() au lieu de length pour compter le nombre d’éléments.
m
Vous accédez à un élément à l’aide de a.get(i) au lieu de a[i].
Exemple 5.4 : ArrayListTest.java
import java.util.*;
public class ArrayListTest
{
public static void main(String[] args)
{
// remplir le tableau staff avec trois objets Employee
ArrayList<employee> staff = new ArrayList<employee>();
staff.add(new Employee("Carl Cracker", 75000,
1987, 12, 15));
staff.add(new Employee("Harry Hacker", 50000,
1989, 10, 1));
staff.add(new Employee("Tony Tester", 40000,
1990, 3, 15));
// augmenter le salaire de tout le monde de 5%
for (Employee e : staff)
e.raiseSalary(5);
// afficher les informations concernant tous les objets Employee
for (Employee e : staff)
System.out.println("name=" + e.getName()
+ ",salary=" + e.getSalary()
+ ",hireDay=" + e.getHireDay());
}
}
Livre Java .book Page 213 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
213
class Employee
{
public Employee(String n, double s,
int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar
= new GregorianCalendar(year, month - 1, day);
hireDay = calendar.getTime();
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
private String name;
private double salary;
private Date hireDay;
}
java.util.ArrayList<T> 1.2
•
void set(int index, T obj)
Place une valeur dans la liste de tableau à l’index spécifié, ce qui remplace le contenu précédent.
Paramètres :
size() -1.
•
index
La position d’insertion, qui doit être comprise entre 0 et
obj
La nouvelle valeur
T get(int index)
Récupère la valeur stockée à l’index spécifié.
Paramètres :
index
L’index de l’élément à récupérer, qui doit être compris entre
0 et size() -1.
Livre Java .book Page 214 Jeudi, 25. novembre 2004 3:04 15
214
•
Au cœur de Java 2 - Notions fondamentales
void add(int index, T obj)
Place un nouvel élément à la position spécifiée, en décalant les autres éléments vers le haut.
Paramètres :
•
index
La position d’insertion, qui doit être comprise entre 0 et
size() -1.
obj
Le nouvel élément.
T remove(int index)
Supprime un élément et décale vers le bas les éléments d’indice supérieur. L’élément supprimé
est renvoyé.
Paramètres :
index
L’indice de l’élément à supprimer. Il doit être compris entre
0 et size() -1.
Compatibilité entre les listes de tableaux brutes et tapées
Lorsque vous écrivez un nouveau code avec JDK 5.0 et versions ultérieures, utilisez des paramètres
de type, comme ArrayList<Employee>, pour les listes de tableau. Vous pouvez toutefois avoir
besoin d’interagir avec le code existant, qui utilise le type ArrayList brut.
Supposons que vous disposiez de la classe existante suivante :
public class EmployeeDB
{
public void update(ArrayList list) { ... }
public ArrayList find(String query) { ... }
}
Vous pouvez transférer une liste de tableau tapée à la méthode update sans autre transtypage :
ArrayList<Employee> staff = ...;
employeeDB.update(staff);
L’objet staff est simplement transféré à la méthode update.
ATTENTION
Même si vous ne recevez pas d’erreur ou d’avertissement du compilateur, cet appel n’est pas tout à fait sûr. La
méthode update pourrait ajouter des éléments dans la liste de tableau de type Employee. Une exception
survient lors de la récupération de ces éléments. Ceci peut paraître effrayant mais, en y réfléchissant bien, le
comportement est simplement le même que celui avant le JDK 5.0. L’intégrité de la machine n’est donc jamais
remise en cause. Dans ce cas, vous ne perdez pas en sécurité, mais vous ne profitez pas non plus des vérifications
de compilation.
A l’inverse, lorsque vous attribuez un ArrayList brut à un ArrayList tapé, vous obtenez un avertissement :
ArrayList<Employee> result = employeeDB.find(query); // produit un avertissement
INFO
Pour voir le texte de l’avertissement, vous devez procéder à la compilation avec l’option -Xlint:unchecked.
Livre Java .book Page 215 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
215
Utiliser un transtypage ne permet pas de se débarrasser de l’avertissement :
ArrayList<Employee> result = (ArrayList<Employee>) employeeDB.find(query);
➥// produit un autre avertissement
Vous obtenez un autre avertissement, vous indiquant que le transtypage est erroné.
Ceci est la conséquence d’une limitation assez malheureuse des types avec paramètres dans Java.
Pour des raisons de compatibilité, le compilateur traduit toutes les listes de tableaux tapées en objets
ArrayList bruts, après avoir vérifié que les règles de type n’ont pas été violées. Dans un programme
en cours d’exécution, toutes les listes de tableaux sont identiques, il n’y a pas de paramètres de types
dans la machine virtuelle. Ainsi, les transtypages (ArrayList) et (ArrayList<Employee>) transportent des vérifications d’exécution identiques.
Vous ne pouvez pas faire grand-chose face à cette situation. Lorsque vous interagissez avec du code
existant, étudiez les avertissements du compilateur et persuadez-vous que les avertissements ne sont
pas sérieux.
Enveloppes d’objets et autoboxing
Il est parfois nécessaire de convertir un type primitif — comme int — en un objet. Tous les types
primitifs ont une contrepartie sous forme de classe. Par exemple, il existe une classe Integer correspondant au type primitif int. Une classe de cette catégorie est généralement appelée classe enveloppe (object wrapper). Les classes enveloppes portent des noms correspondant aux types (en
anglais) : Integer, Long, Float, Double, Short, Byte, Character, Void et Boolean (les six
premières héritent de la superclasse Number). Les classes enveloppes sont inaltérables : vous ne
pouvez pas modifier une valeur enveloppée, une fois l’enveloppe construite. Elles sont aussi final,
vous ne pouvez donc pas en faire des sous-classes.
Supposons que nous voulions travailler sur une liste d’entiers. Malheureusement, le paramètre de
type entre les signes ne peut pas être un type primitif. Il n’est pas possible de former un ArrayList<int>. C’est ici que la classe enveloppe Integer fait son apparition. Vous pouvez déclarer une
liste de tableaux d’objets Integer :
ArrayList<Integer> list = new ArrayList<Integer>();
ATTENTION
Un ArrayList<Integer> est bien moins efficace qu’un tableau int[] car chaque valeur est enveloppée séparément dans un objet. Vous ne voudriez utiliser cette construction que pour les petites collections lorsque la commodité du programmeur est plus importante que l’efficacité.
Une autre innovation du JDK 5.0 facilite l’ajout et la récupération d’éléments de tableau. L’appel
list.add(3);
est automatiquement traduit en
list.add(new Integer(3));
Cette conversion est appelée autoboxing.
Livre Java .book Page 216 Jeudi, 25. novembre 2004 3:04 15
216
Au cœur de Java 2 - Notions fondamentales
INFO
Vous pourriez penser que l’enveloppement automatique est plus cohérent, mais la métaphore "boxing" (emballage)
provient du C#.
A l’inverse, lorsque vous attribuez un objet Integer à une valeur int, il est automatiquement
déballé (unboxing). En fait, le compilateur traduit
int n = list.get(i);
en
int n = list.get(i).intValue();
Les opérations d’autoboxing et d’unboxing fonctionnent même avec les expressions arithmétiques.
Vous pouvez par exemple appliquer l’opération d’incrémentation en une référence d’enveloppe :
Integer n = 3;
n++;
Le compilateur insère automatiquement des instructions pour déballer l’objet, augmenter la valeur
du résultat et le remballer.
Dans la plupart des cas, vous avez l’illusion que les types primitifs et leurs enveloppes ne sont qu’un
seul et même élément. Ils ne diffèrent considérablement qu’en un seul point : l’identité. Comme
vous le savez, l’opérateur ==, appliqué à des objets d’enveloppe, ne teste que si les objets ont des
emplacements mémoire identiques. La comparaison suivante échouerait donc probablement :
Integer a = 1000;
Integer b = 1000;
if (a == b) ...
Toutefois, une implémentation Java pourrait, si elle le choisit, envelopper des valeurs communes
dans des objets identiques, et la comparaison pourrait donc réussir. Cette ambiguïté n’est pas
souhaitable. La solution consiste à appeler la méthode equals lorsque l’on compare des objets
d’enveloppe.
INFO
La caractéristique d’autoboxing exige que boolean, byte, char = 127 et que short et int compris entre −128 et
127 soient enveloppés dans des objets fixes. Par exemple, si a et b avaient été initialisés avec 100 dans l’exemple
précédent, la comparaison aurait réussi.
Enfin, soulignons que le boxing et l’unboxing sont proposés par le compilateur et non par la machine
virtuelle. Le compilateur insère les appels nécessaires lorsqu’il génère les bytecodes d’une classe.
La machine virtuelle exécute simplement ces bytecodes.
Les classes enveloppe existent depuis le JDK 1.0 mais, avant le JDK 5.0, vous deviez insérer à la
main le code pour le boxing et l’unboxing.
Vous verrez souvent les enveloppes de nombres pour une autre raison. Les concepteurs de Java ont
découvert que les enveloppes constituent un endroit pratique pour y stocker certaines méthodes de
base, comme celles qui permettent de convertir des chaînes de chiffres en nombres.
Livre Java .book Page 217 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
217
Pour transformer une chaîne en entier, vous devez utiliser l’instruction suivante :
int x = Integer.parseInt(s);
Ceci n’a rien à voir avec les objets Integer ; parseInt est une méthode statique. Mais la classe
Integer constitue un bon endroit pour l’y placer.
Les notes API montrent quelques méthodes parmi les plus importantes de la classe Integer. Les
autres classes de nombre implémentent les méthodes correspondantes.
ATTENTION
Certains pensent, à tort, que les classes enveloppes peuvent être employées pour implémenter des méthodes capables de modifier les paramètres numériques. Nous avons vu au Chapitre 4 qu’il était impossible d’écrire une méthode
Java qui incrémente un paramètre entier, car les paramètres des méthodes Java sont toujours passés par valeur.
public static void triple(int x) // ne marchera pas
{
X = 3 * x; // modifie la variable locale
}
Mais ne pourrait-on pas contourner ce problème en substituant un Integer à un int ?
public static void triple(Integer x) // ne marchera pas
{
. . .
}
Le problème vient du fait que les objets Integer sont inaltérables : l’information contenue dans l’enveloppe ne
peut pas changer. Vous ne pouvez pas employer les classes enveloppes pour créer une méthode qui modifie les paramètres numériques.
INFO
Si vous désirez écrire une méthode qui modifie des paramètres numériques, vous pouvez utiliser un des types holder
définis dans le package org.omg.CORBA. Il existe des types IntegerHolder, BooleanHolder, etc. Chaque type
holder a un champ public value grâce auquel vous pouvez accéder à la valeur stockée :
public static void triple(IntHolder x)
{
x.value = 3 * x.value;
}
java.lang.Integer 1.0
•
int intValue()
Renvoie la valeur de cet objet Integer dans un résultat de type int (surcharge la méthode
intValue de la classe Number).
•
static String toString(int i)
Renvoie un nouvel objet chaîne représentant le nombre, en base 10.
•
static String toString(int i, int radix)
Permet de renvoyer une représentation du nombre i dans la base spécifiée par le paramètre
radix.
Livre Java .book Page 218 Jeudi, 25. novembre 2004 3:04 15
218
•
Au cœur de Java 2 - Notions fondamentales
static int parseInt(String s)
Renvoie la valeur d’entier de la chaîne s, si elle représente un nombre entier en base 10.
•
static int parseInt(String s, int radix)
Renvoie la valeur d’entier de la chaîne s, si elle représente un nombre entier exprimé dans la
base spécifiée par le paramètre radix.
•
static Integer valueOf(String s)
Renvoie un nouvel objet Integer initialisé avec la valeur de la chaîne spécifiée, si celle-ci représente un nombre entier en base 10.
•
static Integer valueOf(String s, int radix)
Renvoie un nouvel objet Integer initialisé avec la valeur de la chaîne s, si celle-ci représente un
nombre entier exprimé dans la base spécifiée par le paramètre radix.
java.text.NumberFormat 1.1
•
Number parse(String s)
Renvoie la valeur numérique, en supposant que la chaîne spécifiée représente un nombre.
Méthodes ayant un nombre variable de paramètres
Avant JDK 5.0, chaque méthode Java disposait d’un nombre fixe de paramètres. Il est toutefois
maintenant possible de fournir des méthodes qui peuvent être appelées avec un nombre variable de
paramètres (quelquefois appelés méthodes "varargs").
Vous avez déjà vu une telle méthode, la méthode printf. Par exemple, les appels
System.out.printf("%d", n);
et
System.out.printf("%d %s", n, "widgets");
appellent tous deux la même méthode, même si l’un a deux paramètres et l’autre, trois.
La méthode printf est définie comme ceci :
public class PrintStream
{
public PrintStream printf(String fmt, Object... args)
➥{ return format(fmt, args); }
}
Ici, l’ellipse... fait partie du code Java. Elle montre que la méthode peut recevoir un nombre arbitraire d’objets (en plus du paramètre fmt).
La méthode printf reçoit en fait deux paramètres, la chaîne format et un tableau Object[] qui
contient tous les autres paramètres (si l’appelant fournit des entiers ou autres valeurs de type
primitif, l’autoboxing les transforme en objets). Elle dispose maintenant de la tâche non enviable
d’analyser la chaîne fmt et de se conformer au spécificateur du format ith avec la valeur
args[i].
Autrement dit, pour l’implémenteur de printf, le type de paramètre Object... est exactement le
même que Object[].
Livre Java .book Page 219 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
219
Le compilateur doit transformer chaque appel à printf, en regroupant les paramètres dans un
tableau et en procédant à l’autoboxing en fonction des besoins :
System.out.printf("%d %d", new Object[] { new Integer(d), "widgets" } );
Vous pouvez définir vos propres méthodes avec des paramètres variables et spécifier tout type pour
les paramètres, même un type primitif. Voici un exemple simple : une fonction qui calcule le maximum
d’un nombre variable de valeurs :
public static double max(double... values)
{
double largest = Double.MIN_VALUE;
for (double v : values) if (v > largest) largest = v;
return largest;
}
Appelez simplement la fonction comme ceci :
double m = max(3.1, 40.4, -5);
Le compilateur transfère un nouveau double[] { 3.1, 40.4, -5 } à la fonction max.
INFO
Le transfert d’un tableau comme dernier paramètre d’une méthode avec des paramètres variables est autorisé, par
exemple :
System.out.printf("%d %s", new Object[] { new Integer(1), "widgets" } );
Vous pouvez donc redéfinir une fonction existante dont le dernier paramètre est un tableau par une méthode disposant de paramètres variables, sans casser un code existant. MessageFormat.format a, par exemple, été amélioré dans
ce sens dans le JDK 5.0.
Réflexion
La bibliothèque de réflexion constitue une boîte à outils riche et élaborée pour écrire des programmes qui manipulent dynamiquement du code Java. Cette fonctionnalité est très largement utilisée
dans JavaBeans, l’architecture des composants Java (voir le Volume 2 pour en savoir plus sur JavaBeans). Au moyen de la réflexion, Java est capable de supporter des outils comme ceux auxquels les
utilisateurs de Visual Basic sont habitués. Précisément, lorsque de nouvelles classes sont ajoutées au
moment de la conception ou de l’exécution, des outils de développement d’application rapide
peuvent se renseigner dynamiquement sur les capacités des classes qui ont été ajoutées.
Un programme qui peut analyser les capacités des classes est appelé réflecteur. Le mécanisme de
réflexion est extrêmement puissant. Comme vous le montrent les sections suivantes, il peut être
employé pour :
m
analyser les capacités des classes au moment de l’exécution ;
m
étudier les objets au moment de l’exécution, par exemple pour écrire une seule méthode
toString qui fonctionne pour toutes les classes ;
m
implémenter un code générique pour la manipulation des tableaux ;
m
profiter des objets Method qui se comportent comme les pointeurs de fonction des langages tels
que C++.
Livre Java .book Page 220 Jeudi, 25. novembre 2004 3:04 15
220
Au cœur de Java 2 - Notions fondamentales
La réflexion est un mécanisme puissant et complexe ; il intéresse toutefois principalement les
concepteurs d’outils, et non les programmeurs d’applications. Si vous vous intéressez à la programmation des applications plutôt qu’aux outils pour les programmeurs Java, vous pouvez ignorer sans
problème le reste de ce chapitre et y revenir par la suite.
La classe Class
Lorsque le programme est lancé, le système d’exécution de Java gère, pour tous les objets, ce que
l’on appelle "l’identification de type à l’exécution". Cette information mémorise la classe à laquelle
chaque objet appartient. L’information de type au moment de l’exécution est employée par la
machine virtuelle pour sélectionner les méthodes correctes à exécuter.
Vous pouvez accéder à cette information en travaillant avec une classe Java particulière, baptisée
singulièrement Class. La méthode getClass() de la classe Object renvoie une instance de type
Class :
Employee e;
. . .
Class cl = e.getClass();
Tout comme un objet Employee décrit les propriétés d’un employé particulier, un objet Class décrit
les propriétés d’une classe particulière. La méthode la plus utilisée de Class est sans conteste
getName, qui renvoie le nom de la classe. Par exemple, l’instruction
System.out.println(e.getClass().getName() + " " + e.getName());
affiche
Employee Harry Hacker
si e est un employé ou
Manager Harry Hacker
si e est un directeur.
Vous pouvez aussi obtenir un objet Class correspondant à une chaîne, à l’aide de la méthode statique
forName :
String className = "java.util.Date";
Class cl = Class.forName(className);
Cette technique est utilisée si le nom de classe est stocké dans une chaîne qui peut changer à l’exécution. Cela fonctionne lorsque className est bien le nom d’une classe ou d’une interface. Dans le cas
contraire, la méthode forName lance une exception vérifiée. Consultez le texte encadré plus loin pour
voir comment fournir un gestionnaire d’exception lorsque vous utilisez cette méthode.
ASTUCE
Au démarrage, la classe contenant votre méthode main est chargée. Elle charge toutes les classes dont elle a besoin.
Chacune de ces classes charge les classes qui lui sont nécessaires, et ainsi de suite. Cela peut demander un certain
temps pour une application importante, ce qui est frustrant pour l’utilisateur. Vous pouvez donner aux utilisateurs
de votre programme l’illusion d’un démarrage plus rapide, à l’aide de l’astuce suivante. Assurez-vous que la classe
contenant la méthode main ne fait pas explicitement référence aux autres classes. Affichez d’abord un écran splash,
puis forcez manuellement le chargement des autres classes en appelant Class.forName.
Livre Java .book Page 221 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
221
Une troisième technique emploie un raccourci pratique pour obtenir un objet de type Class. En
effet, si T est d’un type Java quelconque, alors T.class représente l’objet classe correspondant.
Voici quelques exemples :
Class cl1 = Date.class; // si vous importez java.util.*;
Class cl2 = int.class;
Class cl3 = Double[].class;
Remarquez qu’un objet Class désigne en réalité un type, qui n’est pas nécessairement une classe.
Par exemple, int n’est pas une classe, mais int.class est pourtant un objet du type Class.
INFO
Depuis le JDK 5.0, la classe Class possède des paramètres. Par exemple, Employee.class est de type
Class<Employee>. Nous ne traiterons pas de ce problème car il compliquerait encore un concept déjà abstrait.
Dans un but pratique, vous pouvez ignorer le paramètre de type et travailler avec le type Class brut. Voyez le Chapitre 13 pour en savoir plus sur ce problème.
ATTENTION
Pour des raisons historiques, la méthode getName renvoie des noms étranges pour les types tableaux :
• Double[].class.getName() returns "[Ljava.lang.Double;".
• int[].class.getName() returns "[I".
La machine virtuelle gère un unique objet Class pour chaque type. Vous pouvez donc utiliser
l’opérateur == pour comparer les objets class, par exemple :
if (e.getClass() == Employee.class) . . .
Interception d’exceptions
La gestion des exceptions est vue en détail au Chapitre 11 mais, en attendant, vous pouvez
rencontrer des cas où les méthodes menacent de lancer des exceptions.
Lorsqu’une erreur se produit au moment de l’exécution, un programme peut "lancer une exception". L’action de lancer une exception est plus souple que de terminer le programme, car vous
pouvez fournir un gestionnaire qui "intercepte" l’exception et la traite.
Si vous ne fournissez pas de gestionnaire, le programme se termine pourtant et affiche à la
console un message donnant le type de l’exception. Vous avez peut-être déjà vu un compte rendu
d’exception si vous employez à tort une référence null ou que vous dépassiez les limites d’un
tableau.
Il existe deux sortes d’exceptions : vérifiées et non vérifiées (checked/unchecked). Dans le cas
d’exceptions vérifiées, le compilateur vérifie que vous avez fourni un gestionnaire. Cependant, de
nombreuses exceptions communes, telle qu’une tentative d’accéder à une référence null, ne sont
pas vérifiées. Le compilateur ne vérifie pas si vous fournissez un gestionnaire pour ces erreurs —
après tout, vous êtes censé mobiliser votre énergie pour éviter ces erreurs plutôt que de coder des
gestionnaires pour les traiter.
Livre Java .book Page 222 Jeudi, 25. novembre 2004 3:04 15
222
Au cœur de Java 2 - Notions fondamentales
Mais toutes les erreurs ne sont pas évitables. Si une exception peut se produire en dépit de vos
efforts, le compilateur s’attendra à ce que vous fournissiez un gestionnaire. Class.forName est un
exemple de méthode qui déclenche une exception vérifiée. Vous étudierez, au Chapitre 11,
plusieurs stratégies de gestion d’exception. Pour l’instant, nous nous contenterons de vous indiquer
l’implémentation du gestionnaire le plus simple.
Placez une ou plusieurs méthodes pouvant lancer des exceptions vérifiées, dans un bloc d’instructions
try, puis fournissez le code du gestionnaire dans la clause catch.
try
{
instructions pouvant déclencher des exceptions
}
catch(Exception e)
{
action du gestionnaire
}
En voici un exemple :
try
{ String name = . . .; // extraire le nom de classe
Class cl = Class.forName(name); // peut lancer une exception
. . . // faire quelque chose avec cl
}
catch(Exception e)
{
e.printStackTrace();
}
Si le nom de classe n’existe pas, le reste du code dans le bloc try est sauté, et le programme se
poursuit à la clause catch (ici, nous imprimons une trace de pile à l’aide de la méthode printStackTrace de la classe Throwable qui est la superclasse de la classe Exception). Si aucune des
méthodes dans le bloc try ne déclenche d’exception, le code du gestionnaire dans la clause catch
est sauté.
Vous n’avez besoin de fournir de gestionnaire d’exception que pour les exceptions vérifiées. Il est
facile de déterminer les méthodes qui lancent des exceptions vérifiées — le compilateur protestera
quand vous appellerez une méthode menaçant de lancer une exception vérifiée et que ne fournirez
pas de gestionnaire.
Il existe une autre méthode utile qui permet de créer une instance de classe à la volée. Cette méthode
se nomme, assez naturellement, newInstance(). Par exemple,
e.getClass().newInstance();
crée une nouvelle instance du même type de classe que e. La méthode newInstance appelle le
constructeur par défaut (celui qui ne reçoit aucun paramètre) pour initialiser l’objet nouvellement
créé. Une exception est déclenchée si la classe ne dispose pas de constructeur par défaut.
Une combinaison de forName et de newInstance permet de créer un objet à partir d’un nom de
classe stocké dans une chaîne :
String s = "java.util.Date";
Object m = Class.forName(s).newInstance();
Livre Java .book Page 223 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
223
INFO
Vous ne pourrez pas employer cette technique si vous devez fournir des paramètres au constructeur d’une classe que
vous souhaitez instancier à la volée. Vous devrez recourir à la méthode newInstance de la classe Constructor (il
s’agit d’une des classes du package java.lang.reflect, dont nous parlerons dans la section suivante).
INFO C++
La méthode newInstance correspond à un constructeur virtuel en C++. Cependant, la construction virtuelle en C++
n’est pas une caractéristique du langage, mais une simple tournure idiomatique qui doit être prise en charge par une
bibliothèque spécialisée. Class est comparable à la classe type_info de C++, et la méthode getClass équivaut à
l’opérateur typeid. Néanmoins, la classe Class de Java est plus souple que type_info qui peut seulement fournir
le nom d’un type, et non créer de nouveaux objets de ce type.
java.lang.Class 1.0
•
static Class forName(String className)
Renvoie l’objet Class qui représente la classe ayant pour nom className.
•
Object newInstance()
Renvoie une nouvelle instance de cette classe.
java.lang.reflect.Constructor 1.1
•
Object newInstance(Object[] args)
Construit une nouvelle instance de la classe déclarante du constructeur.
Paramètres :
args
Les paramètres fournis au constructeur. Voir la section
relative à la réflexion pour plus d’informations sur la façon
de fournir les paramètres.
java.lang.Throwable 1.0
•
void printStackTrace()
Affiche l’objet Throwable et la trace de la pile dans l’unité d’erreur standard.
La réflexion pour analyser les caractéristiques d’une classe
Nous vous proposons un bref aperçu des éléments les plus importants du mécanisme de réflexion,
car il permet d’examiner la structure d’une classe.
Les trois classes Field, Method et Constructor, qui se trouvent dans le package java.lang.reflect,
décrivent respectivement les champs, les méthodes et les constructeurs d’une classe. Ces trois classes disposent d’une méthode getName qui renvoie le nom de l’élément. La classe Field possède une
méthode getType renvoyant un objet, de type Class, qui décrit le type du champ. Les classes
Method et Constructor possèdent des méthodes permettant d’obtenir les types des paramètres, et la
classe Method signale aussi le type de retour. Les trois classes possèdent également une méthode
appelée getModifiers : elle renvoie un entier dont les bits sont utilisés comme sémaphores pour
décrire les modificateurs spécifiés, tels que public ou static. Vous pouvez alors utiliser les méthodes statiques de la classe Modifier du package java.lang.reflect pour analyser les entiers
renvoyés par getModifiers. Par exemple, il existe des méthodes telles que isPublic, isPrivate ou
isFinal pour déterminer si un constructeur ou une méthode a été déclarée public, private ou final.
Livre Java .book Page 224 Jeudi, 25. novembre 2004 3:04 15
224
Au cœur de Java 2 - Notions fondamentales
Il vous suffit d’appeler la méthode appropriée de Modifier et de l’utiliser sur l’entier renvoyé par
getModifiers. Il est également possible d’employer Modifier.toString pour afficher les modificateurs.
Les méthodes getFields, getMethods et getConstructors de la classe Class renvoient dans
des tableaux les éléments publics, les méthodes et les constructeurs gérés par la classe. Ces
éléments sont des objets de la classe correspondante de java.lang.reflect. Cela inclut les
membres publics des superclasses. Les méthodes getDeclaredFields, getDeclaredMethods et
getDeclaredConstructors de Class renvoient des tableaux constitués de tous les champs,
méthodes et constructeurs déclarés dans la classe, y compris les membres privés et protégés,
mais pas les membres des superclasses.
L’Exemple 5.5 explique comment afficher toutes les informations relatives à une classe. Le
programme vous demande le nom d’une classe, puis affiche la signature de chaque méthode et de
chaque constructeur ainsi que le nom de chaque champ de données de la classe en question. Par
exemple, si vous tapez :
java.lang.Double
Le programme affiche :
class java.lang.Double extends java.lang.Number
{
public java.lang.Double(java.lang.String);
public java.lang.Double(double);
public
public
public
public
public
public
public
public
public
public
public
public
public
public
public
public
public
public
public
public
public
int hashCode();
int compareTo(java.lang.Object);
int compareTo(java.lang.Double);
boolean equals(java.lang.Object);
java.lang.String toString();
static java.lang.String toString(double);
static java.lang.Double valueOf(java.lang.String);
static boolean isNaN(double);
boolean isNaN();
static boolean isInfinite(double);
boolean isInfinite();
byte byteValue();
short shortValue();
int intValue();
long longValue();
float floatValue();
double doubleValue();
static double parseDouble(java.lang.String);
static native long doubleToLongBits(double);
static native long doubleToRawLongBits(double);
static native double longBitsToDouble(long);
public static final double POSITIVE_INFINITY;
public static final double NEGATIVE_INFINITY;
public static final double NaN;
public static final double MAX_VALUE;
public static final double MIN_VALUE;
public static final java.lang.Class TYPE;
private double value;
private static final long serialVersionUID;
}
Livre Java .book Page 225 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
225
Ce qui est remarquable dans ce programme, c’est qu’il peut analyser toute classe apte à être chargée
par l’interpréteur, et pas seulement les classes disponibles au moment où le programme est compilé.
Nous le réutiliserons au chapitre suivant pour examiner les classes internes générées automatiquement
par le compilateur Java.
Exemple 5.5 : ReflectionTest.java
import java.util.*;
import java.lang.reflect.*;
public class ReflectionTest
{
public static void main(String[] args)
{
/* lire le nom de classe dans les args de ligne de commande
ou l’entrée de l’utilisateur */
String name;
if (args.length > 0)
name = args[0];
else
{
Scanner in = new Scanner(System.in);
System.out.println("Enter class Name ("e.g. java.util.Date): ");
name = in.next();
}
try
{
/* afficher le nom de classe et de superclasse
(if != Object) */
Class cl = Class.forName(name);
Class supercl = cl.getSuperclass();
System.out.print("class " + name);
if (supercl != null && supercl != Object.class)
System.out.print(" extends " + supercl.getName());
System.out.print("\n{\n");
printConstructors(cl);
System.out.println();
printMethods(cl);
System.out.println();
printFields(cl);
System.out.println("}");
}
catch(ClassNotFoundException e) { e.printStackTrace(); }
System.exit(0);
}
/**
affiche tous les constructeurs d’une classe
@param cl Une classe
*/
public static void printConstructors(Class cl)
{
Constructor[] constructors = cl.getDeclaredConstructors();
for (Constructor c : constructors)
{
Livre Java .book Page 226 Jeudi, 25. novembre 2004 3:04 15
226
Au cœur de Java 2 - Notions fondamentales
String name = c.getName();
System.out.print("
" + Modifier.toString(c.getModifiers()));
System.out.print(" " + name + "(");
// afficher les types des paramètres
Class[] paramTypes = c.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++)
{
if (j > 0) System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
imprime toutes les méthodes d’une classe
@param cl Une classe
*/
public static void printMethods(Class cl)
{
Method[] methods = cl.getDeclaredMethods();
for (Method m : methods)
Class retType = m.getReturnType();
String name = m.getName();
/* affiche les modificateurs, le type renvoyé
et le nom de la méthode */
System.out.print("
" + Modifier.toString(m.getModifiers()));
System.out.print("
" + retType.getName() + " " + name
+ "(");
// imprime les types des paramètres
Class[] paramTypes = m.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++)
{
if (j > 0) System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
Affiche tous les champs d’une classe
@param cl Une classe
*/
public static void printFields(Class cl)
{
Field[] fields = cl.getDeclaredFields();
for (Field f : fields)
Class type = f.getType();
String name = f.getName();
Livre Java .book Page 227 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
System.out.print("
System.out.println("
+ ";");
227
" + Modifier.toString(f.getModifiers()));
" + type.getName() + " " + name
}
}
}
java.lang.Class 1.0
Field[] getFields() 1.1
Field[] getDeclaredFields() 1.1
•
•
La méthode getFields renvoie un tableau contenant les objets Field représentant les champs
publics de cette classe ou de ses superclasses. La méthode getDeclaredFields renvoie un
tableau d’objets Field pour tous les champs de cette classe. Ces méthodes renvoient un tableau
de longueur 0 s’il n’y a pas de champ correspondant ou si l’objet Class représente un type
primitif ou un type tableau.
•
•
Method[] getMethods 1.1()
Method[] getDeclaredMethods() 1.1
Renvoient un tableau qui contient des objets Method : getMethods renvoie des méthodes publiques et inclut des méthodes héritées ; getDeclaredMethods renvoie toutes les méthodes de cette
classe ou interface mais n’inclut pas les méthodes héritées.
•
•
Constructor[] getConstructors 1.1 ()
Constructor[] getDeclaredConstructors() 1.1
Renvoient un tableau d’objets Constructor représentant tous les constructeurs publics (avec
getConstructors) ou tous les constructeurs (avec getDeclaredConstructors) de la classe
désignée par cet objet Class.
java.lang.reflect.Field 1.1
java.lang.reflect.Method 1.1
java.lang.reflect.Constructor 1.1
• Class getDeclaringClass()
Renvoie l’objet Class de la classe qui définit ce constructeur, cette méthode ou ce champ.
•
Class[] getExceptionTypes()
Dans les classes Constructor et Method, renvoie un tableau d’objets Class qui représentent les
types d’exceptions déclenchées par la méthode.
•
int getModifiers()
Renvoie un entier décrivant les modificateurs du constructeur, de la méthode ou du champ. Utilisez
les méthodes de la classe Modifier pour analyser le résultat.
•
String getName()
Renvoie une chaîne qui donne le nom du constructeur, de la méthode ou du champ.
•
Class[] getParameterTypes()
Dans les classes Constructor et Method, renvoie un tableau d’objets Class qui représentent les
types des paramètres.
•
Class getReturnType() (dans les classes Method)
Renvoie un objet Class qui représente le type de retour.
Livre Java .book Page 228 Jeudi, 25. novembre 2004 3:04 15
228
Au cœur de Java 2 - Notions fondamentales
java.lang.reflect.Modifier 1.1
•
static String toString(int modifiers)
Renvoie une chaîne avec les modificateurs correspondant aux bits définis dans modifiers.
•
•
•
•
•
•
•
•
•
•
•
static boolean isAbstract(int modifiers)
static boolean isFinal(int modifiers)
static boolean isInterface(int modifiers)
static boolean isNative(int modifiers)
static boolean isPrivate(int modifiers)
static boolean isProtected(int modifiers)
static boolean isPublic(int modifiers)
static boolean isStatic(int modifiers)
static boolean isStrict(int modifiers)
static boolean isSynchronized(int modifiers)
static boolean isVolatile(int modifiers)
Ces méthodes testent le bit dans la valeur modifiers qui correspond à l’élément modificateur du
nom de la méthode.
La réflexion pour l’analyse des objets à l’exécution
Dans la section précédente, nous avons vu comment trouver le nom et le type des champs de
n’importe quel objet :
m
obtenir l’objet Class correspondant ;
m
appeler getDeclaredFields sur l’objet Class.
Dans cette section, nous allons franchir une étape supplémentaire et étudier le contenu des champs.
Bien entendu, il est facile de lire le contenu d’un champ spécifique d’un objet dont le nom et le type
sont connus lors de l’écriture du programme. Mais la réflexion nous permet de lire les champs des
objets qui n’étaient pas connus au moment de la compilation.
A cet égard, la méthode essentielle est la méthode get de la classe Field. Si f est un objet de type
Field (obtenu par exemple grâce à getDeclaredFields) et obj un objet de la classe dont f est un
champ, alors f.get(obj) renvoie un objet dont la valeur est la valeur courante du champ de obj.
Tout cela peut paraître un peu abstrait ; nous allons donc prendre un exemple :
Employee harry = new Employee("Harry Hacker", 35000,
10, 1, 1989);
Class cl = harry.getClass();
// l’objet class représentant Employee
Field f = cl.getDeclaredField("name");
// le champ name de la classe Employee
Object v = f.get(harry);
/* la valeur du champ name de l’objet harry
c’est-à-dire l’objet String "Harry Hacker" */
En fait, ce code pose un problème. Comme le champ name est un champ privé, la méthode get
déclenche une exception IllegalAccessException (Exception pour accès interdit). Cette méthode
ne peut être employée que pour obtenir les valeurs des champs accessibles. Le mécanisme de sécurité de Java vous permet de connaître les champs d’un objet, mais pas de lire la valeur de ces champs
si vous n’avez pas une autorisation d’accès.
Livre Java .book Page 229 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
229
Par défaut, le mécanisme de réflexion respecte le contrôle des accès. Néanmoins, si un
programme Java n’est pas contrôlé par un gestionnaire de sécurité qui le lui interdit, il peut outrepasser son droit d’accès. Pour cela, il faut invoquer la méthode setAccessible d’un objet
Field, Method ou Constructor :
f.setAccessible(true);
// les appels f.get(harry) sont maintenant autorisés
La méthode setAccessible se trouve dans la classe AccessibleObject, qui est la superclasse
commune des classes Field, Method et Constructor. Cette fonctionnalité est destinée au débogage,
au stockage permanent et à des mécanismes similaires. Nous l’emploierons pour étudier une
méthode toString générique.
La méthode get pose un second problème. Le champ name est de type String, il est donc possible
de récupérer la valeur en tant que Object. Mais supposons que nous désirions étudier le champ
salary. Celui-ci est de type double, et les nombres ne sont pas des objets en Java. Il existe deux
solutions. La première consiste à utiliser la méthode getDouble de la classe Field ; la seconde est
un appel à get, car le mécanisme de réflexion enveloppe automatiquement la valeur du champ dans
la classe enveloppe appropriée, en l’occurrence, Double.
Bien entendu, il est possible de modifier les valeurs obtenues. L’appel f.set(obj, value) affecte
une nouvelle valeur au champ de l’objet obj représenté par f.
L’Exemple 5.6 montre comment écrire une méthode toString générique capable de fonctionner
avec n’importe quelle classe. Elle appelle d’abord getDeclaredField pour obtenir tous les champs
de données. Elle utilise ensuite la méthode setAccessible pour rendre tous ces champs accessibles,
puis elle récupère le nom et la valeur de chaque champ. L’exemple 5.6 convertit chaque valeur en
chaîne par un appel à sa propre méthode toString :
class ObjectAnalyzer
{
public String toString(Object obj)
{
Class cl = obj.getClass();
...
String r = cl.getName();
/* inspecter les champs de cette classe et de
toutes les superclasses */
do
{
r += "[";
Field[] fields = cl.getDeclaredFields();
AccessibleObject.setAccessible(fields, true);
// extraire les noms et valeurs de tous les champs
for (Field f : fields)
{
if (!Modifier.isStatic(f.getModifiers()))
{
if (!r.endsWith("[")) r += ","
r += f.getName() + "=";
try
{
Object val = f.get(obj);
r += val.toString(val);
}
Livre Java .book Page 230 Jeudi, 25. novembre 2004 3:04 15
230
Au cœur de Java 2 - Notions fondamentales
catch (Exception e) { e.printStackTrace(); }
}
}
r += "]";
cl = cl.getSuperclass();
}
while (cl != Object.class);
return r;
}
. . .
}
La totalité du code de l’Exemple 5.6 doit traiter deux complexités. Les cycles de référence pourraient entraîner une récursion infinie. ObjectAnalyzer suit donc la trace des objets qui ont déjà été
visités. De même, pour regarder dans les tableaux, vous devez adopter une approche différente. Vous
en saurez plus dans la section suivante.
Cette méthode toString peut être employée pour disséquer n’importe quel objet. Par exemple,
l’appel
ArrayList<Integer> squares = new ArrayList<Integer>();
for (int i = 1; i <= 5; i++) squares.add(i * i);
System.out.println(new ObjectAnalyzer().toString(squares));
produit l’impression
java.util.ArrayList[elementData=class
java.lang.Object[]{java.lang.Integer[value=1][][],
java.lang.Integer[value=4][][],
java.lang.Integer[value=9][][],java.lang.Integer[value=16][][],
java.lang.Integer[value=25][][],
null,null,null,null,null},size=5][modCount=5][][]
La méthode générique toString est utilisée dans l’Exemple 5.6 pour implémenter les méthodes
toString de vos propres classes, comme ceci :
public String toString()
{
return ObjectAnalyzer.toString(this);
}
Il s’agit d’une technique sûre pour créer une méthode toString, qui pourra vous être utile dans vos
programmes.
Exemple 5.6 : ObjectAnalyzerTest.java
import java.lang.reflect.*;
import java.util.*;
import java.text.*;
public class ObjectAnalyzerTest
{
public static void main(String[] args)
{
ArrayList<Integer> squares = new ArrayList<Integer>();
for (int i = 1; i <= 5; i++) squares.add(i * i);
System.out.println(new ObjectAnalyzer().toString(squares));
}
}
Livre Java .book Page 231 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
class ObjectAnalyzer
{
/**
Convertit un objet en une représentation chaîne
qui liste tous les champs.
@param obj Un objet
@return une chaîne avec le nom de classe de l’objet
et tous les noms et valeurs de champs
*/
public String toString(Object obj)
{
if (obj == null) return "null";
if (visited.contains(obj)) return "...";
visited.add(obj);
Class cl = obj.getClass();
if (cl == String.class) return (String) obj;
if (cl.isArray())
{
String r = cl.getComponentType() + "[]{";
for (int i = 0; i < Array.getLength(obj); i++)
{
if (i > 0) r += ",";
Object val = Array.get(obj, i);
if (cl.getComponentType().isPrimitive()) r += val;
else r += toString(val);
}
return r + "}";
}
String r = cl.getName();
/* inspecter les champs de cette classe et de
toutes les superclasses */
do
{
r += "[";
Field[] fields = cl.getDeclaredFields();
AccessibleObject.setAccessible(fields, true);
// extraire les noms et valeurs de tous les champs
for (Field f : fields)
{
if (!Modifier.isStatic(f.getModifiers()))
{
if (!r.endsWith("[")) r += ",";
r += f.getName() + "=";
try
{
Class t = f.getType();
Object val = f.get(obj);
if (t.isPrimitive()) r += val;
else r += toString(val);
}
catch (Exception e) { e.printStackTrace(); }
}
r += "]";
cl = cl.getSuperclass();
}
231
Livre Java .book Page 232 Jeudi, 25. novembre 2004 3:04 15
232
Au cœur de Java 2 - Notions fondamentales
while (cl != null);
return r;
}
private ArrayList<Object> visited = new ArrayList<Object>();
}
java.lang.reflect.AccessibleObject 1.2
•
void setAccessible(boolean flag)
Définit l’indicateur d’accessibilité de l’objet réflexion. La valeur true signifie que la vérification
d’accès de Java est invalidée et que les propriétés privées de l’objet peuvent être lues et affectées.
•
boolean isAccessible()
Récupère la valeur de l’indicateur d’accessibilité de cet objet réflexion.
•
static void setAccessible(AccessibleObject[] array,boolean flag)
Permet de spécifier l’état de l’indicateur d’accessibilité pour un tableau d’objets.
java.lang.Class 1.1
•
•
Field getField(String name)
Field[] getFields()
Récupère le champ public avec le nom donné ou un tableau de tous les champs.
•
•
Field getDeclaredField(String name)
Field[] getDeclaredFields()
Récupère le champ qui est déclaré dans cette classe avec le nom donné ou un tableau de tous les
champs.
java.lang.reflect.Field 1.1
•
Object get(Object obj)
Récupère la valeur du champ décrit par cet objet Field dans l’objet obj.
•
void set(Object obj, Object newValue)
Définit le champ décrit par cet objet Field dans l’objet obj avec une nouvelle valeur.
La réflexion pour créer un tableau générique
La classe Array du package java.lang.reflect permet de créer des tableaux dynamiques.
Lorsqu’on utilise cette caractéristique avec la méthode arrayCopy, décrite au Chapitre 3, il est
possible d’étendre dynamiquement la taille d’un tableau existant tout en en préservant le contenu
actuel.
Le problème que nous souhaitons résoudre est assez typique. Supposons que nous disposions d’un
tableau — d’un type quelconque — qui est saturé et que nous voulions l’agrandir. Comme nous
sommes fatigués d’écrire systématiquement le code traditionnel ("augmenter le bloc mémoire et
recopier"), nous décidons d’écrire une méthode générique permettant d’agrandir un tableau :
Employee[] a = new Employee[100];
. . .
// le tableau est saturé
a = (Employee[])arrayGrow(a);
Livre Java .book Page 233 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
233
Comment écrire cette méthode générique ? Heureusement, un Tableau Employee[] peut être
converti en tableau Object[]. Cela s’annonce bien. Voici une première mouture de notre méthode
générique. Nous augmentons le tableau de 10 % + 10 éléments (car une simple augmentation de
10 % ne suffirait pas pour de petits tableaux) :
static Object[] badArrayGrow(Object[] a) // sans intérêt
{
int newLength = a.length * 11 / 10 + 10;
Object[] newArray = new Object[newLength];
System.arraycopy(a, 0, newArray, 0, a.length);
return newArray;
}
La question se pose néanmoins de l’utilisation du tableau qui en résulte. Le type de tableau renvoyé
par ce code est un tableau d’objets (Object[]), il est en fait créé par cette instruction :
new Object[newLength]
Un tableau d’objets ne peut pas être transtypé en tableau d’employés (Employee[]). Java déclencherait une exception de transtypage ClassCastException à l’exécution. Comme nous l’avons déjà
expliqué, un tableau Java mémorise le type de ses éléments, c’est-à-dire le type d’élément utilisé
dans l’expression new lors de la création du tableau. Il est valide de transtyper temporairement un
Tableau Employee[] en tableau Object[], et de le retranstyper ensuite dans son type d’origine.
Mais un tableau qui a été initialement créé en tant que tableau Object[] ne peut jamais être transtypé en Tableau Employee[]. Pour développer ce genre de tableau générique, nous devons être capables de créer un nouveau tableau ayant le même type que le tableau original. Pour cela, il nous faut
exploiter les méthodes de la classe Array du package java.lang.reflect. La clé de l’opération est
la méthode newInstance de la classe Array, qui construit un nouveau tableau. Il faut indiquer à
cette méthode, en paramètres, le type des éléments et la nouvelle taille désirée :
Object newArray = Array.newInstance(componentType, newLength);
Pour parvenir à nos fins, il nous faut connaître la taille (ou longueur) et le type du nouveau tableau.
Cette taille est obtenue par un appel à Array.getLength(a). La méthode statique getLength
renvoie la taille de n’importe quel tableau. Pour obtenir le type, il faut suivre ces étapes :
1. Obtenir d’abord l’objet classe de a.
2. Confirmer qu’il s’agit bien d’un tableau.
3. Utiliser ensuite la méthode getComponentType de la classe Class (qui n’est définie que pour les
objets classe représentant des tableaux) afin de connaître le véritable type du tableau.
On peut se demander pourquoi getLength est une méthode de Array et getComponentType une
méthode de Class. Ce choix a sans doute paru approprié aux concepteurs.
Voici la nouvelle version de notre méthode :
static Object goodArrayGrow(Object a) // utile
{
Class cl = a.getClass();
if (!cl.isArray()) return null;
Class componentType = cl.getComponentType();
int length = Array.getLength(a);
int newLength = length * 11 / 10 + 10;
Object newArray = Array.newInstance(componentType,
newLength);
Livre Java .book Page 234 Jeudi, 25. novembre 2004 3:04 15
234
Au cœur de Java 2 - Notions fondamentales
System.arraycopy(a, 0, newArray, 0, length);
return newArray;
}
Remarquez que notre méthode arrayGrow peut être employée pour augmenter des tableaux de tout
type, et pas seulement des tableaux d’objets :
int[] a = { 1, 2, 3, 4 };
a = (int[]) goodArrayGrow(a);
Pour permettre cela, le paramètre de goodArrayGrow est déclaré de type Object, et non comme un
tableau d’objets (Object[]). Un tableau de type entier int[] peut être converti en Object, mais un
tableau d’objets ne le peut pas !
L’Exemple 5.7 illustre le fonctionnement des deux méthodes d’augmentation de tableau. Notez que
la conversion de type de la valeur renvoyée par badArrayGrow va provoquer une exception.
Exemple 5.7 : Arraygrowtest.java
import java.lang.reflect.*;
import java.util.*;
public class ArrayGrowTest
{
public static void main(String[] args)
{
int[] a = { 1, 2, 3 };
a = (int[]) goodArrayGrow(a);
arrayPrint(a);
String[] b = { "Tom", "Dick", "Harry" };
b = (String[]) goodArrayGrow(b);
arrayPrint(b);
System.out.println
("The following call will generate an exception.");
b = (String[]) badArrayGrow(b);
}
/**
Cette méthode tente d’agrandir un tableau en allouant
un nouveau tableau et en copiant tous les éléments.
@param a Le tableau à agrandir
@return Un tableau plus grand contenant tous les éléments de a.
Toutefois, le tableau renvoyé est du type Object[],
et non du même type que a
*/
static Object[] badArrayGrow(Object[] a)
{
int newLength = a.length * 11 / 10 + 10;
Object[] newArray = new Object[newLength];
System.arraycopy(a, 0, newArray, 0, a.length);
return newArray;
}
/**
Cette méthode agrandit un tableau en allouant
un nouveau tableau et en copiant tous les éléments.
@param a Le tableau à agrandir. Peut être un tableau object
Livre Java .book Page 235 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
235
ou un tableau du type fondamental
@return Un tableau plus grand contenant tous les éléments de a.
*/
static Object goodArrayGrow(Object a)
{
Class cl = a.getClass();
if (!cl.isArray()) return null;
Class componentType = cl.getComponentType();
int length = Array.getLength(a);
int newLength = length * 11 / 10 + 10;
Object newArray = Array.newInstance(componentType,
newLength);
System.arraycopy(a, 0, newArray, 0, length);
return newArray;
}
/**
Une méthode permettant d’afficher tous les éléments dans un tableau
@param a Le tableau à afficher. Peut être un tableau object
ou un tableau du type fondamental
*/
static void arrayPrint(Object a)
{
Class cl = a.getClass();
if (!cl.isArray()) return;
Class componentType = cl.getComponentType();
int length = Array.getLength(a);
System.out.print(componentType.getName()
+ "[" + length + "] = { ");
for (int i = 0; i < Array.getLength(a); i++)
System.out.print(Array.get(a, i) + " ");
System.out.println("}");
}
}
java.lang.reflect.Array 1.1
•
•
static Object get(Object array, int index)
static xxx getXxx(Object array, int index)
(xxx est l’un des types primitifs boolean, byte, char, double, float, int, long, short).
Ces méthodes renvoient la valeur du tableau donné, stockée à l’index donné.
•
•
static void set(Object array, int index, Object newValue)
static setXxx(Object array, int index, xxx newValue)
(xxx est l’un des types primitifs boolean, byte, char, double, float, int, long, short).
Ces méthodes stockent une nouvelle valeur dans le tableau donné, à l’index donné.
•
static int getLength(Object array)
Renvoie la longueur du tableau donné.
•
•
static Object newInstance(Class componentType, int length)
static Object newInstance(Class componentType, int[] lengths)
Renvoie un nouveau tableau du type de composant donné avec les dimensions données.
Livre Java .book Page 236 Jeudi, 25. novembre 2004 3:04 15
236
Au cœur de Java 2 - Notions fondamentales
Les pointeurs de méthodes
A première vue, Java ne possède pas de pointeurs de méthodes — ils permettent de fournir l’adresse
d’une méthode à une autre méthode, afin que la seconde puisse appeler la première. En fait, les
concepteurs du langage ont précisé que les pointeurs de méthodes étaient dangereux et que les interfaces Java (dont nous parlerons au prochain chapitre) constituaient une meilleure solution. En
réalité, depuis le JDK 1.1, Java dispose de pointeurs de méthodes, qui sont une conséquence (peutêtre accidentelle) du développement du package de réflexion.
INFO
Parmi les extensions non standard du langage que Microsoft a ajouté à son dérivé de Java, J++ (et son successeur, C#),
on trouve un autre type de pointeur de méthode, appelé délégué, qui est différent de la classe Method que nous
avons vue dans cette section. Cependant, les classes internes (que nous verrons au chapitre suivant) constituent une
construction plus utile que les délégués.
Pour voir à l’œuvre les pointeurs de méthode, rappelez-vous que vous pouvez inspecter un champ
d’un objet à l’aide de la méthode get de la classe Field. Pour sa part, la classe Method dispose d’une
méthode invoke permettant d’appeler la méthode enveloppée dans l’objet Method. La signature de
la méthode invoke est la suivante :
Object invoke(Object obj, Object... args)
Le premier paramètre est implicite et les autres objets fournissent les paramètres explicites. Avant la
version JDK 5.0, vous deviez transmettre un tableau d’objets ou null si la méthode ne disposait pas
de paramètres explicites.
Dans le cas d’une méthode statique, le premier paramètre est ignoré — il peut recevoir la valeur null.
Par exemple, si m1 représente la méthode getName de la classe Employee, le code suivant vous
montre comment l’appeler :
String n = (String) m1.invoke(harry);
A l’image des méthodes get et set du champ Field, un problème se pose si le paramètre ou le
type de résultat est non pas une classe, mais un type primitif. Il faut soit se reposer sur l’autoboxing soit, avant le JDK 5.0, envelopper tout type primitif dans sa classe enveloppe correspondante.
Inversement, si le type de retour est un type primitif, la méthode invoke renverra plutôt le type enveloppe. Supposons que m2 représente la méthode getSalary de la classe Employee. L’objet renvoyé
est alors un objet Double, vous devez donc le transtyper en conséquence. Depuis le JDK 5.0,
l’unboxing automatique gère les autres opérations :
double s = (Double) m2.invoke(harry);
Comment obtenir un objet Method ? Il est bien évidemment possible d’appeler getDeclaredMethods, qui renvoie un tableau d’objets Method dans lequel on recherchera la méthode désirée. On
peut également appeler la méthode getMethod de la classe Class. Elle est comparable à getField,
qui reçoit une chaîne de caractères représentant un nom de champ et renvoie un objet Field. Cependant, puisqu’il peut exister plusieurs méthodes homonymes, il faut être certain d’obtenir la bonne.
Livre Java .book Page 237 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
237
C’est la raison pour laquelle il faut également fournir les types de paramètres de la méthode désirée.
La signature de getMethod est :
Method getMethod(String name, Class... parameterTypes)
Voici, par exemple, la manière d’obtenir des pointeurs sur les méthodes getName et raiseSalary de
la classe Employee :
Method m1 = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary", double.class);
Avant le JDK 5.0, vous deviez emballer les objets Class dans un tableau ou fournir null s’il n’y
avait pas de paramètres).
Maintenant que vous connaissez les règles d’utilisation des objets Method, nous allons les mettre en
application. Le programme de l’Exemple 5.8 affiche une table de valeurs pour des fonctions mathématiques comme Math.sqrt ou Math.sin. L’affichage ressemble à ceci :
public static native double java.lang.Math.sqrt(double)
1.0000 |
1.0000
2.0000 |
1.4142
3.0000 |
1.7321
4.0000 |
2.0000
5.0000 |
2.2361
6.0000 |
2.4495
7.0000 |
2.6458
8.0000 |
2.8284
9.0000 |
3.0000
10.0000 |
3.1623
Le code d’affichage d’une table est bien sûr indépendant de la fonction réelle qui s’affiche sous cette
forme :
double dx = (to - from) / (n - 1);
for (double x = from; x <= to; x += dx)
{
double y = (Double) f.invoke(null,x);
System.out.printf("%10.4f | %10.4f%n" + y, x, y);
}
Ici, f est un objet de type Method. Le premier paramètre de invoke est null, car nous appelons une
méthode statique.
Pour tabuler la fonction Math.sqrt, nous définissons f sur :
Math.class.getMethod("sqrt", double.class)
C’est la méthode de la classe Math dont le nom est sqrt et qui a un seul paramètre de type double.
L’Exemple 5.8 présente le code complet du tabulateur générique et quelques tests.
Exemple 5.8 : MethodPointerTest.java
import java.lang.reflect.*;
public class MethodPointerTest
{
public static void main(String[] args) throws Exception
{
/* obtenir les pointeurs sur
les méthodes square et sqrt */
Method square = MethodPointerTest.class.getMethod("square",
Livre Java .book Page 238 Jeudi, 25. novembre 2004 3:04 15
238
Au cœur de Java 2 - Notions fondamentales
double.class);
Method sqrt = Math.class.getMethod("sqrt", double.class);
// afficher les tables de valeurs x- et yprintTable(1, 10, 10, square);
printTable(1, 10, 10, sqrt);
}
/**
Renvoie le carré d’un nombre
@param x Un nombre
@return x au carré
*/
public static double square(double x)
{
return x * x;
}
/**
Affiche une table avec des valeurs x et y pour une méthode
@param from La limite inférieure des valeurs x
@param to La limite supérieure des valeurs x
@param n Le nombre de rangées dans la table
@param f Une méthode avec un paramètre double et
une valeur renvoyée double
*/
public static void printTable(double from, double to,
int n, Method f)
{
// Affiche la méthode comme en-tête de la table
System.out.println(f);
/* construit le formateur pour affichage
avec une précision de 4 chiffres */
double dx = (to - from) / (n - 1);
for (double x = from; x <= to; x += dx)
{
try
{
double y = (Double) f.invoke(null, x);
System.out.printf("%10.4f | %10.4f%n", x, y);
}
catch (Exception e) { e.printStackTrace(); }
}
}
}
Cet exemple est clair : on peut accomplir avec les objets Method tout ce qui est réalisable avec les
pointeurs de fonction du C (ou les délégués du C#). A cette différence près, toutefois : en langage C,
ce style de programmation est généralement considéré comme incongru et dangereux. Que se passet-il ici lorsque vous appelez une méthode en lui passant un paramètre incorrect ? La méthode invoke
déclenche une exception.
Livre Java .book Page 239 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
239
Les paramètres et le résultat de la méthode invoke sont nécessairement de type Object. Cela signifie que l’on doit effectuer un bon nombre de transtypages. En conséquence, le compilateur n’a pas
l’occasion de vérifier votre code, et les erreurs n’apparaissent que durant les tests, lorsqu’elles sont
plus difficiles à corriger. De plus, une routine qui exploite la réflexion pour obtenir un pointeur de
méthode est sensiblement plus lente qu’un code appel qui appelle les méthodes directement.
Nous vous recommandons de n’employer les objets Method dans vos programmes qu’en cas d’absolue nécessité. L’utilisation des interfaces et des classes internes est de loin préférable. Tout comme
les concepteurs de Java, nous vous conseillons de ne pas employer les objets Method pour les fonctions de rappel (callback). L’usage des interfaces pour les fonctions de rappel produit un code plus
rapide et plus facile à mettre à jour (nous reviendrons sur tous ces aspects au prochain chapitre).
java.lang.reflect.Method 1.1
•
public Object invoke(Object implicitParameter, Object[] explicitParameters)
Appelle la méthode décrite par cet objet, en transmettant les paramètres donnés et en renvoyant
la valeur que la méthode renvoie. Pour les méthodes statiques, transférez null comme paramètre
implicite. Transférez les valeurs des types primitifs en utilisant les enveloppes. Un type primitif
renvoie des valeurs qui ne doivent pas être enveloppées.
Classes d’énumération
Vous avez vu au Chapitre 3 comment définir les types énumérés dans le JDK 5.0. Voici un exemple
typique :
public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE };
Le type défini par cette déclaration est en fait une classe. La classe possède exactement quatre
instances ; il n’est pas possible de construire de nouveaux objets.
Vous n’avez donc jamais besoin d’utiliser equals pour les valeurs de types énumérés. Utilisez
simplement == pour les comparer.
Vous pouvez, si vous le souhaitez, ajouter des constructeurs, des méthodes et des champs à un type
énuméré. Bien entendu, les constructeurs ne sont invoqués que lorsque les constantes énumérées
sont construites. En voici un exemple :
enum Size
{
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
private Size(String abbreviation)
{ this.abbreviation = abbreviation; }
public String getAbbreviation() { return abbreviation; }
private String abbreviation;
}
Tous les types énumérés sont des sous-classes de la classe Enum. Ils héritent de plusieurs méthodes
de cette classe. La méthode la plus utile est toString, qui renvoie le nom de la constante énumérée.
Par exemple, Size.SMALL.toString() renvoie la chaîne "SMALL".
Livre Java .book Page 240 Jeudi, 25. novembre 2004 3:04 15
240
Au cœur de Java 2 - Notions fondamentales
L’inverse de toString est la méthode statique valueOf. Par exemple, l’instruction
Size s = (Size) Enum.valueOf(Size.class, "SMALL");
définit s sur Size.SMALL.
Chaque type énuméré possède une méthode de valeurs statique qui renvoie un tableau de toutes les
valeurs de l’énumération :
Size[] values = Size.values();
Le petit programme de l’Exemple 5.9 montre comment fonctionnent les types énumérés.
INFO
Tout comme Class, la classe Enum dispose d’un paramètre de type que nous avons ignoré pour des raisons de simplicité. Par exemple, le type énuméré Size étend en fait Enum<Size>.
Exemple 5.9 : EnumTest.java
import java.util.*;
public class EnumTest
{
public static void main(String[] args)
{
Scanner in = new Scanner(System.in);
System.out.print("Enter a size:
(SMALL, MEDIUM, LARGE, EXTRA_LARGE) ");
String input = in.next().toUpperCase();
Size size = Enum.valueOf(Size.class, input);
System.out.println("size=" + size);
System.out.println("abbreviation=" + size.getAbbreviation());
if (size == Size.EXTRA_LARGE)
System.out.println("Good job--you paid attention to the _.");
}
}
enum Size
{
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
private Size(String abbreviation)
{ this.abbreviation = abbreviation; }
public String getAbbreviation() { return abbreviation; }
private String abbreviation;
}
java.lang.Enum 5.0
•
static Enum valueOf(Class enumClass, String name)
Renvoie la constante énumérée de la classe donnée, avec le nom donné.
•
String toString()
Renvoie le nom de cette constante énumérée.
Livre Java .book Page 241 Jeudi, 25. novembre 2004 3:04 15
Chapitre 5
L’héritage
241
Conseils pour l’utilisation de l’héritage
Nous terminerons ce chapitre par quelques conseils que nous considérons utiles sur l’utilisation de
l’héritage.
1. Placez les opérations communes et les champs communs dans la superclasse.
C’est la raison pour laquelle nous avons placé le champ name dans la classe Person au lieu de le
dupliquer dans Employee et Student.
2. N’utilisez pas de champs protégés.
Certains programmeurs pensent que le fait de définir la plupart des champs d’instance comme
protected, "au cas où", est une bonne idée, afin que les sous-classes puissent accéder à ces
champs en cas de besoin. Pourtant, le mécanisme protected n’apporte pas une grande protection,
pour deux raisons. Tout d’abord, le jeu des sous-classes est illimité — n’importe qui peut former
une sous-classe à partir de vos classes, puis écrire un code permettant d’accéder directement aux
champs d’instance protégés, rompant ainsi l’encapsulation. Et, dans Java, toutes les classes dans le
même package ont accès aux champs protégés, qu’il s’agisse ou non de sous-classes.
Les méthodes protected peuvent toutefois être utiles pour indiquer celles qui ne sont pas
prévues pour un usage général et doivent être redéfinies dans les sous-classes. La méthode clone
en est un bon exemple.
3. Utilisez l’héritage pour déterminer la relation d’appartenance ("est").
L’héritage permet de réduire le code. Il est parfois employé de manière abusive. Supposons que
nous ayons besoin d’une classe Contractor. Les contractants ont un nom et une date d’embauche, mais ils ne perçoivent pas de salaire. En revanche, ils sont payés à l’heure et ne restent pas
assez longtemps pour obtenir une augmentation. La tentation est grande de former une sousclasse Contractor de la classe Employee et d’y ajouter un champ hourlyWage (tarif horaire).
class Contractor extends Employee
{ . . .
private double hourlyWage;
}
Ce n’est pourtant pas une bonne idée, car désormais chaque objet contractant va posséder à la
fois un champ salaire et un champ tarif horaire. Cela vous posera de nombreux problèmes lorsque vous implémenterez des méthodes pour l’affichage des fiches de paie ou des formulaires
d’impôt. Finalement, vous écrirez plus de code que si vous n’aviez pas utilisé l’héritage.
La relation contractant/employé ne passe pas le test d’appartenance ("est"). Un contractant n’est
pas un type particulier d’employé.
4. N’employez pas l’héritage à moins que toutes les méthodes héritées ne soient valables.
Supposons que vous désiriez écrire une classe Holiday. Chaque jour de congé est un jour et les
jours peuvent être exprimés comme des instances de la classe GregorianCalendar, nous
pouvons donc employer l’héritage.
class Holiday extends GregorianCalendar { . . . }
Malheureusement, le jeu des congés n’est pas fermé sous les opérations héritées. L’une des méthodes
publiques de GregorianCalendar est add. Et add peut transformer un congé en non-congé :
Holiday christmas;
christmas.add(Calendar.DAY_OF_MONTH, 12);
Livre Java .book Page 242 Jeudi, 25. novembre 2004 3:04 15
242
Au cœur de Java 2 - Notions fondamentales
L’héritage n’est donc pas indiqué dans ce cas.
5. Ne modifiez pas le comportement attendu lorsque vous remplacez une méthode.
Le principe de substitution s’applique non seulement à la syntaxe mais surtout au comportement.
Lorsque vous remplacez une méthode, vous ne devez pas changer son comportement sans raison
valable. Le compilateur ne peut pas vous aider, il ne peut pas vérifier si vos nouvelles définitions
sont adaptées. Vous pouvez "réparer" le problème de la méthode add dans la classe Holiday en
redéfinissant add, par exemple pour qu’il n’ait aucun rôle, pour déclencher une exception ou
pour passer au prochain holiday.
Toutefois, cette réparation viole le principe de substitution. La suite d’instructions
int d1 = x.get(Calendar.DAY_OF_MONTH);
x.add(Calendar.DAY_OF_MONTH, 1);
int d2 = x.get(Calendar.DAY_OF_MONTH);
System.out.println(d2 - d1);
devrait avoir le comportement attendu, que x soit ou non du type GregorianCalendar ou Holiday.
Bien entendu, c’est là qu’est le problème. Les gens raisonnables ou non peuvent argumenter à loisir
sur le comportement attendu. Certains auteurs avancent, par exemple, que le principe de substitution
exige que Manager.equals ignore le champ de bonus car Employee.equals l’ignore. Ces discussions sont toujours inutiles si elles n’ont pas d’objectif concret. Au final, ce qui importe, c’est que
vous ne détourniez pas l’intention du premier concepteur lorsque vous remplacez des méthodes dans
les sous-classes.
6. Utilisez le polymorphisme plutôt que l’information de type.
Chaque fois que vous rencontrez du code ayant cette forme :
if (x est de type 1)
action1(x);
else if (x est de type 2)
action2(x);
pensez au polymorphisme.
Est-ce que action1 et action2 représentent un concept commun ? Si oui, faites-en une méthode
d’une superclasse ou une interface commune aux deux types. Vous pourrez alors appeler simplement :
x.action();
et exécuter l’action appropriée grâce au mécanisme de répartition dynamique inhérent au polymorphisme.
Le code qui emploie les interfaces ou les méthodes polymorphes est plus facile à mettre à jour et
à améliorer que le code qui utilise des tests multiples.
7. N’abusez pas de la réflexion.
Le mécanisme de réflexion vous permet d’écrire des programmes étonnamment généralistes, en
détectant les champs et les méthodes au moment de l’exécution. Cette capacité peut être extrêmement utile pour la programmation système, mais elle n’est généralement pas appropriée pour
les applications. La réflexion est fragile — le compilateur ne peut pas vous aider à détecter les
erreurs de programmation. Toutes les erreurs sont trouvées au moment de l’exécution et déclenchent
des exceptions.
Livre Java .book Page 243 Jeudi, 25. novembre 2004 3:04 15
6
Interfaces et classes internes
Au sommaire de ce chapitre
✔ Interfaces
✔ Clonage d’objets
✔ Interfaces et callbacks
✔ Classes internes
✔ Proxies
Vous connaissez maintenant tous les éléments de base qui concernent la programmation orientée
objet en Java. Ce chapitre présentera plusieurs techniques avancées, qui sont très couramment utilisées. Malgré leur nature peu évidente, il vous faudra les maîtriser pour compléter votre panoplie
d’outils Java.
La première est appelée interface, une façon de décrire ce que les classes doivent faire, sans préciser
comment elles doivent le faire. Une classe peut implémenter une ou plusieurs interfaces. Vous
pouvez ensuite utiliser les objets de ces classes "implémentantes" chaque fois que la conformité avec
l’interface est nécessaire. Après les interfaces, nous verrons le clonage (ou copie intégrale) d’un
objet. Le clone d’un objet est un nouvel objet qui a le même état que l’original. En particulier, vous
pouvez modifier le clone sans affecter l’original. Nous étudierons ensuite le mécanisme des classes
internes. Les classes internes sont techniquement quelque peu complexes — elles sont définies à
l’intérieur d’autres classes et leurs méthodes peuvent accéder aux champs de la classe qui les
contient. Ces classes sont utiles pour la conception de collections de classes devant coopérer. Elles
permettent d’écrire du code concis, de qualité "professionnelle", qui permet de gérer les événements
de l’interface utilisateur graphique.
Ce chapitre conclura avec une discussion sur les proxies, objets qui implémentent des interfaces
arbitraires. Un proxy est une construction très spécialisée, utile pour la création d’outils système.
Vous pouvez sauter cette section dans l’immédiat.
Livre Java .book Page 244 Jeudi, 25. novembre 2004 3:04 15
244
Au cœur de Java 2 - Notions fondamentales
Interfaces
Dans le langage Java, une interface n’est pas une classe, mais un jeu de conditions pour les classes
qui veulent se conformer à l’interface.
Généralement, le fournisseur d’un service annonce : "Si votre classe se conforme à une interface
particulière, je vais fournir le service". Examinons un exemple concret. La méthode sort de la classe
Arrays promet de trier un tableau d’objets, mais à une condition : les objets doivent appartenir à des
classes qui implémentent l’interface Comparable.
Voici à quoi ressemble l’interface Comparable :
public interface Comparable
{
int compareTo(Object other);
}
Cela signifie que toute classe qui implémente l’interface Comparable doit posséder une méthode
compareTo, et la méthode doit accepter un paramètre Object et renvoyer un entier.
INFO
Depuis le JDK 5.0, l’interface Comparable a été améliorée pour devenir un type générique :
public interface Comparable<T>
{
int compareTo(T other); // le paramètre a le type T
}
Par exemple, une classe qui implémente Comparable<Employee> doit fournir une méthode :
int compareTo(Employee other)
Vous pouvez toujours utiliser le type "brut" Comparable sans paramètre de type, mais vous devrez alors transtyper
manuellement le paramètre de la méthode compareTo sur le type souhaité.
Toutes les méthodes d’une interface sont automatiquement public. C’est la raison pour laquelle il n’est
pas nécessaire de fournir le mot clé public lorsque vous déclarez une méthode dans une interface.
Il existe une condition supplémentaire : lors de l’appel à x.compareTo(y), la méthode compareTo
doit en réalité être capable de comparer deux objets et d’indiquer lequel entre x ou y est le plus
grand. La méthode doit renvoyer un nombre négatif si x est plus petit que y, zéro s’ils sont égaux, et
un nombre positif dans les autres cas.
Cette interface particulière a une seule méthode. Certaines interfaces ont plus d’une méthode.
Comme vous le verrez plus tard, les interfaces peuvent aussi définir des constantes. Ce qui importe,
en fait, est ce que les interfaces ne peuvent pas fournir. Les interfaces n’ont jamais de champs
d’instance, et les méthodes ne sont jamais implémentées dans l’interface. La fourniture des
champs d’instance et des implémentations de méthode est prise en charge par les classes qui implémentent l’interface. Vous pouvez imaginer une interface comme étant semblable à une classe
abstraite sans champs d’instance. Il y a toutefois certaines différences entre ces deux concepts —
nous les étudierons plus en détail ultérieurement.
Livre Java .book Page 245 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
245
Supposons maintenant que nous voulions utiliser la méthode sort de la classe Arrays pour trier un
tableau d’objets Employee. La classe Employee doit donc implémenter l’interface Comparable.
Pour qu’une classe implémente une interface, vous devez exécuter deux étapes :
m
déclarer que votre classe a l’intention d’implémenter l’interface donnée ;
m
fournir les définitions pour toutes les méthodes dans l’interface.
Pour déclarer qu’une classe implémente une interface, employez le mot clé implements :
class Employee implements Comparable
Bien entendu, la classe Employee doit maintenant fournir la méthode compareTo. Supposons que
nous voulions comparer le salaire des employés. Voici une méthode compareTo qui renvoie −1 si le
salaire du premier employé est inférieur à celui du second, 0 s’ils sont égaux, et 1 dans les autres
cas :
public int compareTo(Object otherObject)
{
Employee other = (Employee) otherObject;
if (salary < other.salary) return -1;
if (salary > other.salary) return 1;
return 0;
}
ATTENTION
Dans la déclaration de l’interface, la méthode compareTo n’a pas été déclarée public, car toutes les méthodes dans
une interface sont automatiquement publiques. Cependant, lors de l’implémentation de l’interface, vous devez
déclarer la méthode comme public. Sinon le compilateur suppose que la visibilité de la méthode se situe au niveau
du package — ce qui est la valeur par défaut pour une classe. Le compilateur proteste alors, car vous essayez de fournir un privilège d’accès plus faible.
Avec le JDK 5.0, nous pouvons faire un peu mieux. Nous déciderons d’implémenter plutôt le type
d’interface Comparable<Employee> :
class Employee implements Comparable<Employee>
{
public int compareTo(Employee other)
{
if (salary < other.salary) return -1;
if (salary > other.salary) return 1;
return 0;
}
. . .
}
Sachez que le transtypage disgracieux du paramètre Object a disparu.
ASTUCE
La méthode compareTo de l’interface Comparable renvoie un entier. Si les objets ne sont pas égaux, la valeur négative ou positive renvoyée n’a aucune importance. Cette largesse peut être utile si vous comparez des champs entiers.
Supposons, par exemple, que chaque employé ait un unique identificateur entier id, et que vous vouliez trier les
employés par numéro d’ID. Vous pouvez simplement renvoyer id - other.id. Cette valeur sera négative si le
Livre Java .book Page 246 Jeudi, 25. novembre 2004 3:04 15
246
Au cœur de Java 2 - Notions fondamentales
premier ID est inférieur au second, égale à 0 s’ils sont identiques et positive dans les autres cas. L’indication est suffisante ; la plage des entiers doit être suffisamment petite pour qu’il ne se produise pas un dépassement lors de la
soustraction. Si vous savez que les ID ont une valeur non négative ou une valeur absolue qui est au maximum
(Integer.MAX_VALUE - 1) / 2, vous ne risquez rien.
Cette astuce de soustraction n’est évidemment pas valable pour les nombres à virgule flottante. La différence
salary - other.salary peut alors être arrondie à 0 si les salaires sont suffisamment proches bien que non
identiques.
Vous avez vu ce qu’une classe doit faire pour tirer parti du service de tri — elle doit simplement
implémenter une méthode compareTo. C’est tout à fait raisonnable. Il doit y avoir un moyen pour
que la méthode sort puisse comparer les objets. Mais pourquoi la classe Employee ne peut-elle tout
simplement fournir une méthode compareTo sans implémenter l’interface Comparable ?
Les interfaces sont nécessaires, car le langage Java est fortement typé. Lors d’un appel de méthode, le
compilateur doit pouvoir vérifier que la méthode existe réellement. Quelque part dans la méthode
sort, on trouvera des instructions comme celles-ci :
if (a[i].compareTo(a[j]) > 0)
{
// réorganiser a[i] et a[j]
. . .
}
Le compilateur doit savoir que a[i] possède réellement une méthode compareTo. Si a est un tableau
d’objets Comparable, l’existence de la méthode est confirmée, car chaque classe qui implémente
l’interface Comparable doit fournir la méthode.
INFO
Il serait logique que la méthode sort dans la classe Arrays soit définie pour accepter un Tableau Comparable[],
afin que le compilateur puisse protester si quelqu’un appelle sort avec un tableau dont le type d’élément n’implémente pas l’interface Comparable. Malheureusement, il n’en est rien. La méthode sort accepte un tableau
Object[] et a recours à un transtypage bizarre :
// dans la bibliothèque standard -- Non recommandé
if (((Comparable) a[i]).compareTo(a[j]) > 0)
{
// réorganiser a[i] et a[j]
. . .
}
Si a[i] n’appartient pas à une classe qui implémente l’interface Comparable, la machine virtuelle déclenche une
exception.
L’Exemple 6.1 décrit la totalité du code nécessaire pour le tri d’un tableau d’employés.
Exemple 6.1 : EmployeeSortTest.java
import java.util.*;
public class EmployeeSortTest
{
public static void main(String[] args)
Livre Java .book Page 247 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
{
Employee[] staff = new Employee[3];
staff[0] = new Employee("Harry Hacker", 35000);
staff[1] = new Employee("Carl Cracker", 75000);
staff[2] = new Employee("Tony Tester", 38000);
Arrays.sort(staff);
// afficher les informations pour tous les objets Employee
for (Employee e : staff)
System.out.println("name=" + e.getName()
+ ",salary=" + e.getSalary());
}
}
class Employee implements Comparable<Employee>
{
public Employee(String n, double s)
{
name = n;
salary = s;
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
/**
Compare les salaires des employés
@param other Un autre objet Employee
@return une valeur négative si cet employé
a un salaire inférieur à celui de otherObject,
0 si les salaires sont identiques,
une valeur positive dans les autres cas
*/
public int compareTo(Employee other)
{
if (salary < other.salary) return -1;
if (salary > other.salary) return 1;
return 0;
}
private String name;
private double salary;
}
247
Livre Java .book Page 248 Jeudi, 25. novembre 2004 3:04 15
248
Au cœur de Java 2 - Notions fondamentales
java.lang.Comparable 1.0
•
int compareTo(Object otherObject)
Compare cet objet à l’objet otherObject et renvoie zéro s’ils sont identiques, un entier négatif
s’il est inférieur à otherObject ou un entier positif s’il est supérieur.
java.lang.Comparable<T> 5.0
•
int compareTo(T other)
Compare cet objet à d’autres et renvoie un entier négatif si cet objet est inférieur à l’autre, 0 s’ils
sont égaux et un entier positif dans les autres cas.
java.util.Arrays 1.2
•
static void sort(Object[] a)
Trie les éléments du tableau a, à l’aide d’un algorithme mergesort personnalisé. Tous les
éléments du tableau doivent appartenir à des classes qui implémentent l’interface Comparable ;
ils doivent aussi tous être comparables entre eux.
INFO
Selon les standards du langage : "L’implémenteur doit s’assurer que sgn(x.compareTo(y)) = –?sgn(y.compareTo(x)) pour toutes les occurrences de x et y (cela implique que x.compareTo(y) doit déclencher une exception
si y.compareTo(x) déclenche une exception)." Ici, "sgn" est le signe d’un nombre : sgn(n) vaut –1 si n est négatif,
0 si n égale 0, et 1 si n est positif. En bon français, si vous inversez les paramètres de compareTo, le signe (mais pas
nécessairement la valeur réelle) du résultat doit aussi être inversé.
Comme avec la méthode equals, des problèmes peuvent survenir lorsque l’on fait appel à l’héritage.
Etant donné que Manager étend Employee, il implémente Comparable<Employee> et non pas Comparable<Manager>. Si Manager choisit de remplacer compareTo, il doit être préparé à comparer les directeurs aux
employés. Il ne peut pas simplement transtyper l’employé en directeur :
class Manager extends Employee
{
public int compareTo(Employee other)
{
Manager otherManager = (Manager) other; // NON
. . .
}
. . .
}
La règle d’"antisymétrie" est violée. Si x est un Employee et y un Manager, l’appel x.compareTo(y) ne déclenche
pas d’exception — il compare simplement x et y en tant qu’employés. Mais l’inverse, y.compareTo(x) déclenche
une exception ClassCastException.
C’est la même situation qu’avec la méthode equals vue au Chapitre 5 ; la solution est la même. Il existe deux scénarios
distincts.
Si les sous-classes ont des notions différentes de la comparaison, vous devez interdire la comparaison des objets
appartenant à des classes différentes. Chaque méthode compareTo devrait commencer par le test
if (getClass() != other.getClass()) throw new ClassCastException();
Livre Java .book Page 249 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
249
S’il existe un algorithme commun pour comparer les objets de sous-classe, fournissez simplement une seule méthode
compareTo dans la superclasse et déclarez-la sous la forme final.
Supposons par exemple que vous vouliez que les directeurs aient une meilleure situation que les employés ordinaires, en dehors du salaire. Qu’adviendra-t-il des autres sous-classes comme Executive et Secretary ? Si vous devez
établir un ordre hiérarchique, fournissez une méthode dans la classe Employee (par exemple rank). Amenez
chaque sous-classe à remplacer rank et implémentez une seule méthode compareTo qui prendra en compte les
valeurs du rang.
Propriétés des interfaces
Les interfaces ne sont pas des classes. En particulier, vous ne devez jamais utiliser l’opérateur new
pour instancier une interface :
x = new Comparable(. . .); // ERREUR
Cependant, bien que vous ne puissiez pas construire des objets interface, vous pouvez toujours
déclarer des variables interface :
Comparable x; // OK
Une variable interface doit faire référence à un objet d’une classe qui implémente l’interface :
x = new Employee(. . .);
// OK du moment que Employee implémente Comparable
Ensuite, tout comme vous utilisez instanceof pour vérifier si un objet appartient à une classe spécifique, vous pouvez utiliser instanceof pour vérifier si un objet implémente une interface :
if (unObjet instanceof Comparable) { . . . }
Tout comme vous pouvez construire des hiérarchies de classes, vous pouvez étendre des interfaces.
Cela autorise plusieurs chaînes d’interfaces allant d’un plus large degré de généralité à un plus petit
degré de spécialisation. Supposez, par exemple, que vous ayez une interface appelée Moveable :
public interface Moveable
{
void move(double x, double y);
}
Vous pourriez alors imaginer une interface appelée Powered qui l’étendrait :
public interface Powered extends Moveable
{
double milesPerGallon();
}
Bien que vous ne puissiez pas mettre des champs d’instance ou des méthodes statiques dans une
interface, vous pouvez fournir des constantes à l’intérieur. Par exemple :
public interface Powered extends Moveable
{
double milesPerGallon();
double SPEED_LIMIT = 95; // une constante public static final
}
Tout comme les méthodes dans une interface sont automatiquement public, les champs sont
toujours public static final.
Livre Java .book Page 250 Jeudi, 25. novembre 2004 3:04 15
250
Au cœur de Java 2 - Notions fondamentales
INFO
Il est légal de qualifier les méthodes d’interface de public, et les champs de public static final. Certains
programmeurs le font, par habitude ou pour une plus grande clarté. Cependant les spécifications du langage Java
recommandent de ne pas fournir de mots clés redondants, et nous respectons cette recommandation.
Certaines interfaces définissent simplement les constantes et pas de méthodes. Par exemple, la
bibliothèque standard contient une interface SwingConstants qui définit les constantes NORTH,
SOUTH, HORIZONTAL, etc. Toute classe qui choisit d’implémenter l’interface SwingConstants hérite
automatiquement de ces constantes. Ses méthodes peuvent faire simplement référence à NORTH au
lieu du SwingConstants.NORTH, plus encombrant. Toutefois, cette utilisation des interfaces semble
plutôt dégénérée, et nous ne la recommandons pas.
Alors que chaque classe ne peut avoir qu’une superclasse, les classes peuvent implémenter plusieurs
interfaces. Cela vous donne un maximum de flexibilité pour la définition du comportement d’une
classe. Par exemple, le langage Java a une importante interface intégrée, appelée Cloneable (nous
reviendrons en détail sur cette interface dans la section suivante). Si votre classe implémente Cloneable, la méthode clone dans la classe Object réalisera une copie fidèle des objets de votre classe.
Supposez, par conséquent, que vous vouliez disposer des fonctions de clonage et de comparaison ; il
vous suffira d’implémenter les deux interfaces :
class Employee implements Cloneable, Comparable
Utilisez des virgules pour séparer les interfaces qui décrivent les caractéristiques que vous voulez
fournir.
Interfaces et classes abstraites
Si vous avez lu la section concernant les classes abstraites au Chapitre 5, vous vous demandez peutêtre pourquoi les concepteurs du langage Java se sont compliqué la tâche en introduisant le concept
d’interface. Pourquoi ne pas faire de Comparable une classe abstraite ?
abstract class Comparable // Pourquoi pas ?
{
public abstract int compareTo(Object other);
}
La classe Employee pourrait simplement étendre cette classe abstraite et fournir la méthode
compareTo :
class Employee extends Comparable // Pourquoi pas ?
{
public int compareTo(Object other) { . . . }
}
Il y a, malheureusement, un problème majeur en ce qui concerne l’utilisation d’une classe de base
abstraite pour exprimer une propriété générique. Une classe ne peut étendre qu’une seule classe.
Supposons qu’une autre classe dérive déjà de la classe Employee, disons Person. La classe Employee
ne peut alors étendre une seconde classe :
class Employee extends Person, Comparable // ERREUR
En revanche, chaque classe peut implémenter autant d’interfaces qu’elle le souhaite :
class Employee extends Person implements Comparable // OK
Livre Java .book Page 251 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
251
Les autres langages de programmation, en particulier C++, autorisent une classe à avoir plus d’une
superclasse. Cette fonctionnalité est appelée héritage multiple. Les concepteurs de Java ont choisi de
ne pas la prendre en charge, car elle rend le langage, soit très complexe (comme dans C++), soit
moins efficace (comme dans Eiffel).
Les interfaces, elles, procurent la plupart des avantages de l’héritage multiple tout en évitant les
risques de complexité et d’inefficacité.
INFO C++
Le langage C++ prend en charge l’héritage multiple, avec tous les tracas qui l’accompagnent, comme les classes de
base virtuelles, les règles de prédominance et les transtypages de pointeurs transversaux. Peu de programmeurs C++
exploitent réellement l’héritage multiple, et certains affirment qu’il ne devrait jamais être utilisé. D’autres
conseillent de n’employer l’héritage multiple que pour l’héritage de style "mix-in". Dans ce style, une classe de base
primaire décrit l’objet parent et des classes de base additionnelles (les mix-in) peuvent fournir des caractéristiques
complémentaires. Ce style est comparable à une classe Java ayant une seule classe de base et des interfaces additionnelles. Néanmoins, en C++, les mix-in peuvent ajouter un comportement par défaut, ce que ne peuvent pas faire les
interfaces Java.
Clonage d’objets
Lorsque vous faites une copie d’une variable, l’original et la copie sont des références au même objet
(voir Figure 6.1). Cela signifie que tout changement apporté à l’une des variables affecte aussi
l’autre.
Employee original = new Employee("John Public", 50000);
Employee copy = original;
copy.raiseSalary(10); // aïe--a aussi changé l’original
Si vous voulez que copy soit un nouvel objet qui, au départ, est identique à original, mais dont
l’état peut diverger avec le temps, appelez la méthode clone.
Employee copy = original.clone();
copy.raiseSalary(10); // OK—-original inchangé
En réalité, les choses ne sont pas si simples. La méthode clone est une méthode protected de
Object, ce qui signifie que votre code ne peut pas simplement l’appeler directement. Seule la
classe Employee peut cloner des objets Employee. Et il y a une bonne raison à cela. Réfléchissons
à la manière dont la classe Object peut implémenter clone. Elle ne sait rien de l’objet et ne peut
effectuer qu’une copie champ à champ. Si tous les champs de données de l’objet sont des nombres
ou appartiennent à un autre type de base, la copie des champs ne posera pas de problème. Mais si
l’objet contient des références à des sous-objets, la copie des champs vous donnera une autre référence au sous-objet. En conséquence, l’original et les objets clonés continueront à partager certaines
informations.
Pour visualiser ce phénomène, considérons la classe Employee, présentée au Chapitre 4. La
Figure 6.2 montre ce qui se passe lorsque vous appelez la méthode clone de la classe Object pour
cloner un tel objet Employee. Vous pouvez constater que l’opération de clonage par défaut est
"superficielle" (shallow) — elle ne clone pas les objets qui sont référencés à l’intérieur d’autres
objets.
Livre Java .book Page 252 Jeudi, 25. novembre 2004 3:04 15
252
Au cœur de Java 2 - Notions fondamentales
Le fait que cette copie soit superficielle est-il important ? Cela dépend. Si le sous-objet qui est
partagé entre l’original et le clone superficiel est inaltérable, le partage est sûr. Cela se produit
certainement si le sous-objet appartient à une classe inaltérable, telle que String. Le sous-objet peut
aussi tout simplement rester constant pendant toute la durée de vie de l’objet, sans que des méthodes
d’altération ne l’affectent, ni qu’aucune méthode ne produise de référence vers lui.
Figure 6.1
Copying
Copie et clonage.
original =
Employee
copy =
Cloning
original =
Employee
copy =
Employee
Cependant, assez fréquemment, les sous-objets sont altérables, et vous devez redéfinir la méthode
clone pour réaliser une copie "intégrale" (deep copy) qui clone aussi les sous-objets. Dans notre
exemple, le champ hireDay est un objet Date, qui lui est altérable.
Pour chaque classe, vous devez décider si oui ou non :
1. La méthode clone par défaut convient.
2. La méthode clone par défaut peut être corrigée par un appel de clone sur les sous-objets
altérables.
3. La méthode clone ne doit pas être tentée.
Livre Java .book Page 253 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Figure 6.2
Interfaces et classes internes
original =
Une copie
"shallow".
Employee
253
String
name =
salary =
50000.0
hireDay =
copy =
Employee
Date
name =
salary =
50000.0
hireDay =
La troisième option est celle par défaut. Pour pouvoir choisir la première ou la deuxième option, une
classe doit :
1. Implémenter l’interface Cloneable.
2. Redéfinir la méthode clone avec le modificateur d’accès public.
INFO
La méthode clone est déclarée protected dans la classe Object afin que votre code ne puisse pas simplement
appeler unObjet.clone(). Mais les méthodes protégées ne sont-elles pas accessibles à partir de n’importe quelle
sous-classe, et toute classe n’est-elle pas une sous-classe de Object ? Heureusement, les règles concernant les accès
protégés sont plus subtiles (voir Chapitre 5). Une sous-classe peut appeler une méthode clone protégée, seulement
pour cloner ses propres objets. Vous devez redéfinir clone comme publique pour permettre aux objets d’être clonés
par n’importe quelle méthode.
Dans ce cas, l’apparence de l’interface Cloneable n’a rien à voir avec l’utilisation normale des
interfaces. En particulier, elle ne spécifie pas la méthode clone — qui est héritée de la classe
Object. L’interface sert purement de balise, indiquant que le concepteur de la classe comprend le
processus de clonage. Les objets sont tellement paranoïaques en ce qui concerne le clonage, qu’ils
génèrent une exception vérifiée (checked exception), si un objet requiert le clonage, mais n’implémente pas cette interface.
INFO
L’interface Cloneable fait partie des quelques interfaces de balisage que fournit Java (certains programmeurs les
appellent interfaces marqueur). Souvenez-vous que l’intérêt habituel d’une interface telle que Comparable est
d’assurer qu’une classe implémente une méthode ou un jeu de méthodes spécifiques. Une interface de balisage n’a
pas de méthodes ; son seul objectif est de permettre l’utilisation de instanceof dans une requête de type :
if (obj instanceof Cloneable) . . .
Nous vous recommandons de ne pas utiliser cette technique dans vos programmes.
Livre Java .book Page 254 Jeudi, 25. novembre 2004 3:04 15
254
Au cœur de Java 2 - Notions fondamentales
Même si l’implémentation par défaut (copie superficielle) de clone est adéquate, vous devez toujours
implémenter l’interface Cloneable, redéfinir clone comme public, appeler super.clone(). Voici un
exemple :
class Employee implements Cloneable
{
// élever le niveau de visibilité à public
public Employee clone() throws CloneNotSupportedException
{
return super.clone();
}
. . .
}
INFO
Avant le JDK 5.0, la méthode clone avait toujours un type de retour Object. Les types de retours covariants du JDK
5.0 vous permettent de spécifier le type de retour correct pour vos méthodes clone.
La méthode clone que vous venez de voir n’ajoute aucune fonctionnalité à la copie "shallow" fournie par Object.clone. Elle se contente de rendre la méthode public. Pour réaliser une copie intégrale,
vous devrez poursuivre l’exercice et cloner les champs d’instance altérables.
Voici un exemple de méthode clone qui crée une copie intégrale :
class Employee implements Cloneable
{
. . .
public Object clone() throws CloneNotSupportedException
{
// appeler Object.clone()
Employee cloned = (Employee) super.clone();
// cloner les champs altérables
cloned.hireDay = (Date) hireDay.clone()
return cloned;
}
}
La méthode clone de la classe Object menace de lancer une exception CloneNotSupportedException, et ce dès que clone est appelée sur un objet dont la classe n’implémente pas l’interface
Cloneable. Bien entendu, les classes Employee et Date implémentent l’interface Cloneable,
l’exception ne sera donc pas déclenchée. Toutefois, le compilateur ne le sait pas. Nous avons donc
déclaré l’exception :
public Employee clone() throws CloneNotSupportedException
Vaudrait-il mieux l’intercepter ?
public Employee clone()
{
try
{
return super.clone();
}
Livre Java .book Page 255 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
255
catch (CloneNotSupportedException e) { return null; }
// ceci n’arriverait pas, puisque nous utilisons Cloneable
}
Ceci convient bien pour les classes final. Dans les autres cas, il vaut mieux laisser le spécificateur
throws à sa place. Les sous-classes ont alors l’option de lancer une exception CloneNotSupportedException si elles ne prennent pas en compte le clonage.
Vous devez prendre garde lorsque vous clonez des sous-classes. Par exemple, une fois que vous avez
défini la méthode clone pour la classe Employee, n’importe qui peut aussi cloner les objets Manager. La méthode clone Employee peut-elle assurer cette tâche ? Cela dépend des champs de la classe
Manager. Dans notre cas, cela ne pose pas de problème, car le champ bonus est du type primitif.
Mais Manager pourrait avoir acquis des champs qui exigent une copie intégrale ou qui ne peuvent
pas être clonés. Il n’est absolument pas garanti que l’implémenteur de la sous-classe dispose d’un
clone fixe pour réaliser l’action adéquate. Vous devez donc vous assurer que la méthode clone est
déclarée comme protégée (protected) dans la classe Object. Mais vous n’aurez pas ce luxe si vous
souhaitez que les utilisateurs de vos classes puissent appeler clone.
Devez-vous alors implémenter clone dans vos propres classes ? Si vos clients doivent réaliser des
copies intégrales, probablement oui. Certains auteurs considèrent toutefois qu’il vaut mieux éviter
totalement clone et implémenter à sa place une autre méthode dans ce but. Nous convenons que
clone est plutôt étrange, mais vous rencontrerez les mêmes problèmes en utilisant une autre
méthode. De toute façon, le clonage est moins commun que vous pourriez le penser. Moins de 5 %
des classes de la bibliothèque standard implémentent clone.
Le programme de l’Exemple 6.2 clone un objet Employee, puis invoque deux méthodes d’altération.
La méthode raiseSalary change la valeur du champ salary, alors que la méthode setHireDay
change l’état du champ hireDay. Aucune de ces altérations n’affecte l’objet original, car clone a été
définie pour faire une copie intégrale.
INFO
Le Chapitre 12 montre un autre mécanisme pour cloner les objets à l’aide de la fonction de sérialisation d’objet de
Java. Ce mécanisme est facile à implémenter et sûr, mais il n’est pas très efficace.
Exemple 6.2 : CloneTest.java
import java.util.*;
public class CloneTest
{
public static void main(String[] args)
{
try
{
Employee original = new Employee("John Q. Public", 50000);
original.setHireDay(2000, 1, 1);
Employee copy = original.clone();
copy.raiseSalary(10);
copy.setHireDay(2002, 12, 31);
System.out.println("original=" + original);
System.out.println("copy=" + copy);
}
Livre Java .book Page 256 Jeudi, 25. novembre 2004 3:04 15
256
Au cœur de Java 2 - Notions fondamentales
catch (CloneNotSupportedException e)
{
e.printStackTrace();
}
}
}
class Employee implements Cloneable
{
public Employee(String n, double s)
{
name = n;
salary = s;
}
public Employee clone() throws CloneNotSupportedException
{
// appeler Object.clone()
Employee cloned = (Employee)super.clone();
// cloner les champs altérables
cloned.hireDay = (Date)hireDay.clone();
return cloned;
}
/**
Affecte au jour d’embauche (hireday) une date donnée
@param year L’année du jour d’embauche
@param month Le mois du jour d’embauche
@param day Le jour d’embauche
*/
public void setHireDay(int year, int month, int day)
{
hireDay = new GregorianCalendar(year, month - 1, day).getTime();
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
public String toString()
{
return "Employee[name=" + name
+ ",salary=" + salary
+ ",hireDay=" + hireDay()
+ "]";
}
private String name;
private double salary;
private Date hireDay;
Livre Java .book Page 257 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
257
Interfaces et callbacks
Un pattern habituel de la programmation est celui des callbacks. Dans ce pattern, vous indiquez
l’action à réaliser en cas de survenue d’un événement particulier. Vous pourriez vouloir, par exemple, qu’une action particulière survienne lorsque l’on clique sur un bouton ou que l’on sélectionne
un élément de menu. Toutefois, comme vous n’avez pas encore vu comment implémenter les interfaces utilisateur, nous envisagerons une situation similaire, mais plus simple.
Le package javax.swing contient une classe Timer, utile pour être averti de l’expiration d’un délai
imparti. Par exemple, si une partie de votre programme contient une horloge, vous pouvez demander
à être averti à chaque seconde, de manière à pouvoir mettre à jour l’affichage de l’horloge.
Lorsque vous construisez un minuteur, vous définissez l’intervalle de temps et lui indiquez ce qu’il
doit faire lorsque le délai est écoulé.
Comment indiquer au minuteur ce qu’il doit faire ? Dans de nombreux langages de programmation, vous
fournissez le nom d’une fonction que le minuteur doit appeler périodiquement. Toutefois, les classes de la
bibliothèque Java adoptent une approche orientée objet. Vous transférez un objet d’une classe quelconque.
Le minuteur appelle alors l’une des méthodes de cet objet. Transférer un objet est une opération plus
flexible que transférer une fonction car l’objet peut transporter des informations complémentaires.
Bien entendu, le minuteur doit savoir quelle méthode appeler. Vous devez spécifier un objet d’une classe
qui implémente l’interface ActionListener du package java.awt.event. Voici cette interface :
public interface ActionListener
{
void actionPerformed(ActionEvent event);
}
Le minuteur appelle la méthode actionPerformed lorsque le délai a expiré.
INFO C++
Comme vous l’avez vu au Chapitre 5, Java possède l’équivalent des pointeurs de fonction, à savoir les objets Method.
Ils sont toutefois difficiles à utiliser, plus lents, et la sécurité des types ne peut pas être vérifiée au moment de la
compilation. Dès que vous utilisez un pointeur de fonction en C++, envisagez d’utiliser une interface en Java.
Supposons que vous souhaitiez afficher le message "At the tone, the time is ..." (à la tonalité, il
sera…), suivi d’un bip, et ce toutes les 10 secondes. Définissez une classe qui implémente l’interface
ActionListener. Vous placerez alors toutes les instructions que vous voulez voir exécuter dans la
méthode actionPerformed :
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
Date now = new Date();
System.out.println("At the tone, the time is " + now);
Toolkit.getDefaultToolkit().beep();
}
}
Remarquez le paramètre ActionEvent de la méthode actionPerformed. Il apporte des informations sur l’événement, comme l’objet source qui l’a généré (voir le Chapitre 8 pour en savoir plus).
Livre Java .book Page 258 Jeudi, 25. novembre 2004 3:04 15
258
Au cœur de Java 2 - Notions fondamentales
Toutefois, il n’est pas important d’obtenir des informations détaillées sur ce programme, et vous
pouvez ignorer le paramètre en toute sécurité.
Construisez ensuite un objet de cette classe et transmettez-le au constructeur Timer :
ActionListener listener = new TimePrinter();
Timer t = new Timer(10000, listener);
Le premier paramètre du constructeur Timer correspond au délai qui doit s’écouler entre les notifications, mesuré en millièmes de seconde. Nous voulons être avertis toutes les 10 secondes. Le
deuxième paramètre est l’objet écouteur.
Enfin, vous démarrez le minuteur :
t.start();
Toutes les 10 secondes, un message du type
At the tone, the time is Thu Apr 13 23:29:08 PDT 2000
s’affiche, suivi d’un bip.
L’Exemple 6.3 fait fonctionner le minuteur et son écouteur. Une fois le minuteur démarré, le
programme affiche un message et attend que l’utilisateur clique sur OK pour s’arrêter. Entre-temps,
l’heure actuelle s’affiche par intervalles de 10 secondes.
Soyez patient lorsque vous exécutez le programme. La boîte de dialogue "Quit program?" (fermer le
programme ?) s’affiche immédiatement, mais le premier message du minuteur n’apparaît qu’après
10 secondes.
Sachez que le programme importe la classe javax.swing.Timer par nom, en plus d’importer
javax.swing.* et java.util.*. Ceci annule l’ambiguïté qui existait entre javax.swing.Timer et
java.util.Timer, une classe non liée pour programmer les tâches d’arrière-plan.
Exemple 6.3 : TimerTest.java
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;
// résoudre le conflit avec java.util.Timer
public class TimerTest
{
public static void main(String[] args)
{
ActionListener listener = new TimePrinter();
// construit un minuteur qui appelle l’écouteur
// toutes les 10 secondes
Timer t = new Timer(10000, listener);
t.start();
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
class TimePrinter implements ActionListener
Livre Java .book Page 259 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
259
{
public void actionPerformed(ActionEvent event)
{
Date now = new Date();
System.out.println("At the tone, the time is " + now);
Toolkit.getDefaultToolkit().beep();
}
}
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;
// résoudre le conflit avec java.util.Timer
public class TimerTest
{
public static void main(String[] args)
{
ActionListener listener = new TimePrinter();
// construit un minuteur qui appelle l’écouteur
// toutes les 10 secondes
Timer t = new Timer(10000, listener);
t.start();
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
Date now = new Date();
System.out.println("At the tone, the time is " + now);
Toolkit.getDefaultToolkit().beep();
}
}
javax.swing.JOptionPane 1.2
•
static void showMessageDialog(Component parent, Object message)
Affiche une boîte de dialogue avec une invite et un bouton OK. La boîte de dialogue est centrée
sur le composant parent. Si parent est null, la boîte de dialogue est centrée à l’écran.
javax.swing.Timer 1.2
•
Timer(int interval, ActionListener listener)
Construit un minuteur qui avertit l’écouteur lorsque les millièmes de seconde de l’intervalle se
sont écoulés.
•
void start()
Démarre le minuteur. Une fois lancé, il appelle actionPerformed sur ses écouteurs.
•
void stop()
Arrête le minuteur. Une fois arrêté, il n’appelle plus actionPerformed sur ses écouteurs.
Livre Java .book Page 260 Jeudi, 25. novembre 2004 3:04 15
260
Au cœur de Java 2 - Notions fondamentales
javax.awt.Toolkit 1.0
• static Toolkit getDefaultToolkit()
Récupère la boîte à outils par défaut. Une boîte à outils contient des informations sur l’environnement de l’interface graphique utilisateur.
•
void beep()
Emet un bip.
Classes internes
Une classe interne est une classe qui est définie à l’intérieur d’une autre classe. Trois raisons justifient
l’emploi de classes internes :
m
Les méthodes de classe internes peuvent accéder aux données, à partir de l’envergure où elles
sont définies, y compris les données qui pourraient être des données privées.
m
Les classes internes peuvent être cachées aux autres classes du même package.
m
Les classes internes anonymes sont utiles pour définir des callbacks sans écrire beaucoup de
code.
Nous allons diviser ce sujet assez complexe en plusieurs étapes :.
m
A partir de la section suivante, vous verrez une classe interne simple qui accède à un champ
d’instance de sa classe externe.
m
A la section "Règles particulières de syntaxe pour les classes internes", nous verrons les règles
de syntaxe spéciales pour les classes internes.
m
A la section "Utilité, nécessité et sécurité des classes internes", nous étudierons les classes internes pour voir comment les traduire en classes ordinaires. Les lecteurs délicats pourront sauter
cette section.
m
A la section "Classes internes locales", nous discuterons des classes internes locales qui peuvent
accéder aux variables locales dans la portée qui les englobe.
m
A la section "Classes internes anonymes", nous présenterons les classes internes anonymes et
montrerons comment les utiliser habituellement pour implémenter les callbacks.
m
Enfin, à partir de la section "Classes internes statiques", vous verrez comment utiliser les classes
internes statiques pour les classes d’aide imbriquées.
INFO C++
Le langage C++ permet l’emploi de classes imbriquées. Une classe imbriquée se situe dans la portée de la classe qui
l’englobe. Voici un exemple typique ; une classe de liste liée définit une classe permettant de stocker les liens et une
classe qui détermine une position d’itération :
class LinkedList
{
public:
class Iterator // une classe imbriquée
{
public:
void insert(int x);
int erase();
Livre Java .book Page 261 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
261
. . .
};
. . .
private:
class Link // une classe imbriquée
{
public:
Link* next;
int data;
};
. . .
};
L’imbrication est une relation entre les classes, non entre les objets. Un objet LinkedList n’a pas de sous-objets de
types Iterator ou Link.
Cela procure deux avantages : le contrôle de nom et le contrôle d’accès. Puisque le nom Iterator est imbriqué dans
la classe LinkedList, il est connu à l’extérieur sous la forme LinkedList::Iterator et ne peut pas entrer en
conflit avec une autre classe baptisée Iterator. En Java, cet avantage est moins important, car les packages Java
procurent un contrôle de nom équivalent. Remarquez que la classe Link se trouve dans la partie private de la
classe LinkedList. Elle est absolument invisible au reste du code. Pour cette raison, il n’y a aucun risque à déclarer
ses champs public. Ils ne sont accessibles qu’aux méthodes de la classe LinkedList (qui a le besoin légitime d’y
accéder). Ils sont invisibles à l’extérieur de LinkedList. En Java, ce genre de contrôle n’était pas possible avant
l’arrivée des classes internes.
Les classes internes de Java possèdent cependant une caractéristique supplémentaire qui les rend plus riches, et donc
plus utiles que les classes imbriquées de C++. Un objet d’une classe interne possède une référence implicite à l’objet
de la classe externe qui l’a instancié. Grâce à ce pointeur, il peut accéder à tous les champs de l’objet externe. Nous
verrons dans ce chapitre les détails de ce mécanisme.
En Java, les classes internes static ne sont pas dotées de ce pointeur. Elles sont exactement équivalentes aux classes
imbriquées de C++.
Accéder à l’état d’un objet à l’aide d’une classe interne
La syntaxe des classes internes est assez complexe. C’est pourquoi nous utiliserons un exemple
simple, bien que peu réaliste, pour démontrer l’usage des classes internes. Nous allons refactoriser
l’exemple TimerTest et extraire une classe TalkingClock. Une horloge parlante se construit avec
deux paramètres : l’intervalle entre les annonces et une balise pour activer ou désactiver le bip :
class TalkingClock
{
public TalkingClock(int interval, boolean beep)
public void start() { . . . }
{ . . . }
private int interval;
private boolean beep;
private class TimePrinter implements ActionListener
// une classe interne
{
. . .
}
}
Livre Java .book Page 262 Jeudi, 25. novembre 2004 3:04 15
262
Au cœur de Java 2 - Notions fondamentales
Remarquez la classe TimePrinter, qui se trouve à l’intérieur de la classe TalkingClock. Cela ne
signifie pas que chaque TalkingClock possède un champ d’instance TimePrinter. Comme vous le
verrez, les objets TimePrinter sont construits par les méthodes de la classe TalkingClock.
La classe TimePrinter est une classe interne privée au sein de TalkingClock. C’est un mécanisme
sécurisé. Seules les méthodes de TalkingClock peuvent générer des objets TimePrinter. Seules
les classes internes peuvent être privées. Les classes régulières ont toujours une visibilité publique
ou au niveau du package.
Voici la classe TimePrinter plus en détail. Sachez que la méthode actionPerformed vérifie la
balise du bip avant d’émettre le son :
private class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
Date now = new Date();
System.out.println("At the tone, the time is " + now);
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
Une chose surprenante se passe. La classe TimePrinter n’a pas de champ d’instance ni de variable
nommée beep. En fait, beep fait référence au champ de l’objet TalkingClock qui a créé TimePrinter. C’est une innovation. Traditionnellement, une méthode pouvait référencer les champs de l’objet
qui l’invoquait. Une méthode de la classe interne a accès à la fois à ses propres champs et à ceux de
l’objet externe créateur.
Pour que tout cela fonctionne, un objet d’une classe interne obtient toujours une référence implicite
à l’objet qui l’a créé (voir Figure 6.3).
Figure 6.3
Un objet d’une classe
interne possède une
référence à un objet
d’une classe externe.
TimePrinter
outer =
TalkingClock
interval =
beep =
1000
true
Cette référence est invisible dans la définition de la classe interne. Pour faire la lumière sur ce
concept, nous allons appeler la référence à l’objet externe outer. La méthode actionPerformed est
alors équivalente à ce qui suit :
public void actionPerformed(ActionEvent event)
{
Date now = new Date();
Livre Java .book Page 263 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
263
System.out.println("At the tone, the time is " + now);
if (outer.beep) Toolkit.getDefaultToolkit().beep();
}
La référence de classe externe est définie dans le constructeur. Etant donné que TalkingClock ne
définit aucun constructeur, le compilateur synthétise un constructeur, générant un code comme celuici :
public TimePrinter(TalkingClock clock) // Code généré automatiquement
{
outer = clock;
}
Notez bien que outer n’est pas un mot clé du langage Java. Nous l’avons uniquement utilisé pour
illustrer le mécanisme mis en œuvre dans une classe interne.
INFO
Si une classe interne possède des constructeurs, le compilateur les modifie, en ajoutant un paramètre pour la référence de classe externe.
Lorsqu’un objet TimePrinter est construit dans la méthode start, le compilateur passe la référence this au constructeur dans l’horloge parlante courante :
ActionListener listener = new TimePrinter( this); //
// paramètre ajouté automatiquement
L’Exemple 6.4 décrit le programme complet qui teste la classe interne. Examinez de nouveau le
contrôle d’accès. Si la classe TimePrinter avait été une classe régulière, il aurait fallu accéder à la
balise beep par l’intermédiaire d’une méthode publique de la classe TalkingClock. L’emploi d’une
classe interne constitue une amélioration. Il n’est pas nécessaire de fournir des méthodes d’accès qui
n’intéressent qu’une seule autre classe.
Exemple 6.4 : InnerClassTest.java
import
import
import
import
import
java.awt.*;
java.awt.event.*;
java.util.*;
javax.swing.*;
javax.swing.Timer;
public class InnerClassTest
{
public static void main(String[] args)
{
TalkingClock clock = new TalkingClock(1000, true);
clock.start();
// laisser le programme fonctionner jusqu’à ce que l’utilisateur
// clique sur "OK"
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
Livre Java .book Page 264 Jeudi, 25. novembre 2004 3:04 15
264
Au cœur de Java 2 - Notions fondamentales
/**
Une horloge qui affiche l’heure à intervalles réguliers.
*/
class TalkingClock
{
/**
Construit une horloge parlante
@param interval l’intervalle entre les messages (en millièmes de
seconde
@param beep true si l’horloge doit sonner
*/
public TalkingClock(int interval, boolean beep)
{
this.interval = interval;
this.beep = beep;
}
/**
Lance l’horloge.
*/
public void start()
{
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval, listener);
t.start();
}
private int interval;
private boolean beep;
private class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
Date now = new Date();
System.out.println("At the tone, the time is " + now);
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
}
Règles particulières de syntaxe pour les classes internes
Dans la section précédente, nous avons explicité la référence de classe externe d’une classe interne
en la baptisant outer. En réalité, la syntaxe correcte pour la référence externe est un peu plus
complexe. L’expression
ClasseExterne.this
indique la référence de classe externe. Vous pouvez par exemple, écrire la méthode actionPerformed
de la classe interne TimePrinter de la façon suivante :
public void actionPerformed(ActionEvent event)
{
...
if (TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();
}
Livre Java .book Page 265 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
265
Inversement, vous pouvez écrire le constructeur de l’objet interne plus explicitement, à l’aide de la
syntaxe :
objetExterne.new ClasseInterne(paramètres de construction)
Par exemple,
ActionListener listener = this.new TimePrinter();
Ici, la référence à la classe externe de l’objet TimePrinter nouvellement construit est définie par la
référence this de la méthode qui crée l’objet de la classe interne. C’est le cas le plus courant.
Comme toujours, l’indication this. est redondante. Néanmoins, il est également possible de donner
à la référence de la classe externe une autre valeur en la nommant explicitement. Par exemple, si
TimePrinter est une classe interne publique, vous pouvez construire un objet TimePrinter pour
n’importe quelle horloge parlante :
TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();
Notez que vous faites référence à une classe interne de la façon suivante
ClasseExterne.ClasseInterne
lorsqu’elle se trouve être hors de la portée de la classe externe. Par exemple, si TimePrinter avait
été une classe publique, vous auriez pu y faire référence sous la forme TalkingClock.TimePrinter
ailleurs dans votre programme.
Utilité, nécessité et sécurité des classes internes
Lorsque les classes internes ont été ajoutées au langage Java dans le JDK 1.1, de nombreux programmeurs les ont considérées comme une nouvelle fonctionnalité majeure qui était hors de propos dans
la philosophie Java. La syntaxe est complexe (nous le verrons en examinant les classes internes
anonymes dans la suite de ce chapitre). Il n’est pas évident de saisir l’interaction des classes internes avec
d’autres caractéristiques du langage comme le contrôle d’accès et la sécurité.
En ajoutant une fonctionnalité élégante et intéressante plutôt que nécessaire, Java a-t-il commencé à
suivre la voie funeste de tant d’autres langages, en se dotant d’une caractéristique élégante et intéressante,
mais pas nécessaire ?
Nous ne donnerons pas une réponse définitive, mais notez que les classes internes constituent un
phénomène qui est lié au compilateur et non à la machine virtuelle. Les classes internes sont traduites en fichiers de classe réguliers, avec des signes $ délimitant les noms des classes externes et internes,
et la machine virtuelle Java ne les reconnaît pas comme une particularité.
Par exemple, la classe TimePrinter à l’intérieur de la classe TalkingClock est traduite en un
fichier de classe TalkingClock$TimePrinter.class. Pour le constater, faites cette expérience :
exécutez le programme ReflectionTest du Chapitre 5 et donnez-lui comme classe de réflexion la
classe TalkingClock$TimePrinter. Vous obtiendrez ce qui suit :
class TalkingClock$TimePrinter
{
private TalkingClock$TimePrinter(TalkingClock);
TalkingClock$TimePrinter(TalkingClock, TalkingClock$1);
Livre Java .book Page 266 Jeudi, 25. novembre 2004 3:04 15
266
Au cœur de Java 2 - Notions fondamentales
public void actionPerformed(java.awt.event.ActionEvent);
final TalkingClock this$0;
}
INFO
Si vous travaillez sous UNIX, n’oubliez pas d’échapper le caractère $ si vous fournissez le nom de la classe sur la ligne
de commande. Vous devez donc exécuter le programme ReflectionTest de la façon suivante :
java ReflectionTest ’TalkingClock$TimePrinter’.
Vous pouvez voir que le compilateur a généré un champ d’instance supplémentaire, this$0, pour la
référence à la classe externe (le nom this$0 est synthétisé par le compilateur — vous ne pouvez pas
y faire référence dans le code source). Vous pouvez aussi voir le paramètre ajouté pour le constructeur. En réalité, la séquence de construction est quelque peu mystérieuse. Un constructeur privé
établit le champ this$0 et un constructeur visible au package a un second paramètre du type TalkingClock$1, une classe visible pour le package, sans champs ni méthodes. Cette classe n’est jamais
instanciée. La classe TalkingClock appelle
new TalkingClock$TimePrinter(this, null)
Si le compilateur peut effectuer automatiquement cette transformation, ne pourrait-on pas simplement programmer manuellement le même mécanisme ? Essayons. Nous allons faire de TimePrinter
une classe régulière, extérieure à la classe TalkingClock. Lors de la construction d’un objet TimePrinter, nous lui passerons la référence this de l’objet qui le crée :
class TalkingClock
{
. . .
public void start()
{
ActionListener listener = new TimePrinter(this);
Timer t = new Timer(interval, listener);
t.start();
}
}
class TimePrinter implements ActionListener
{
public TimePrinter(TalkingClock clock)
{
outer = clock;
}
. . .
private TalkingClock outer;
}
Examinons maintenant la méthode actionPerformed. Elle doit pouvoir accéder à outer.beep.
if (outer.beep) . . . // ERREUR
Nous venons de rencontrer un problème. La classe interne peut accéder aux données privées de la
classe externe, mais notre classe externe TimePrinter ne le peut pas.
Livre Java .book Page 267 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
267
Nous voyons bien que les classes internes sont intrinsèquement plus puissantes que les classes régulières, puisqu’elles ont un privilège d’accès supérieur.
Il est légitime de se demander comment les classes internes peuvent acquérir ce privilège d’accès
supérieur, alors qu’elles sont traduites en classes régulières simplement dotées de noms particuliers
— la machine virtuelle ne les distingue pas. Pour résoudre ce mystère, utilisons de nouveau le
programme ReflectionTest afin d’espionner la classe TalkingClock :
class TalkingClock
{
public TalkingClock(int, boolean);
static boolean access$100(TalkingClock);
public void start();
private int interval;
private boolean beep;
}
Remarquez la méthode statique access$100 ajoutée par le compilateur à la classe externe. Elle
renvoie le champ beep de l’objet transféré en tant que paramètre.
Elle est appelée par les méthodes de la classe interne. L’instruction
if(beep)
dans la méthode actionPerformed de la classe TimePrinter réalise, en fait, l’appel suivant :
if (access$100(outer));
Y a-t-il un risque pour la sécurité ? Bien sûr. Il est facile à un tiers d’invoquer la méthode
access$100 pour lire le champ privé beep. Bien entendu, access$100 n’est pas un nom autorisé
pour une méthode Java. Néanmoins, pour des pirates familiers de la structure des fichiers de classe,
il est facile de produire un fichier de classe avec des instructions (machine virtuelle) qui appellent
cette méthode, par exemple en utilisant un éditeur hexadécimal. Les méthodes d’accès secrètes ayant
une visibilité au niveau du package, le code d’attaque devrait être placé dans le même package que
la classe attaquée.
En résumé, si une classe interne accède à un champ privé, il est possible d’accéder à ce champ par le
biais de classes ajoutées au package de la classe externe, mais une telle opération exige du talent et
de la détermination. Un programmeur ne peut pas obtenir accidentellement un tel privilège d’accès ;
pour y parvenir, il doit intentionnellement créer ou modifier un fichier de classe.
Classes internes locales
Si vous examinez attentivement le code de l’exemple TalkingClock, vous constaterez que vous
n’avez besoin qu’une seule fois du nom du type TimePrinter : lorsque vous créez un objet de ce
type dans la méthode start.
Dans une telle situation, vous pouvez définir les classes localement à l’intérieur d’une seule
méthode :
public void start()
{
class TimePrinter implements ActionListener
{
Livre Java .book Page 268 Jeudi, 25. novembre 2004 3:04 15
268
Au cœur de Java 2 - Notions fondamentales
public void actionPerformed(ActionEvent event)
{
Date now = new Date();
System.out.println("At the tone, the time is " + now);
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(1000, listener);
t.start();
}
Les classes locales ne sont jamais déclarées avec un spécificateur d’accès (c’est-à-dire public ou
private). Leur portée est toujours restreinte au bloc dans lequel elles sont déclarées.
Les classes locales présentent l’immense avantage d’être complètement cachées au monde extérieur,
et même au reste du code de la classe TalkingClock. A part start, aucune méthode ne connaît
l’existence de la classe TimePrinter.
Les classes locales ont un autre avantage sur les autres classes internes. Elles peuvent non seulement
accéder aux champs de leurs classes externes, mais également aux variables locales ! Ces variables
locales doivent être déclarées final. Voici un exemple typique. Déplaçons les paramètres interval
et beep du constructeur TalkingClock à la méthode start :
public void start(int interval, final boolean beep)
{
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
Date now = new Date();
System.out.println("At the tone, the time is " + now);
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(1000, listener);
t.start();
}
Notez que la classe TimePrinter n’a plus besoin de stocker une variable d’instance beep. Elle fait
simplement référence à la variable paramètre de la méthode qui contient la définition de classe.
Cela n’est peut-être pas si surprenant. La ligne
if (beep) . . .
est située au plus profond de la méthode start, alors pourquoi ne pourrait-elle pas avoir accès à la
variable beep ?
Afin de comprendre ce problème délicat, examinons de plus près le flux d’exécution :
1. La méthode start est appelée.
Livre Java .book Page 269 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
269
2. La variable objet listener est initialisée par un appel au constructeur de la classe interne TimePrinter.
3. La référence à listener est passée au constructeur de Timer, le temporisateur est démarré, et la
méthode start se termine. A ce moment, la variable paramètre beep de la méthode start
n’existe plus.
4. Une seconde plus tard, la méthode actionPerformed s’exécute if (beep) . . . :
double interest = balance * rate / 100;.
Pour que le code de la méthode actionPerformed fonctionne, la classe TimePrinter doit avoir fait
une copie du champ beep avant qu’il ne disparaisse en tant que variable locale de la méthode start.
Et c’est exactement ce qui se passe. Dans notre exemple, le compilateur synthétise le nom TalkingClock$1TimePrinter pour la classe interne locale. Si vous utilisez le programme ReflectionTest
pour espionner la classe TalkingClock$1TimePrinter, vous obtiendrez ce qui suit :
class TalkingClock$1TimePrinter
{
TalkingClock$1TimePrinter(TalkingClock, boolean);
public void actionPerformed(java.awt.event.ActionEvent);
final boolean val$beep;
final TalkingClock this$0;
}
Remarquez le paramètre boolean supplémentaire du constructeur et la variable d’instance
val$beep. Lorsqu’un objet est créé, la valeur beep est passée au constructeur et stockée dans le
champ val$beep. Cela engendre bien des soucis pour les implémenteurs du compilateur. Celui-ci
doit détecter l’accès des variables locales, créer un champ de données pour chacune d’elles et copier
les variables locales dans le constructeur afin que les champs de données soient initialisés avec les
mêmes valeurs.
Du point de vue du programmeur, toutefois, l’accès aux variables locales est assez plaisant. Il simplifie vos classes internes en réduisant le nombre des champs d’instance que vous devez programmer
explicitement.
Comme nous l’avons déjà signalé, les méthodes d’une classe locale peuvent uniquement faire référence à des variables locales déclarées final. C’est la raison pour laquelle le paramètre beep a été
déclaré final dans notre exemple. Une variable locale déclarée final ne peut pas être modifiée
après avoir été initialisée. Ainsi, nous avons la garantie que la variable locale et sa copie dans la
classe locale auront bien la même valeur.
INFO
Vous avez déjà vu des variables final utilisées en tant que constantes, à l’image de ce qui suit :
public static final double SPEED_LIMIT = 55;
Le mot clé final peut être appliqué aux variables locales, aux variables d’instance et aux variables statiques. Dans
tous les cas, cela signifie la même chose : cette variable ne peut être affectée qu’une seule fois après sa création.
Il n’est pas possible d’en modifier ultérieurement la valeur — elle est définitive.
Cela dit, il n’est pas obligatoire d’initialiser une variable final lors de sa définition. Par exemple, le paramètre
final beep est initialisé une fois après sa création, lorsque la méthode start est appelée (si elle est appelée
Livre Java .book Page 270 Jeudi, 25. novembre 2004 3:04 15
270
Au cœur de Java 2 - Notions fondamentales
plusieurs fois, chaque appel crée son propre paramètre beep). La variable d’instance val$beep, que nous avons vue
dans la classe interne TalkingClock$1TimePrinter, est définie une seule fois, dans le constructeur de la classe
interne. Une variable final qui n’est pas initialisée lors de sa définition est souvent appelée variable finale vide.
Classes internes anonymes
Lors de l’utilisation de classes internes locales, vous pouvez souvent aller plus loin. Si vous ne désirez créer qu’un seul objet de cette classe, il n’est même pas nécessaire de donner un nom à la classe.
Une telle classe est appelée classe interne anonyme :
public void start(int interval, final boolean beep)
{
ActionListener listener = new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
Date now = new Date();
System.out.println("At the tone, the time is " + now);
if (beep) Toolkit.getDefaultToolkit().beep();
}
};
Timer t = new Timer(1000, listener);
t.start();
}
Il s’agit là d’une syntaxe assez obscure qui signifie :
Créer un nouvel objet d’une classe qui implémente l’interface ActionListener, où la méthode
actionPerformed requise est celle définie entre les accolades { }.
Tous les paramètres employés pour construire l’objet sont donnés entre les parenthèses ( ) qui
suivent le nom du supertype. En général, la syntaxe est :
new SuperType(paramètres de construction)
{
méthodes et données de la classe interne
}
SuperType peut être ici une interface telle que ActionListener ; la classe interne implémente alors
cette interface. SuperType peut également être une classe, et dans ce cas la classe interne étend cette
classe.
Une classe interne anonyme ne peut pas avoir de constructeurs, car le nom d’un constructeur doit
être identique à celui de la classe (et celle-ci n’a pas de nom). Au lieu de cela, les paramètres de
construction sont donnés au constructeur de la superclasse. En particulier, chaque fois qu’une classe
interne implémente une interface, elle ne peut pas avoir de paramètres de construction. Il faut néanmoins
toujours fournir les parenthèses, comme dans cet exemple :
new TypeInterface() { méthodes et données }
Il faut y regarder à deux fois pour faire la différence entre la construction d’un nouvel objet d’une
classe régulière et la construction d’un objet d’une classe interne anonyme qui étend cette classe.
Livre Java .book Page 271 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
271
Si la parenthèse fermante de la liste de paramètres du constructeur est suivie d’une accolade
ouvrante, cela définit une classe interne anonyme :
Person queen =
// un objet
Person count =
// un objet
new Person("Mary");
Person
new Person("Dracula") { ... };
d’une classe interne étendant Person
Les classes internes anonymes sont-elles une brillante idée, ou plutôt un excellent moyen d’écrire du
code hermétique ? Sans doute les deux. Lorsque le code d’une classe interne est très court — quelques lignes de code simple — cela peut faire gagner un peu de temps. Mais c’est exactement le genre
d’économie qui vous amène à concourir pour le prix du code Java le plus obscur.
Il est dommage que les concepteurs de Java n’aient pas tenté de perfectionner la syntaxe des classes
internes anonymes, car, le plus souvent, la syntaxe de Java constitue une amélioration par rapport à
celle de C++. Les concepteurs auraient pu aider l’utilisateur avec une syntaxe comme celle-ci :
Person count = new class extends Person("Dracula") { ... };
// Attention, ce n’est pas la syntaxe réelle de Java
Mais ils ne l’ont pas fait. Bien des programmeurs trouvant difficile de déchiffrer un code contenant
de trop nombreuses classes internes anonymes, il est recommandé d’en restreindre l’usage.
L’Exemple 6.5 contient le code source complet du programme d’horloge parlante avec une classe
interne anonyme. Si vous comparez ce programme avec celui de l’Exemple 6.4, vous verrez que le
code avec la classe interne anonyme est plus court, et, heureusement, avec un peu de pratique, aussi
facile à comprendre.
Exemple 6.5 : AnonymousInnerClassTest.java
import
import
import
import
import
java.awt.*;
java.awt.event.*;
java.util.*;
javax.swing.*;
javax.swing.Timer;
public class AnonymousInnerClassTest
{
public static void main(String[] args)
{
TalkingClock clock = new TalkingClock();
clock.start(1000, true);
// laisse le programme fonctionner jusqu’à ce que l’utilisateur
// clique sur "OK"
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
/**
Une horloge qui affiche l’heure à intervalles réguliers.
*/
class TalkingClock
{
/**
Démarre l’horloge.
@param interval l’intervalle entre les messages
Livre Java .book Page 272 Jeudi, 25. novembre 2004 3:04 15
272
Au cœur de Java 2 - Notions fondamentales
(en millièmes de seconde)
@param beep true si l’horloge doit émettre un bip
*/
public void start(int interval, final boolean beep)
{
ActionListener listener = new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
Date now = new Date();
System.out.println("At the tone, the time is " + now);
if (beep) Toolkit.getDefaultToolkit().beep();
}
};
Timer t = new Timer(interval, listener);
t.start();
}
}
Classes internes statiques
On désire parfois utiliser une classe interne pour cacher simplement une classe dans une autre, sans
avoir besoin de fournir à la classe interne une référence à un objet de la classe externe. A cette fin, la
classe interne est déclarée static.
Voici un exemple typique qui montre les raisons d’un tel choix. Prenons le calcul des valeurs minimale et maximale d’un tableau. Bien entendu, on écrit normalement une méthode pour calculer
le minimum et une autre pour calculer le maximum. Lorsque ces deux méthodes sont appelées, le
tableau est parcouru deux fois. Il serait plus efficace de parcourir le tableau une seule fois et de calculer
simultanément les deux valeurs :
double min = Double.MAX_VALUE;
double max = Double.MIN_VALUE;
for (double v : values)
{
if (min > v) min = v;
if (max < v) max = v;
}
Cependant, la méthode doit renvoyer deux nombres. Pour ce faire, nous déclarons une classe Pair
qui stocke deux valeurs numériques :
class Pair
{
public Pair(double f, double s)
{
first = f;
second = s;
}
public double getFirst() { return first; }
public double getSecond() { return second; }
private double first;
private double second;
}
Livre Java .book Page 273 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
273
La fonction minmax peut alors renvoyer un objet de type Pair :
class ArrayAlg
{
public static Pair minmax(double[] values)
{
. . .
return new Pair(min, max);
}
}
L’appelant de la fonction utilise ensuite les méthodes getFirst et getSecond pour récupérer les
réponses :
Pair p = ArrayAlg.minmax(d);
System.out.println("min = " + p.getFirst());
System.out.println("max = " + p.getSecond());
Bien sûr, le nom Pair est un terme très courant et, dans un grand projet, il se peut qu’un autre
programmeur ait eu la même idée brillante, mais sa classe Pair contient une paire de chaînes de
caractères. Ce conflit potentiel de noms peut être évité en faisant de Pair une classe interne publique
dans ArrayAlg. La classe sera alors connue du public sous le nom ArrayAlg.Pair :
ArrayAlg.Pair p = ArrayAlg.minmax(d);
Quoi qu’il en soit, et contrairement aux autres classes internes employées dans les exemples précédents, nous ne voulons pas avoir une référence à un autre objet au sein d’un objet Pair. Cette référence
peut être supprimée en déclarant la classe interne comme static :
class ArrayAlg
{
public static class Pair
{
. . .
}
. . .
}
Seules les classes internes peuvent évidemment être déclarées static. Une classe interne static
ressemble exactement à n’importe quelle autre classe interne, avec cette différence, cependant,
qu’un objet d’une classe interne statique ne possède pas de référence à l’objet externe qui l’a créé.
Dans notre exemple, nous devons utiliser une classe interne statique, car l’objet de la classe interne
est construit dans une méthode statique :
public static Pair minmax(double[] d)
{
. . .
return new Pair(min, max);
}
Si Pair n’avait pas été déclaré static, le compilateur aurait indiqué qu’aucun objet implicite de
type ArrayAlg n’était disponible pour initialiser l’objet de la classe interne.
INFO
On utilise une classe interne statique lorsque la classe interne n’a pas besoin d’accéder à un objet de la classe externe.
On emploie aussi le terme classe imbriquée pour désigner les classes internes statiques.
Livre Java .book Page 274 Jeudi, 25. novembre 2004 3:04 15
274
Au cœur de Java 2 - Notions fondamentales
INFO
Les classes internes qui sont déclarées dans une interface sont automatiquement statiques et publiques.
L’Exemple 6.6 contient le code source complet de la classe ArrayAlg et de la classe imbriquée Pair.
Exemple 6.6 : StaticInnerClassTest.java
public class StaticInnerClassTest
{
public static void main(String[] args)
{
double[] d = new double[20];
for (int i = 0; i < d.length; i++)
d[i] = 100 * Math.random();
ArrayAlg.Pair p = ArrayAlg.minmax(d);
System.out.println("min = " + p.getFirst());
System.out.println("max = " + p.getSecond());
}
}
class ArrayAlg
{
/**
Une paire de nombres à virgule flottante
*/
public static class Pair
{
/**
Construit une paire à partir de
deux nombres à virgule flottante
@param f Le premier nombre
@param s Le second nombre
*/
public Pair(double f, double s)
{
first = f;
second = s;
}
/**
Renvoie le premier nombre de la paire
@return le premier nombre
*/
public double getFirst()
{
return first;
}
/**
Renvoie le second nombre de la paire
@return le second nombre
*/
public double getSecond()
{
return second;
}
Livre Java .book Page 275 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
275
private double first;
private double second;
}
/**
Calcule à la fois le minimum et le maximum d’un tableau
@param values Un tableau de nombres à virgule flottante
@return une paire dont le premier élément est le minimum et
dont le second élément est le maximum
*/
public static Pair minmax(double[] values)
{
double min = Double.MAX_VALUE;
double max = Double.MIN_VALUE;
for (double v : values)
{
if (min > v) min = v;
if (max < v) max = v;
}
return new Pair(min, max);
}
}
Proxies
Dans cette dernière section du chapitre, nous allons étudier les proxies, une fonctionnalité devenue
disponible avec la version 1.3 du JDK. Vous utilisez un proxy pour créer, au moment de l’exécution,
de nouvelles classes qui implémentent un jeu donné d’interfaces. Les proxies ne sont nécessaires que
si vous ne savez pas, au moment de la compilation, quelles interfaces vous devez implémenter. Cette
situation se produit rarement pour les programmeurs d’application. Cependant, pour certaines applications système, la souplesse que procurent les proxies peut avoir une importance capitale. Grâce
aux proxies, vous pouvez souvent éviter la génération mécanique et la compilation de code stub.
INFO
Les classes "stub" sont utilisées dans plusieurs situations spécialisées. Lorsque vous utilisiez l’invocation de méthode
distante (RMI), un utilitaire spécial appelé rmic produisait des classes stub que vous deviez ajouter à votre
programme (voir le Chapitre 5 du Volume 2 pour plus d’informations au sujet de RMI). Par ailleurs, lors de l’emploi
de BeanBox, des classes stub étaient produites et compilées à la volée lors de la connexion de beans à d’autres beans
(voir le Chapitre 8 du Volume 2 pour plus d’informations sur les beans Java). Depuis le JDK 5.0, la fonction de proxy
permet de générer les stubs RMI sans avoir à exécuter un utilitaire.
Supposons que vous ayez un tableau d’objets Class représentant des interfaces (ne contenant éventuellement qu’une seule interface), dont vous pouvez ne pas connaître la nature exacte au moment de
la compilation. Vous voulez alors construire un objet d’une classe qui implémente ces interfaces.
C’est là un problème ardu. Si un objet Class représente une classe réelle, vous pouvez simplement
utiliser la méthode newInstance ou la réflexion pour trouver un constructeur de cette classe. Mais
vous ne pouvez pas instancier une interface. Et vous ne pouvez pas non plus définir de nouvelles
classes dans un programme qui s’exécute.
Livre Java .book Page 276 Jeudi, 25. novembre 2004 3:04 15
276
Au cœur de Java 2 - Notions fondamentales
Pour résoudre ce problème, certains programmes — tels que la BeanBox dans les toutes premières
versions du kit de développement de Bean — généraient du code, le plaçaient dans un fichier, invoquaient le compilateur puis chargeaient le fichier de classe résultant. Cette procédure est lente, bien
entendu, et demande aussi le déploiement du compilateur avec le programme. Le mécanisme de
proxy est une meilleure solution. La classe proxy peut créer des classes entièrement nouvelles au
moment de l’exécution. Une telle classe proxy implémente les interfaces que vous spécifiez. En
particulier, elle possède les méthodes suivantes :
m
toutes les méthodes requises par les interfaces spécifiées ;
m
toutes les méthodes définies dans la classe Object (toString, equals, etc.).
Néanmoins, vous ne pouvez pas définir de nouveau code pour ces méthodes au moment de
l’exécution. A la place, vous devez fournir un gestionnaire d’invocation. Ce gestionnaire est un
objet de toute classe implémentant l’interface InvocationHandler. Cette interface possède une seule
méthode :
Object invoke(Object proxy, Method method, Object[] args)
Chaque fois qu’une méthode est appelée sur l’objet proxy, la méthode invoke du gestionnaire
d’invocation est appelée, avec l’objet Method et les paramètres de l’appel original. Le gestionnaire d’invocation doit alors trouver comment gérer l’appel.
Pour créer un objet proxy, vous appelez la méthode newProxyInstance de la classe Proxy. Cette
méthode a trois paramètres :
m
Un chargeur de classe. Dans le cadre du modèle de sécurité Java, il est possible d’utiliser différents chargeurs de classe pour les classes système, les classes qui sont téléchargées à partir
d’Internet, etc. Pour l’instant, nous spécifierons null pour utiliser le chargeur de classe par
défaut.
m
Un tableau d’objets Class, un pour chaque interface à implémenter.
m
Un gestionnaire d’invocation.
Deux questions restent en suspens. Comment définissons-nous le gestionnaire ? Et que pouvonsnous faire de l’objet proxy résultant ? Les réponses dépendent évidemment du problème que nous
voulons résoudre avec le mécanisme de proxy. Les proxies peuvent être employés dans bien des cas,
par exemple :
m
le routage d’appels de méthode vers des serveurs distants ;
m
l’association d’événements de l’interface utilisateur avec des actions, dans un programme qui
s’exécute ;
m
la trace des appels de méthode à des fins de débogage.
Dans notre exemple de programme, nous allons utiliser les proxies et les gestionnaires d’invocation pour tracer les appels de méthode. Nous définissons une classe enveloppe TraceHandler qui
stocke un objet enveloppé (wrapped). Sa méthode invoke affiche simplement le nom et les paramètres de la méthode à appeler, puis appelle la méthode avec l’objet enveloppé en tant que paramètre
implicite :
class TraceHandler implements InvocationHandler
{
public TraceHandler(Object t)
Livre Java .book Page 277 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
277
{
target = t;
}
public Object invoke(Object proxy, Method m, Object[] args)
throws Throwable
{
// afficher le nom et les paramètres de la méthode
. . .
// invoquer la méthode
return m.invoke(target, args);
}
private Object target;
}
Voici comment vous construisez un objet proxy qui déclenche l’activité de trace chaque fois que
l’une de ses méthodes est appelée :
Object value = . . .;
// construire l’enveloppe (wrapper)
InvocationHandler handler = new TraceHandler(value);
// construire le proxy pour toutes les interfaces
Class[] interfaces = value.getClass().getInterfaces();
Object proxy = Proxy.newProxyInstance(null, interfaces, handler);
Maintenant, chaque fois qu’une méthode sera appelée sur le proxy, le nom de la méthode et ses
paramètres seront affichés, puis la méthode sera invoquée sur value.
Dans le programme de l’Exemple 6.7, les objets proxy sont employés pour réaliser la trace d’une
recherche binaire. Un tableau est rempli avec les valeurs entières de proxies de 1 à 1 000. Puis la
méthode binarySearch de la classe Arrays est invoquée pour rechercher un entier aléatoire dans le
tableau. Enfin, l’élément correspondant s’affiche :
Object[] elements = new Object[1000];
// remplir les éléments avec des valeurs de proxies de 1 à 1000
for (int i = 0; i < elements.length; i++)
{
Integer value = i + 1;
elements[i] = . . .;
}
// construire un entier aléatoire
Integer key = new Random().nextInt(elements.length) + 1;
// rechercher la clé (key)
int result = Arrays.binarySearch(elements, key);
// afficher la correspondance si trouvée
if (result >= 0)
System.out.println(elements[result]);
La classe Integer implémente l’interface Comparable. Les objets proxy appartiennent à une classe
qui est définie au moment de l’exécution (son nom est du style $Proxy0). Cette classe implémente
également l’interface Comparable. Sa méthode compareTo appelle la méthode invoke du gestionnaire de l’objet proxy.
Livre Java .book Page 278 Jeudi, 25. novembre 2004 3:04 15
278
Au cœur de Java 2 - Notions fondamentales
INFO
Comme vous l’avez vu plus tôt dans ce chapitre, depuis le JDK 5.0 la classe Integer implémente en fait Comparable<Integer>. Toutefois, au moment de l’exécution, tous les types génériques sont effacés et le proxy est construit
avec l’objet de classe, pour la classe brute Comparable.
La méthode binarySearch réalise des appels du style :
if (elements[i].compareTo(key) < 0) . . .
Puisque nous avons complété le tableau avec des objets proxy, les appels à compareTo appellent la
méthode invoke de la classe TraceHandler. Cette méthode affiche le nom de la méthode et ses
paramètres, puis invoque compareTo sur l’objet Integer enveloppé.
Enfin, à la fin du programme, nous appelons :
System.out.println(elements[result]);
La méthode println appelle toString sur l’objet proxy, et cet appel est aussi redirigé vers le
gestionnaire d’invocation.
Voici la trace complète d’une exécution du programme :
500.compareTo(288)
250.compareTo(288)
375.compareTo(288)
312.compareTo(288)
281.compareTo(288)
296.compareTo(288)
288.compareTo(288)
288.toString()
Remarquez l’algorithme de recherche dichotomique qui coupe en deux l’intervalle de recherche à
chaque étape.
Exemple 6.7 : ProxyTest.java
import java.lang.reflect.*;
import java.util.*;
public class ProxyTest
{
public static void main(String[] args)
{
Object[] elements = new Object[1000];
// remplir les éléments avec des proxies de 1 à 1000
for (int i = 0; i < elements.length; i++)
{
Integer value = i + 1;
Class[] interfaces = value.getClass().getInterfaces();
InvocationHandler handler = new TraceHandler(value);
Object proxy = Proxy.newProxyInstance(null,
interfaces, handler);
elements[i] = proxy;
}
// construit un entier aléatoire
Integer key = new Random().nextInt(elements.length) + 1;
Livre Java .book Page 279 Jeudi, 25. novembre 2004 3:04 15
Chapitre 6
Interfaces et classes internes
279
// recherche la clé
int result = Arrays.binarySearch(elements, key);
// affiche la correspondance le cas échéant
if (result >= 0) System.out.println(elements[result]);
}
}
/**
Un gestionnaire d’invocation qui affiche le nom et les paramètres
de la méthode, puis appelle la méthode initiale
*/
class TraceHandler implements InvocationHandler
{
/**
Construit un TraceHandler
@param t le paramètre implicite de l’appel de méthode
*/
public TraceHandler(Object t)
{
target = t;
}
public Object invoke(Object proxy, Method m, Object[] args)
throws Throwable
{
// affiche l’argument implicite
System.out.print(target);
// affiche le nom de la méthode
System.out.print("." + m.getName() + "(");
// affiche les arguments explicites
if (args != null)
{
for (int i = 0; i < args.length; i++)
{
System.out.print(args[i]);
if (i < args.length - 1)
System.out.print(", ");
}
}
System.out.println(")");
// appelle la méthode réelle
return m.invoke(target, args);
}
private Object target;
}
Propriétés des classes proxy
Maintenant que vous avez vu les classes proxy à l’œuvre, nous allons étudier certaines de leurs
propriétés. Souvenez-vous que les classes proxy sont créées à la volée, dans un programme en cours
d’exécution. Cependant, une fois créées, ce sont des classes régulières, comme n’importe quelle
autre classe dans la machine virtuelle.
Livre Java .book Page 280 Jeudi, 25. novembre 2004 3:04 15
280
Au cœur de Java 2 - Notions fondamentales
Toutes les classes proxy étendent la classe Proxy. Une classe proxy n’a qu’une variable d’instance
— le gestionnaire d’invocation qui est défini dans la superclasse Proxy. Toutes les données supplémentaires nécessaires pour exécuter les tâches des objets proxy doivent être stockées dans le gestionnaire d’invocation. Par exemple, lorsque les objets Comparable ont été transformés en proxies dans
le programme de l’Exemple 6.7, la classe TraceHandler a enveloppé les objets réels.
Toutes les classes proxy remplacent les méthodes toString, equals et hashCode de la classe
Object. Comme toutes les méthodes proxy, ces méthodes appellent simplement invoke sur le
gestionnaire d’invocation. Les autres méthodes de la classe Object (comme clone et getClass) ne
sont pas redéfinies.
Les noms des classes proxy ne sont pas définies. La classe Proxy dans la machine virtuelle de Sun
génère des noms de classes commençant par la chaîne $Proxy.
Il n’y a qu’une classe proxy pour un chargeur de classe et un jeu ordonné d’interfaces particuliers.
C’est-à-dire que si vous appelez deux fois la méthode newProxyInstance avec le même chargeur de
classe et le même tableau d’interface, vous obtenez deux objets de la même classe. Vous pouvez
aussi obtenir cette classe à l’aide de la méthode getProxyClass :
Class proxyClass = Proxy.getProxyClass(null, interfaces);
Une classe proxy est toujours public et final. Si toutes les interfaces qu’implémente la classe
proxy sont public, alors la classe proxy n’appartient pas à un package particulier. Sinon, toutes les
interfaces non publiques doivent appartenir au même package, et la classe proxy appartient alors
aussi à ce package.
Vous pouvez déterminer si un objet Class particulier représente une classe proxy en appelant la
méthode isProxyClass de la classe Proxy.
java.lang.reflect.InvocationHandler 1.3
•
Object invoke(Object proxy, Method method, Object[] args)
Définissez cette méthode pour qu’elle contienne l’action que vous voulez exécuter chaque fois
qu’une méthode a été invoquée sur l’objet proxy.
java.lang.reflect.Proxy 1.3
•
static Class getProxyClass(ClassLoader loader, Class[] interfaces)
Renvoie la classe proxy qui implémente les interfaces données.
•
static Object newProxyInstance(ClassLoader loader, Class[] interfaces,
InvocationHandler handler)
Construit une nouvelle instance de la classe proxy qui implémente les interfaces données. Toutes
les méthodes appellent la méthode invoke de l’objet gestionnaire donné.
•
static boolean isProxyClass(Class c)
Renvoie true si c est une classe proxy.
Cela clôt notre dernier chapitre sur les bases du langage de programmation Java. Les interfaces et les
classes internes sont des concepts que vous rencontrerez fréquemment. Comme nous l’avons
mentionné toutefois, les proxies constituent une technique pointue principalement destinée aux
concepteurs d’outils, et non aux programmeurs d’application. Vous êtes maintenant parés à aborder
l’apprentissage des techniques de programmation graphique et les interfaces utilisateur, qui
commence au Chapitre 7.
Livre Java .book Page 281 Jeudi, 25. novembre 2004 3:04 15
7
Programmation graphique
Au sommaire de ce chapitre
✔ Introduction à Swing
✔ Création d’un cadre
✔ Positionnement d’un cadre
✔ Affichage des informations dans un panneau
✔ Formes 2D
✔ Couleurs
✔ Texte et polices
✔ Images
Vous avez étudié jusqu’à présent l’écriture de programmes qui n’acceptaient que des caractères
tapés au clavier, les traitaient et affichaient le résultat dans une console. De nos jours, ce n’est pas
ce que désirent la plupart des utilisateurs. Les programmes modernes et les pages Web ne fonctionnent pas de cette manière. Ce chapitre vous ouvrira la voie qui permet d’écrire des programmes utilisant une interface utilisateur graphique (GUI, Graphic User Interface). Vous apprendrez
en particulier à écrire des programmes qui permettent de spécifier la taille et la position des fenêtres, d’y afficher du texte avec diverses polices de caractères, de dessiner des images, et ainsi de
suite. Vous obtiendrez ainsi un éventail de compétences que nous mettrons en œuvre dans les
prochains chapitres.
Les deux chapitres suivants vous montreront comment gérer des événements — tels que les
frappes au clavier et les clics de la souris — et comment ajouter des éléments d’interfaces :
menus, boutons, etc. Après avoir lu ces trois chapitres, vous saurez écrire des programmes
graphiques autonomes. Le Chapitre 10 abordera la programmation des applets qui emploient ces
caractéristiques et qui sont intégrés dans des pages Web. Des techniques plus sophistiquées de
programmation graphique sont étudiées dans Au cœur de Java 2 Volume 2, paru aux éditions
CampusPress, 2003.
Livre Java .book Page 282 Jeudi, 25. novembre 2004 3:04 15
282
Au cœur de Java 2 - Notions fondamentales
Introduction à Swing
Lors de l’introduction de Java 1.0, le programme contenait une bibliothèque de classe que Sun appelait Abstract Window Toolkit (AWT) pour la programmation de base de l’interface graphique utilisateur (GUI). La manière dont la bibliothèque AWT de base gère les éléments de l’interface utilisateur
se fait par la délégation de leur création et de leur comportement à la boîte à outils native du GUI sur
chaque plate-forme cible (Windows, Solaris, Macintosh, etc.). Si vous avez par exemple utilisé la
version originale d’AWT pour placer une boîte de texte sur une fenêtre Java, une case de texte "pair"
sous-jacente a géré la saisie du texte. Le programme qui en résulte pourrait alors, en théorie,
s’exécuter sur l’une de ces plates-formes, avec "l’aspect" de la plate-forme cible, d’où le slogan de
Sun, "Ecrire une fois, exécuter partout".
L’approche fondée sur les pairs fonctionnait bien pour les applications simples, mais il est rapidement devenu évident qu’il était très difficile d’écrire une bibliothèque graphique portable de haute
qualité qui dépendait des éléments d’une interface utilisateur native. L’interface utilisateur comme
les menus, les barres de défilement et les champs de texte peuvent avoir des différences subtiles de
comportement sur différentes plates-formes. Il était donc difficile d’offrir aux utilisateurs une expérience cohérente et prévisible. De plus, certains environnements graphiques (comme X11/Motif) ne
disposent pas d’une large collection de composants d’interface utilisateur comme l’ont Windows ou
Macintosh. Ceci restreint ensuite une bibliothèque portable fondée sur des pairs à adopter l’approche
du "plus petit dénominateur commun". En conséquence, les applications GUI élaborées avec AWT
n’étaient pas aussi jolies que les applications Windows ou Macintosh natives et n’avaient pas non
plus le type de fonctionnalité que les utilisateurs de ces plates-formes attendaient. Plus triste encore,
il existait différents bogues dans la bibliothèque de l’interface utilisateur AWT sur les différentes
plates-formes. Les développeurs se sont plaints qu’ils devaient tester leurs applications sur chaque
plate-forme, une pratique qui s’est appelée, avec un peu de dérision, "Ecrire une fois, déboguer
partout".
En 1996, Netscape a créé une bibliothèque de GUI dénommée IFC (Internet Foundation Classes) qui
utilisait une approche totalement différente. Les éléments de l’interface utilisateur, comme les
boutons, les menus, etc., étaient dessinés sur des fenêtres vierges. La seule fonctionnalité pair nécessitait une méthode pour faire apparaître les fenêtres et dessiner dedans. Ainsi, les éléments IFC de
Netscape avaient le même aspect et se comportaient de la même manière, quelle que soit la plateforme d’exécution du programme. Sun a collaboré avec Netscape pour parfaire cette approche, avec
pour résultat une bibliothèque d’interfaces utilisateur portant le nom de code "Swing" (quelquefois
appelé "Swing set").
Comme l’a dit Duke Ellington, "Tout ça n’est rien si je n’ai pas le swing." Et Swing est maintenant
le nom officiel du kit de développement d’interface graphique léger. Swing fait partie des classes
JFC (Java Foundation Classes). L’ensemble des JFC est vaste et ne se limite pas à Swing. Elles
incluent non seulement les composants Swing, mais également des API d’accessibilité, de dessin 2D
et de fonctionnalités de glisser-déplacer (ou drag and drop).
INFO
Swing ne remplace pas complètement AWT, il est fondé sur l’architecture AWT. Swing fournit simplement des
composants d’interface plus performants. Vous utilisez l’architecture de base d’AWT, en particulier la gestion des
événements, lorsque vous écrivez un programme Swing. A partir de maintenant, nous emploierons le terme "Swing"
Livre Java .book Page 283 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
283
pour désigner les classes allégées de l’interface utilisateur "dessinée", et "AWT" pour désigner les mécanismes sousjacents du kit de fenêtrage (tels que la gestion d’événements).
Un composant lourd est un composant utilisant les ressources du système d’exploitation hôte ; un composant léger
est un composant n’utilisant pas ces ressources.
Bien entendu, les éléments d’interface Swing seront un peu plus lents à s’afficher sur l’écran que les
composants lourds employés par AWT. Précisons que cette différence de vitesse ne pose pas de
problème sur les machines récentes. En revanche, il existe d’excellentes raisons d’opter pour
Swing :
m
Swing propose un ensemble d’éléments d’interface plus étendu et plus pratique.
m
Swing dépend peu de la plate-forme d’exécution ; en conséquence, il est moins sensible aux
bogues spécifiques d’une plate-forme.
m
Swing procure une bonne expérience à l’utilisateur qui travaille sur plusieurs plates-formes.
Tout cela signifie que Swing remplit — au moins potentiellement — les conditions de la promesse
faite initialement par Sun : "Un même programme s’exécute partout".
Cependant, le troisième avantage cité représente également un recul : si les éléments de l’interface utilisateur se ressemblent sur toutes les plates-formes, leur aspect ("look and feel") est
quand même différent de celui des contrôles natifs, et les utilisateurs vont devoir s’adapter à cette
nouvelle présentation.
Swing résout ce problème d’une manière très élégante. Les programmeurs d’une application Swing
peuvent donner à leur interface un look and feel spécifique. Les Figures 7.1 et 7.2 montrent le même
programme en cours d’exécution, l’un sous Windows, l’autre sous Motif (pour des raisons de copyright, le look and feel Windows n’est disponible que pour les programmes Java exécutés sur des
plates-formes Windows).
Figure 7.1
Application Swing
avec le look and feel
de Windows.
Livre Java .book Page 284 Jeudi, 25. novembre 2004 3:04 15
284
Au cœur de Java 2 - Notions fondamentales
INFO
Bien que cela dépasse le cadre de cet ouvrage, il est possible au programmeur Java d’améliorer un look and feel existant ou d’en concevoir un qui soit entièrement nouveau. C’est un travail fastidieux qui demande au programmeur
de spécifier la manière dont les divers composants Swing seront dessinés. Certains développeurs ont déjà accompli
cette tâche en portant Java sur des plates-formes non traditionnelles (telles que des terminaux de borne ou des ordinateurs de poche). Consultez le site http://javootoo.com pour obtenir une collection d’implémentation "look and
feel" intéressante.
Le JDK 5.0 introduit un nouveau look and feel, appelé Synth, qui facilite ce processus. Synth permet de définir un
nouveau look and feel en fournissant des fichiers image et des descripteurs XML, sans effectuer aucune programmation. Sun a développé un look and feel indépendant de la plate-forme, baptisé "Metal", jusqu’à ce que les spécialistes du marketing le renomment "Java look and feel". Or la plupart des programmeurs continuent à utiliser le terme
"Metal", et c’est ce que nous ferons dans ce livre.
Figure 7.2
Application Swing avec
le look and feel de Motif.
Certains ont fait la critique que Metal était un peu indigeste, et son aspect a été modernisé pour
la version 5.0 (voir Figure 7.3). Sachez que l’aspect Metal prend en charge plusieurs thèmes, des
variations mineures des couleurs et des polices. Le thème par défaut s’appelle "Ocean". Dans cet
ouvrage, nous utiliserons Swing et "Metal" pour tous les programmes graphiques, avec le thème
Ocean.
INFO
La plus grande de la programmation de l’interface utilisateur Java s’effectue aujourd’hui en Swing, à une notable
exception près. L’environnement de développement intégré Eclipse utilise une boîte à outils graphique appelée SWT
qui est identique à AWT, faisant concorder des composants natifs sur diverses plates-formes. Vous trouverez des articles
décrivant SWT à l’adresse http://www.eclipse.org/articles/.
Livre Java .book Page 285 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
285
Figure 7.3
Le look and feel "Metal"
de Swing.
Nous devons néanmoins vous donner un avertissement. Si vous avez déjà programmé des applications pour Windows avec Visual Basic ou C#, vous connaissez la simplicité d’emploi des outils
graphiques et des éditeurs de ressources de ces produits. Ces logiciels permettent de concevoir
l’aspect d’une application et génèrent une bonne partie (ou la totalité) du code de l’interface. Il existe
quelques outils de développement d’interface pour Java, mais ils ne sont pas aussi élaborés que les
outils correspondants destinés à Windows. Quoi qu’il en soit, pour comprendre parfaitement la
programmation d’une interface graphique, vous devez savoir comment la construire manuellement.
Pour cela, bien entendu, il faut écrire beaucoup de code.
Création d’un cadre
En Java, une fenêtre de haut niveau — c’est-à-dire une fenêtre qui n’est pas contenue dans une autre
fenêtre — est appelée frame (cadre ou fenêtre d’encadrement). Pour représenter ce niveau supérieur,
la bibliothèque AWT possède une classe nommée Frame. La version Swing de cette classe est baptisée JFrame, qui étend la classe Frame et désigne l’un des rares composants Swing qui ne soient pas
dessinés sur un canevas (grille). Les éléments de décoration (boutons, barre de titre, icônes, etc.) ne
sont pas dessinés par Swing, mais par le système de fenêtrage de l’utilisateur.
ATTENTION
La plupart des classes de composant Swing commencent par la lettre "J" : JButton, JFrame, etc. Ce sont des classes
comme Button et Frame, mais il s’agit de composants AWT. Si vous omettez par inadvertance la lettre "J", votre
programme peut toujours se compiler et s’exécuter, mais le mélange de Swing et de composants AWT peut amener
à des incohérences visuelles et de comportement.
Livre Java .book Page 286 Jeudi, 25. novembre 2004 3:04 15
286
Au cœur de Java 2 - Notions fondamentales
Les cadres sont des exemples de conteneurs. Cela signifie qu’ils peuvent contenir d’autres composants d’interface tels que des boutons et des champs de texte. Nous allons maintenant étudier les
méthodes employées le plus fréquemment lorsque nous travaillons avec un composant JFrame.
L’Exemple 7.1 présente un programme simple qui affiche une fenêtre vide sur l’écran, comme le
montre la Figure 7.4.
Figure 7.4
Le plus simple
des cadres visibles.
Exemple 7.1 : SimpleFrameTest.java
import javax.swing.*;
public class SimpleFrameTest
{
public static void main(String[] args)
{
SimpleFrame frame = new SimpleFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
class SimpleFrame extends JFrame
{
public SimpleFrame()
{
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
Examinons ce programme ligne par ligne.
Les classes Swing se trouvent dans le package javax.swing. Le terme javax désigne un package
d’extension (pour le distinguer d’un package standard). Les classes Swing constituent en fait une
extension de Java 1.1 mais, comme elles n’ont pas été intégrées dans la hiérarchie standard, il est
possible de les charger dans un navigateur compatible avec Java 1.1 (le gestionnaire de sécurité du
navigateur n’autorise pas l’ajout de packages commençant par "java."). Sur une plate-forme Java 2,
le package Swing n’est plus une extension, mais fait partie de la hiérarchie standard. Toute implémentation de Java qui se veut compatible avec la norme Java 2 doit fournir les classes Swing.
Livre Java .book Page 287 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
287
Quoi qu’il en soit, le terme javax a été conservé pour des raisons de compatibilité avec le code
Java 1.1 (en réalité, le package Swing s’appelait initialement com.sun.java.swing, puis s’est
brièvement nommé java.awt.swing dans les premières versions bêta de Java 2, avant de redevenir com.sun.java.swing dans les dernières versions bêta ; après de nombreuses protestations
de la part des programmeurs Java, le nom javax.swing a été définitivement adopté).
Par défaut, un cadre a une taille assez peu utile de 0 × 0 pixels. Nous définissons une sous-classe
SimpleFrame dont le constructeur définit la taille à 300 × 200 pixels. Dans la méthode main de la
classe SimpleFrameTest, nous commençons par construire un objet SimpleFrame.
Nous indiquons ensuite ce qui doit se passer lorsque l’utilisateur ferme ce cadre. Dans ce cas précis,
le programme doit sortir. Utilisez pour cela l’instruction :
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Dans d’autres programmes comprenant plusieurs cadres, vous ne souhaiterez pas la sortie du
programme si l’utilisateur ferme seulement l’un des cadres. Par défaut, un cadre est masqué lorsque
l’utilisateur le ferme, mais le programme ne se termine pas pour autant.
Le simple fait de construire un cadre ne l’affiche pas automatiquement. Les cadres sont au départ
invisibles. Cela permet au programmeur d’y ajouter des composants avant l’affichage. Pour afficher
le cadre, la méthode main appelle la méthode setVisible du cadre.
La méthode main se termine ensuite. Remarquez que la sortie de main ne met pas fin à l’exécution
du programme, mais seulement à celle du thread principal. L’affichage du cadre active un thread
d’interface utilisateur qui maintient le programme actif.
INFO
Avant le JDK 5.0, il était possible d’utiliser la méthode show, dont la classe JFrame héritait dans la superclasse
Window. Cette dernière avait elle-même une superclasse Component qui disposait également d’une méthode show.
La méthode Component.show était une méthode dépréciée dans le JDK 1.2. Vous êtes censé appeler setVisible(true) pour afficher un composant. Néanmoins, jusqu’au JDK 1.4, la méthode Window.show n’était pas dépréciée. En fait, elle était assez utile, pour afficher la fenêtre et la faire apparaître au premier plan. Malheureusement,
cet avantage a disparu avec la politique de dépréciation, et le JDK 5.0 déprécie la méthode show également pour les
fenêtres.
Le résultat du programme est présenté à la Figure 7.4 : il s’agit d’un cadre parfaitement vide et sans
intérêt. La barre de titre et les éléments, tels que les boutons à droite, sont dessinés par le système
d’exploitation et non par la bibliothèque Swing. Si vous exécutez le même programme sous X
Window, les fioritures du cadre seront différentes. La bibliothèque Swing dessine tout à l’intérieur
du cadre. Dans ce programme, elle emplit simplement le cadre avec une couleur d’arrière-plan par
défaut.
INFO
Depuis le JDK 1.4, vous pouvez désactiver toutes les décorations de cadre en appelant frame.setUndecorated(true).
Livre Java .book Page 288 Jeudi, 25. novembre 2004 3:04 15
288
Au cœur de Java 2 - Notions fondamentales
INFO
Dans l’exemple précédent, nous avons écrit deux classes, une pour définir une classe cadre et une contenant une
méthode main qui crée et affiche un objet cadre. Vous verrez fréquemment des programmes dans lesquels la
méthode main est placée opportunément dans une classe adéquate, de la façon suivante :
class SimpleFrame extends JFrame
{
public static void main(String[] args)
{
SimpleFrame frame = new SimpleFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.show();
}
public SimpleFrame()
{
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
Utiliser la méthode main de la classe Frame pour le code qui lance le programme est plus simple en un sens. Vous
n’avez pas besoin d’introduire une autre classe auxiliaire. Cependant, nombre de programmeurs trouvent ce style de
code peu clair. Pour notre part, nous préférons séparer la classe qui lance le programme de celles qui définissent
l’interface utilisateur.
Positionnement d’un cadre
La classe JFrame ne fournit que peu de méthodes capables de modifier l’aspect d’un cadre.
Cependant, grâce à l’héritage, les diverses superclasses de JFrame proposent la plupart des
méthodes permettant d’agir sur la taille et la position d’un cadre. Les plus importantes sont les
suivantes :
m
La méthode dispose ferme la fenêtre et libère les ressources qui ont été employées lors de sa
création.
m
La méthode setIconImage reçoit un objet Image afin de l’employer comme icône lorsque la
fenêtre est réduite (le terme iconized est souvent utilisé dans la terminologie Java).
m
La méthode setTitle spécifie le texte affiché dans la barre de titre.
m
La méthode setResizable reçoit un paramètre booléen pour déterminer si la taille d’un cadre
peut être modifiée par l’utilisateur.
La Figure 7.5 illustre la chaîne d’héritage de la classe JFrame.
Comme l’indiquent les notes API, c’est généralement dans la classe Component (ancêtre de tous les
objets d’interface utilisateur graphique) ou dans la classe Window (superclasse du parent de la classe
Frame) que l’on recherche les méthodes permettant de modifier la taille et la position des cadres. Par
exemple, la méthode setLocation de la classe Component permet de repositionner un composant.
Si vous appelez :
setLocation(x, y)
Livre Java .book Page 289 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
289
le coin supérieur gauche du cadre est placé à x pixels du bord gauche et y pixels du sommet de
l’écran — (0, 0) représente le coin supérieur gauche de l’écran. De même, la méthode setBounds
de Component permet de modifier simultanément la taille et la position d’un composant (en particulier
JFrame), de la façon suivante :
setBounds(x, y, width, height)
Figure 7.5
La hiérarchie d’héritage
des classes JFrame et JPanel.
Object
Component
Container
JComponent
Window
JPanel
Frame
JFrame
INFO
Pour un cadre, les coordonnées de setLocation et setBounds sont relatives à l’écran. Pour d’autres composants,
placés dans un conteneur, les cordonnées sont relatives au conteneur, comme vous le verrez au Chapitre 9.
Livre Java .book Page 290 Jeudi, 25. novembre 2004 3:04 15
290
Au cœur de Java 2 - Notions fondamentales
N’oubliez pas que si vous ne spécifiez pas explicitement la taille d’un cadre, celui-ci aura par défaut
une largeur et une hauteur de 0 pixel. Pour simplifier notre programme, nous avons donné au cadre
une taille qui devrait être acceptée par la plupart des systèmes d’affichage. Cependant, dans une
application professionnelle, vous devez d’abord déterminer la résolution de l’écran afin d’adapter la
taille du cadre : une fenêtre qui peut sembler raisonnablement grande sur l’écran d’un portable aura
la taille d’un timbre-poste sur un écran en haute résolution. Nous verrons bientôt comment obtenir
(en pixels) les dimensions de l’écran du système. Cette information permettra ensuite de calculer la
taille optimale d’un cadre.
ASTUCE
Les notes API de cette section décrivent les méthodes les plus importantes permettant de donner le meilleur
aspect à une fenêtre (en fonction du système). Certaines de ces méthodes sont définies dans la classe JFrame.
D’autres sont héritées de diverses superclasses de JFrame. Vous devrez parfois consulter la documentation API
afin de savoir si des méthodes spécifiques sont disponibles. Malheureusement, cette recherche peut se révéler
fastidieuse avec la documentation du JDK. Pour les sous-classes, cette documentation ne décrit que les méthodes
surchargées. Par exemple, la méthode toFront peut être appliquée aux objets de type JFrame, mais elle n’est
pas décrite dans la classe JFrame, car il s’agit d’une méthode simplement héritée de la classe Window. Si vous
pensez qu’une méthode particulière devrait être disponible et qu’elle ne soit pas décrite dans la documentation
de la classe avec laquelle vous travaillez, consultez la documentation des méthodes disponibles dans les superclasses. La partie supérieure de chaque page de la documentation de l’API contient des liens hypertexte vers les
classes ancêtres ; de plus, vous trouverez une liste des méthodes héritées après la description des nouvelles
méthodes et des méthodes surchargées.
Pour vous donner une idée de ce que l’on peut faire avec une fenêtre, nous terminerons cette section
en vous proposant un programme de démonstration qui positionne un des cadres et lui donne les
caractéristiques suivantes :
m
Il occupe environ un quart de l’écran.
m
Il est centré au milieu de l’écran.
Par exemple, si la résolution de l’écran est de 800 × 600 pixels, notre cadre occupera
400 × 300 pixels et la position de son coin supérieur gauche sera (200, 150).
Pour connaître la taille de l’écran, procédez aux étapes suivantes. Appelez la méthode statique
getDefaultToolkit de la classe Toolkit pour récupérer l’objet Toolkit (la classe Toolkit est un
dépotoir pour diverses méthodes qui interfacent avec le système de fenêtrage natif). Appelez ensuite
la méthode getScreenSize, qui renvoie la taille de l’écran sous la forme d’un objet Dimension (un
objet d de type Dimension stocke simultanément une largeur et une hauteur dans des variables
d’instance publiques appelées respectivement width et height).
Voici le code :
Toolkit kit = Toolkit.getDefaultToolkit();
Dimension screenSize = kit.getScreenSize();
int screenWidth = screenSize.width;
int screenHeight = screenSize.height;
Livre Java .book Page 291 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
291
Nous fournissons également une icône à notre cadre. Comme la représentation des images dépend
aussi du système, nous employons la boîte à outils pour charger une image. Ensuite, nous affectons
l’image à l’icône du cadre :
Image img = kit.getImage("icon.gif");
setIconImage(img);
La position de l’icône dépendra du système d’exploitation. Sous Windows, par exemple, l’icône
s’affiche dans le coin supérieur gauche de la fenêtre, et elle apparaît également dans la liste des
tâches lorsque vous appuyez sur la combinaison de touches Alt+Tab.
L’Exemple 7.2 présente le programme complet. Lorsque vous exécuterez le programme, remarquez
l’icône "Core Java".
ASTUCE
Il est assez fréquent de définir le cadre principal d’un programme sur la taille maximale. Depuis le JDK 1.4, vous
pouvez maximiser un cadre en appelant :
frame.setExtendedState(Frame.MAXIMIZED_BOTH);
INFO
Si vous écrivez une application qui profite de l’affichage multiple, utilisez les classes GraphicsEnvironment et
GraphicsDevice pour obtenir les dimensions des écrans. Depuis le JDK 1.4, la classe GraphicsDevice vous
permet aussi d’exécuter votre application en mode plein écran.
Exemple 7.2 : CenteredFrameTest.java
/**import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class CenteredFrameTest
{
public static void main(String[] args)
{
CenteredFrame frame = new CenteredFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
class CenteredFrame extends JFrame
{
public CenteredFrame()
{
// extraire les dimensions de l’écran
Toolkit kit = Toolkit.getDefaultToolkit();
Dimension screenSize = kit.getScreenSize();
int screenHeight = screenSize.height;
int screenWidth = screenSize.width;
Livre Java .book Page 292 Jeudi, 25. novembre 2004 3:04 15
292
Au cœur de Java 2 - Notions fondamentales
// centrer le cadre au milieu de l’écran
setSize(screenWidth / 2, screenHeight / 2);
setLocation(screenWidth / 4, screenHeight / 4);
// définir l’icône et le titre du cadre
Image img = kit.getImage("icon.gif");
setIconImage(img);
setTitle("CenteredFrame");
}
}
java.awt.Component 1.0
•
boolean isVisible()
Renvoie true si le composant est visible. Les composants sont initialement visibles par défaut, à
l’exception des composants de haut niveau tels que JFrame.
•
void setVisible(boolean b)
Affiche ou cache le composant, selon la valeur de b (respectivement true ou false).
•
boolean isShowing()
Vérifie que le composant s’affiche sur l’écran. Pour cela, le composant doit être visible et son
éventuel conteneur doit être affiché.
•
boolean isEnabled()
Vérifie que le composant est activé. Un composant activé peut recevoir le focus du clavier. Les
composants sont initialement activés.
•
void setEnabled(boolean b)
Active ou désactive le composant.
•
Point getLocation() 1.1
Renvoie la position du coin supérieur gauche de ce composant, relativement au coin supérieur
gauche de son conteneur (un objet p de type Point encapsule des coordonnées x et y respectivement accessibles par p.x et p.y).
•
Point getLocationOnScreen() 1.1
Renvoie la position du coin supérieur gauche de ce composant, en coordonnées d’écran.
•
void setBounds(int x, int y, int width, int height) 1.1
Déplace et redimensionne le composant. La position du coin supérieur gauche est spécifiée par x
et y. La nouvelle taille est indiquée dans les paramètres width et height.
•
•
void setLocation(int x, int y) 1.1
void setLocation(Point p) 1.1
Déplace le composant. Les coordonnées x et y (ou p.x et p.y) sont relatives au conteneur si le
composant n’est pas un composant de haut niveau ; elles sont relatives à l’écran si le composant
est de haut niveau (par exemple, un cadre JFrame).
•
Dimension getSize() 1.1
Renvoie la taille actuelle du composant.
Livre Java .book Page 293 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
•
•
Programmation graphique
293
void setSize(int width, int height) 1.1
void setSize(Dimension d) 1.1
Modifie la taille du composant en lui attribuant la largeur et la hauteur spécifiées.
java.awt.Window 1.0
•
void toFront()
Affiche cette fenêtre par-dessus toutes les autres.
•
void toBack()
Place cette fenêtre au-dessous de la pile des fenêtres du bureau et réorganise les autres fenêtres
visibles.
java.awt.Frame 1.0
•
void setResizable(boolean b)
Détermine si l’utilisateur peut modifier la taille du cadre.
•
void setTitle(String s)
Attribue le texte de la chaîne s à la barre de titre du cadre.
•
void setIconImage(Image image)
Paramètres :
•
image
L’image qui servira d’icône à ce cadre.
void setUndecorated(boolean b) 1.4
Supprime les décorations de cadre si b vaut true.
•
boolean isUndecorated() 1.4
Renvoie true si ce cadre n’est pas décoré.
•
•
int getExtendedState() 1.4
void setExtendedState(int state) 1.4
Récupère ou définit l’état de la fenêtre. L’état est l’un de ceux-ci :
Frame.NORMAL
Frame.ICONIFIED
Frame.MAXIMIZED_HORIZ
Frame.MAXIMIZED_VERT
Frame.MAXIMIZED_BOTH
java.awt.Toolkit 1.0
•
static Toolkit getDefaultToolkit()
Renvoie la boîte à outils par défaut.
•
Dimension getScreenSize()
Renvoie la taille de l’écran.
•
Image getImage(String filename)
Charge une image à partir du fichier spécifié par filename.
Livre Java .book Page 294 Jeudi, 25. novembre 2004 3:04 15
294
Au cœur de Java 2 - Notions fondamentales
Affichage des informations dans un panneau
Nous allons voir maintenant comment afficher des informations à l’intérieur d’un cadre. Par
exemple, au lieu d’afficher "Not a Hello, World program" en mode texte dans une fenêtre de
console, comme nous l’avons fait au Chapitre 3, nous afficherons le message dans un cadre (voir
Figure 7.6).
Figure 7.6
Un programme
graphique simple.
Il est possible de dessiner directement le message dans le cadre, mais ce n’est pas considéré comme
une bonne technique de programmation. En Java, les cadres sont conçus pour être les conteneurs
d’autres composants (barre de menus et autres éléments d’interface). On dessine normalement sur
un composant panneau préalablement ajouté au cadre.
La structure de JFrame est étonnamment complexe, comme vous pouvez le constater en observant la
Figure 7.7. Vous voyez que JFrame possède quatre couches superposées. Nous n’avons pas à nous
préoccuper ici de la racine (JRoot), de la couche superposée (JLayeredPane) et de la vitre ; elles
sont nécessaires pour l’organisation de la barre de menus et du contenu, ainsi que pour implémenter
l’aspect (look and feel) du cadre. La partie qui intéresse les programmeurs Swing est la couche
contenu. Lorsque nous concevons un cadre, nous ajoutons les composants au contenu à l’aide
d’instructions comme celles-ci :
Container contentPane = frame.getContentPane();
Component c = . . .;
contentPane.add(c);
Jusqu’au JDK 1.4, la méthode add de la classe JFrame était définie de manière à déclencher une
exception avec le message "Do not use JFrame.add(). Use JFrame.getContentPane().add()
instead". Depuis le JDK 5.0, la méthode JFrame.add n’essaie plus de rééduquer les programmeurs
et appelle simplement add sur le volet de contenu.
Ainsi, depuis le JDK 5.0, vous pouvez simplement utiliser l’appel :
frame.add(c);
Dans notre cas, nous désirons uniquement ajouter au contenu un cadre sur lequel nous écrirons le
message. Les panneaux sont implémentés par la classe JPanel. Il s’agit d’éléments d’interface qui
offrent deux propriétés particulièrement utiles :
m
Ils possèdent une surface sur laquelle on peut dessiner.
m
Ils sont eux-mêmes des conteneurs.
Livre Java .book Page 295 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Figure 7.7
Programmation graphique
295
Title
La structure interne
d’un cadre JFrame.
frame
root pane
layered pane
menu bar (optional)
content pane
glass pane
Ainsi, les panneaux peuvent contenir d’autres composants d’interface, tels que des boutons, des
barres de défilement, etc.
Il est préférable d’avoir recours à l’héritage pour créer une nouvelle classe ; nous pouvons alors
surcharger ou ajouter des méthodes afin d’obtenir les fonctionnalités désirées.
En particulier, pour dessiner sur un panneau, nous devons :
m
définir une classe qui étend JPanel ;
m
surcharger la méthode paintComponent de cette classe.
La méthode paintComponent se trouve en fait dans JComponent — la superclasse de tous les
composants Swing qui ne sont pas des fenêtres. Elle reçoit un paramètre, de type Graphics. Un
objet Graphics mémorise une série de paramètres pour le dessin d’images et de texte, tels que la
police définie ou la couleur. Un dessin en Java doit passer par un objet Graphics. Il possède des
méthodes pour dessiner des motifs, des images et du texte.
INFO
Le paramètre Graphics est comparable à un contexte d’affichage de Windows ou à un contexte graphique en
programmation X11.
Livre Java .book Page 296 Jeudi, 25. novembre 2004 3:04 15
296
Au cœur de Java 2 - Notions fondamentales
Voici comment créer un panneau, sur lequel vous pourrez dessiner :
class MyPanel extends JPanel
{
public void paintComponent(Graphics g)
{
. . . // code de dessin
}
}
Chaque fois qu’une fenêtre doit être redessinée, quelle qu’en soit la raison, le gestionnaire d’événement envoie une notification au composant. Les méthodes paintComponent de tous les composants
sont alors exécutées.
N’appelez jamais directement la méthode paintComponent. Elle est appelée automatiquement
lorsqu’une portion de votre application doit être redessinée ; vous ne devez pas créer d’interférence.
Quelles sortes d’actions sont déclenchées par cette réponse automatique ? Par exemple, la fenêtre est
redessinée parce que l’utilisateur a modifié sa taille ou parce qu’il l’ouvre à nouveau après l’avoir
réduite dans la barre des tâches. De même, si l’utilisateur a ouvert, puis refermé une autre fenêtre audessus d’une fenêtre existante, cette dernière doit être redessinée, car son affichage a été perturbé (le
système graphique n’effectue pas de sauvegarde des pixels cachés). Bien entendu, lors de sa
première ouverture, une fenêtre doit exécuter le code qui indique comment, et où, les éléments
qu’elle contient sont affichés.
ASTUCE
Si vous devez forcer une fenêtre à se redessiner, appelez la méthode repaint plutôt que paintComponent. La
méthode repaint appelle paintComponent pour tous les composants de la fenêtre, en fournissant chaque fois un
objet Graphics approprié.
Comme vous avez pu le constater dans le fragment de code précédent, la méthode paintComponent
prend un seul paramètre de type Graphics. Les coordonnées et les dimensions appliquées à cet objet
sont exprimées en pixels. Les coordonnées (0, 0) représentent le coin supérieur gauche du composant
sur lequel vous dessinez.
L’affichage de texte est considéré comme une forme particulière de dessin. La classe Graphics
dispose d’une méthode drawString dont voici la syntaxe :
g.drawString(text, x, y)
Nous souhaitons dessiner la chaîne "Not a Hello, World program" à peu près au centre de la
fenêtre originale. Bien que nous ne sachions pas encore comment mesurer la taille de la chaîne, nous
la ferons commencer à la position (75, 100). Cela signifie que le premier caractère sera situé à
75 pixels du bord gauche et 100 pixels du bord supérieur de la fenêtre (en fait, c’est la ligne de base
du texte qui se situe à 100 pixels — nous verrons bientôt comment mesurer le texte). Notre méthode
paintComponent ressemble donc à ceci :
class NotHelloWorldPanel extends JPanel
{
public void paintComponent(Graphics g)
{
. . . // voir ci-après
Livre Java .book Page 297 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
297
g.drawString("Not a Hello, World program",
MESSAGE_X, MESSAGE_Y);
}
public static final int MESSAGE_X = 75;
public static final int MESSAGE_Y = 100;
}
Mais notre méthode paintComponent n’est pas complète. La classe NotHelloWorldPanel dérive de
la classe JPanel, qui a sa propre idée sur la manière de dessiner le panneau — en le remplissant avec
la couleur de fond. Pour s’assurer que la superclasse effectue sa part du travail, nous devons appeler
super.paintComponent avant de procéder à notre propre opération de dessin :
class NotHelloWorldPanel extends JPanel
{
public void paintComponent(Graphics g)
{
super.paintComponent(g);
. . . // code de dessin
}
}
L’Exemple 7.3 présente le code complet du programme. Si vous utilisez le JDK 1.4 ou version antérieure, n’oubliez pas de transformer l’appel add(panel) par getContentPane().add(panel).
Exemple 7.3 : NotHelloWorld.java
import javax.swing.*;
import java.awt.*;
public class NotHelloWorld
{
public static void main(String[] args)
{
NotHelloWorldFrame frame = new NotHelloWorldFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre qui contient un panneau de message
*/
class NotHelloWorldFrame extends JFrame
{
public NotHelloWorldFrame()
{
setTitle("NotHelloWorld");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter le panneau au cadre
NotHelloWorldPanel panel = new NotHelloWorldPanel();
add(panel);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
Livre Java .book Page 298 Jeudi, 25. novembre 2004 3:04 15
298
Au cœur de Java 2 - Notions fondamentales
/**
Un panneau qui affiche un message.
*/
class NotHelloWorldPanel extends JPanel
{
public void paintComponent(Graphics g)
{
super.paintComponent(g);
g.drawString("Not a Hello, World program",
MESSAGE_X, MESSAGE_Y);
}
public static final int MESSAGE_X = 75;
public static final int MESSAGE_Y = 100;
}
javax.swing.JFrame 1.2
•
Container getContentPane()
Renvoie l’objet du panneau de contenu pour JFrame.
•
void add(Component c)
Ajoute le composant donné au volet de ce cadre (avant le JDK 5.0, cette méthode déclenchait
une exception).
java.awt.Component 1.0
•
void repaint()
Provoque un redessin du composant "dès que possible".
•
public void repaint(int x, int y, int width, int height)
Provoque un redessin d’une partie du composant "dès que possible".
javax.swing.JComponent 1.2
•
void paintComponent(Graphics g)
Surchargez cette méthode pour décrire la manière dont votre composant doit être dessiné.
Formes 2D
Depuis la version 1.0 de JDK, la classe Graphics disposait de méthodes pour dessiner des lignes,
des rectangles, des ellipses, etc. Mais ces opérations de dessin sont très limitées. Par exemple, vous
ne pouvez pas tracer des traits d’épaisseurs différentes ni faire pivoter les formes.
Le JDK 1.2 a introduit la bibliothèque Java 2D qui implémente un jeu d’opérations graphiques très
puissantes. Dans ce chapitre, nous allons seulement examiner les fonctions de base de la bibliothèque Java 2D. Consultez le chapitre du Volume 2 traitant des fonctions AWT avancées pour plus
d’informations sur les techniques sophistiquées.
Pour dessiner des formes dans la bibliothèque Java 2D, vous devez obtenir un objet de la classe
Graphics2D. Il s’agit d’une sous-classe de la classe Graphics. Si votre version du JDK est
Livre Java .book Page 299 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
299
compatible Java 2D, les méthodes telles que paintComponent reçoivent automatiquement un
objet de la classe Graphics2D. Il suffit d’avoir recours à un transtypage, de la façon suivante :
public void paintComponent(Graphics g)
{
Graphics2D g2 = (Graphics2D)g;
. . .
}
La bibliothèque Java 2D organise les formes géométriques d’une façon orientée objet. En particulier,
il existe des classes pour représenter des lignes, des rectangles et des ellipses :
Line2D
Rectangle2D
Ellipse2D
Ces classes implémentent toutes l’interface Shape.
INFO
La bibliothèque Java 2D gère des formes plus complexes — en particulier, les arcs, les courbes quadratiques et cubiques
et les objets "general path". Voir le Chapitre 7 du Volume 2 pour plus d’informations.
Pour dessiner une forme, vous devez d’abord créer un objet d’une classe qui implémente l’interface
Shape puis appeler la méthode draw de la classe Graphics2D. Par exemple :
Rectangle2D rect = . . .;
g2.draw(rect);
INFO
Avant l’apparition de la bibliothèque Java 2D, les programmeurs utilisaient les méthodes de la classe Graphics
telles que drawRectangle pour dessiner des formes. En apparence, les appels de méthode de l’ancien style paraissent plus simples. Cependant, avec la bibliothèque Java 2D, vos options restent ouvertes — vous pouvez ultérieurement
améliorer vos dessins à l’aide des nombreux outils que fournit la bibliothèque.
L’utilisation des classes de formes Java 2D amène une certaine complexité. Contrairement aux
méthodes de la version 1.0, qui utilisaient des entiers pour les coordonnées de pixels, Java 2D
emploie des valeurs de coordonnées en virgule flottante. Cela est souvent pratique, car vous pouvez
spécifier pour vos formes des coordonnées qui sont significatives pour vous (comme des millimètres
par exemple), puis les traduire en pixels. La bibliothèque Java 2D utilise des valeurs float simple
précision pour nombre de ses calculs internes en virgule flottante. La simple précision est suffisante
— après tout, le but ultime des calculs géométriques est de définir des pixels à l’écran ou sur l’imprimante. Tant qu’une erreur d’arrondi reste cantonnée à un pixel, l’aspect visuel n’est pas affecté. De
plus, les calculs en virgule flottante sont plus rapides sur certaines plates-formes et les valeurs float
requièrent un volume de stockage réduit de moitié par rapport aux valeurs double.
Cependant, la manipulation de valeurs float est parfois peu pratique pour le programmeur, car le
langage Java est inflexible en ce qui concerne les transtypages nécessaires pour la conversion de
valeurs double en valeurs float. Par exemple, examinez l’instruction suivante :
float f = 1.2; // Erreur
Livre Java .book Page 300 Jeudi, 25. novembre 2004 3:04 15
300
Au cœur de Java 2 - Notions fondamentales
Cette instruction échoue à la compilation, car la constante 1.2 est du type double, et le compilateur
est très susceptible en ce qui concerne la perte de précision. Le remède consiste à ajouter un suffixe
F à la constante à virgule flottante :
float f = 1.2F; // Ok
Considérez maintenant l’instruction
Rectangle2D r = . . .
float f = r.getWidth(); // Erreur
Cette instruction échouera également à la compilation, pour la même raison. La méthode getWidth
renvoie un double. Cette fois, le remède consiste à prévoir un transtypage :
float f = (float)r.getWidth(); // Ok
Les suffixes et les transtypages étant un inconvénient évident, les concepteurs de la bibliothèque 2D
ont décidé de fournir deux versions de chaque classe de forme : une avec des coordonnées float
pour les programmeurs économes, et une avec des coordonnées double pour les paresseux (dans cet
ouvrage, nous nous rangerons du côté des seconds, et nous utiliserons des coordonnées double dans
la mesure du possible).
Les concepteurs de la bibliothèque ont choisi une méthode curieuse, et assez déroutante au premier
abord, pour le packaging de ces choix. Examinez la classe Rectangle2D. C’est une classe abstraite,
avec deux sous-classes concrètes, qui sont aussi des classes internes statiques :
Rectangle2D.Float
Rectangle2D.Double
La Figure 7.8 montre le schéma d’héritage.
Figure 7.8
Les classes rectangle 2D.
Rectangle2D
Rectangle2D
.Float
Rectangle2D
.Double
Il est préférable de tenter d’ignorer le fait que les deux classes concrètes sont internes statiques —
c’est juste une astuce pour éviter d’avoir à fournir des noms tels que FloatRectangle2D et
DoubleRectangle2D (pour plus d’informations au sujet des classes internes statiques, reportez-vous
au Chapitre 6).
Lorsque vous construisez un objet Rectangle2D.Float, vous fournissez les coordonnées en tant
que nombres float. Pour un objet Rectangle2D.Double, vous fournissez des nombres de type
double :
Rectangle2D.Float floatRect = new Rectangle2D.Float(10.0F,
25.0F, 22.5F, 20.0F);
Livre Java .book Page 301 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
301
Rectangle2D.Double doubleRect = new Rectangle2D.Double(10.0,
25.0, 22.5, 20.0);
En réalité, puisque à la fois Rectangle2D.Float et Rectangle2D.Double étendent la classe
commune Rectangle2D, et que les méthodes dans les sous-classes surchargent simplement les
méthodes dans la superclasse Rectangle2D, il n’y a aucun intérêt à mémoriser le type exact de
forme. Vous pouvez simplement employer des variables Rectangle2D pour stocker les références
du rectangle :
Rectangle2D floatRect = new Rectangle2D.Float(10.0F,
25.0F, 22.5F, 20.0F);
Rectangle2D doubleRect = new Rectangle2D.Double(10.0,
25.0, 22.5, 20.0);
C’est-à-dire que vous n’avez besoin d’utiliser les empoisonnantes classes internes que lorsque vous
construisez les objets formes.
Les paramètres de construction indiquent le coin supérieur gauche, la largeur et la hauteur du
rectangle.
INFO
En réalité, la classe Rectangle2D.Float possède une méthode supplémentaire qui n’est pas héritée de
Rectangle2D, il s’agit de setRect(float x, float y, float h, float w). Vous perdez cette méthode si
vous stockez la référence Rectangle2D.Float dans une variable Rectangle2D. Mais ce n’est pas une grosse perte
— la classe Rectangle2D dispose d’une méthode setRect avec des paramètres double.
Les méthodes de Rectangle2D utilisent des paramètres et des valeurs renvoyées de type double. Par
exemple, la méthode getWidth renvoie une valeur double, même si la largeur est stockée sous la
forme de float dans un objet Rectangle2D.Float.
ASTUCE
Utilisez simplement les classes de forme Double pour éviter d’avoir à manipuler des valeurs float. Cependant, si
vous construisez des milliers d’objets forme, envisagez d’employer les classes Float pour économiser la mémoire.
Ce que nous venons de voir pour les classes Rectangle2D est valable aussi pour les autres classes.
Il existe de plus une classe Point2D avec les sous-classes Point2D.Float et Point2D.Double.
Voici comment l’utiliser :
Point2D p = new Point2D.Double(10, 20);
ASTUCE
La classe Point2D est très utile — elle est plus orientée objet pour fonctionner avec les objets Point2D qu’avec des
valeurs x et y séparées. De nombreux constructeurs et méthodes acceptent des paramètres Point2D. Nous vous
suggérons d’utiliser des objets Point2D autant que possible — ils facilitent généralement la compréhension des
calculs géométriques.
Livre Java .book Page 302 Jeudi, 25. novembre 2004 3:04 15
302
Au cœur de Java 2 - Notions fondamentales
Les classes Rectangle2D et Ellipse2D héritent toutes deux d’une superclasse commune RectangularShape. Bien sûr, les ellipses ne sont pas rectangulaires, mais elles sont incluses dans un
rectangle englobant (voir Figure 7.9).
Figure 7.9
Le rectangle englobant
d’une ellipse.
La classe RectangularShape définit plus de 20 méthodes qui sont communes à ces formes, parmi
lesquelles les incontournables getWidth, getHeight, getCenterX et getCenterY (mais malheureusement, au moment où nous écrivons ces lignes, il n’existe pas de méthode getCenter qui renverrait
le centre sous la forme d’un objet Point2D).
On trouve enfin deux classes héritées de JDK 1.0 qui ont été insérées dans la hiérarchie de la classe
Shape. Les classes Rectangle et Point, qui stockent un rectangle et un point avec des coordonnées
entières, étendent les classes Rectangle2D et Point2D.
La Figure 7.10 montre les relations entre les classes Shape. Les sous-classes Double et Float sont
omises. Les classes héritées sont grisées.
Les objets Rectangle2D et Ellipse2D sont simples à construire. Vous devez spécifier :
m
les coordonnées x et y du coin supérieur gauche ;
m
la largeur et la hauteur.
Figure 7.10
Relations entre
les classes Shape.
Shape
Point2D
Point
Rectangular
Shape
Line2D
Ellipse2D
Rectangle2D
Rectangle
Livre Java .book Page 303 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
303
Pour les ellipses, ces valeurs font référence au rectangle englobant.
Par exemple,
Ellipse2D e = new Ellipse2D.Double(150, 200, 100, 50);
construit une ellipse englobée dans un rectangle dont le coin supérieur gauche a les coordonnées
(150, 200), avec une largeur de 100, et une hauteur de 50.
Il arrive que vous ne disposiez pas directement des valeurs du coin supérieur gauche. Il est assez
fréquent d’avoir les deux coins opposés de la diagonale d’un rectangle, mais peut-être qu’il ne s’agit
pas des coins supérieur gauche et inférieur droit. Vous ne pouvez pas construire simplement un
rectangle ainsi :
Rectangle2D rect = new Rectangle2D.Double(px, py,
qx - px, qy - py); // Erreur
Si p n’est pas le coin supérieur gauche, l’une des différences de coordonnées, ou les deux, seront
négatives et le rectangle sera vide. Dans ce cas, créez d’abord un rectangle vide et utilisez la
méthode setFrameFromDiagonal :
Rectangle2D rect = new Rectangle2D.Double();
rect.setFrameFromDiagonal(px, py, qx, qy);
Ou, mieux encore, si vous connaissez les points d’angle en tant qu’objets Point2D, p et q :
rect.setFrameFromDiagonal(p, q);
Lors de la construction d’une ellipse, vous connaissez généralement le centre, la largeur et la
hauteur, mais pas les points des angles du rectangle englobant (qui ne reposent pas sur l’ellipse).
Il existe une méthode setFrameFromCenter qui utilise le point central, mais qui requiert
toujours l’un des quatre points d’angle. Vous construisez donc généralement une ellipse de la
façon suivante :
Ellipse2D ellipse = new Ellipse2D.Double(centerX - width / 2,
centerY - height / 2, width, height);
Pour construire une ligne, vous fournissez les points de départ et d’arrivée, sous la forme d’objets
Point2D ou de paires de nombres :
Line2D line = new Line2D.Double(start, end);
ou
Line2D line = new Line2D.Double(startX, startY, endX, endY);
Le programme de l’Exemple 7.4 trace :
m
un rectangle ;
m
l’ellipse incluse dans le rectangle ;
m
une diagonale du rectangle ;
m
un cercle ayant le même centre que le rectangle.
La Figure 7.11 montre le résultat.
Livre Java .book Page 304 Jeudi, 25. novembre 2004 3:04 15
304
Au cœur de Java 2 - Notions fondamentales
Figure 7.11
Rectangles et ellipses.
Exemple 7.4 : DrawTest.java
import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
public class DrawTest
{
public static void main(String[] args)
{
DrawFrame frame = new DrawFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre contenant un panneau avec des dessins
*/
class DrawFrame extends JFrame
{
public DrawFrame()
{
setTitle("DrawTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter un panneau au cadre
DrawPanel panel = new DrawPanel();
add(panel);
}
public static final int DEFAULT_WIDTH = 400;
public static final int DEFAULT_HEIGHT = 400;
}
Livre Java .book Page 305 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
/**
Un panneau qui affiche des rectangles et des ellipses.
*/
class DrawPanel extends JPanel
{
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
// dessiner un rectangle
double
double
double
double
leftX = 100;
topY = 100;
width = 200;
height = 150;
Rectangle2D rect = new Rectangle2D.Double(leftX, topY,
width, height);
g2.draw(rect);
// dessiner l’ellipse à l’intérieur
Ellipse2D ellipse = new Ellipse2D.Double();
ellipse.setFrame(rect);
g2.draw(ellipse);
// tracer une ligne diagonale
g2.draw(new Line2D.Double(leftX, topY,
leftX + width, topY + height));
// dessiner un cercle ayant le même centre
double centerX = rect.getCenterX();
double centerY = rect.getCenterY();
double radius = 150;
Ellipse2D circle = new Ellipse2D.Double();
circle.setFrameFromCenter(centerX, centerY,
centerX + radius, centerY + radius);
g2.draw(circle);
}
}
java.awt.geom.RectangularShape 1.2
•
•
•
•
•
•
double getCenterX()
double getCenterY()
double getMinX()
double getMinY()
double getMaxX()
double getMaxY()
Renvoient le centre, les valeurs x ou y minimum ou maximum du rectangle englobant.
305
Livre Java .book Page 306 Jeudi, 25. novembre 2004 3:04 15
306
•
•
Au cœur de Java 2 - Notions fondamentales
double getWidth()
double getHeight()
Renvoient la largeur ou la hauteur du rectangle englobant.
•
•
double getX()
double getY()
Renvoient les coordonnées x ou y du coin supérieur gauche du rectangle englobant.
java.awt.geom.Rectangle2D.Double 1.2
• Rectangle2D.Double(double x, double y, double w, double h)
Construit un rectangle avec les valeurs données pour le coin supérieur gauche, la largeur et la hauteur.
java.awt.geom.Rectangle2D.Float 1.2
• Rectangle2D.Float(float x, float y, float w, float h)
Construit un rectangle avec les valeurs données pour le coin supérieur gauche, la largeur et la hauteur.
java.awt.geom.Ellipse2D.Double 1.2
• Ellipse2D.Double(double x, double y, double w, double h)
Construit une ellipse dont le rectangle englobant a les valeurs données pour le coin supérieur
gauche, la largeur et la hauteur.
java.awt.geom.Point2D.Double 1.2
• Point2D.Double(double x, double y)
Construit un point avec les coordonnées indiquées.
java.awt.geom.Line2D.Double 1.2
• Line2D.Double(Point2D start, Point2D end)
• Line2D.Double(double startX, double startY, double endX, double endY)
Construisent une ligne avec les points de départ et d’arrivée indiqués.
Couleurs
La méthode setPaint de la classe Graphics2D permet de sélectionner une couleur qui sera
employée par toutes les opérations de dessin ultérieures pour le contexte graphique. Pour dessiner
avec plusieurs couleurs, vous devez sélectionner une couleur, effectuer une opération, puis sélectionner
une autre couleur avant de procéder à l’opération suivante.
Les couleurs sont définies à l’aide de la classe Color. La classe java.awt.Color propose des constantes
prédéfinies pour les treize couleurs standard indiquées dans le Tableau 7.1.
Tableau 7.1 : Couleurs standard
BLACK (noir)
BLUE (bleu)
CYAN
DARK_GRAY (gris foncé)
GRAY (gris)
GREEN (vert)
LIGHT_GRAY (gris clair)
MAGENTA
ORANGE
PINK (rose)
RED (rouge)
WHITE (blanc)
YELLOW (jaune)
Livre Java .book Page 307 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
307
Par exemple :
g2.setPaint(Color.red);
g2.drawString("Warning!", 100, 100);
INFO
Avant le JDK 1.4, les noms constants des couleurs étaient en minuscules, comme Color.red. Ceci est étrange car la
convention de codage standard consiste à écrire les noms constants en majuscules. Depuis le JDK 1.4, vous pouvez
écrire les noms des couleurs standard en majuscules ou, pour une compatibilité avec les versions antérieures, en
minuscules.
Vous pouvez spécifier une couleur personnalisée en créant un objet Color à l’aide de ses composantes rouge, verte et bleue. En utilisant une échelle de 0 à 255 (c’est-à-dire un octet) pour les proportions
de rouge, de vert et de bleu, appelez le constructeur de Color de la façon suivante :
Color(int redness, int greenness, int blueness)
Voici un exemple de définition de couleur personnalisée :
g2.setPaint(new Color(0, 128, 128)); // un bleu-vert foncé
g2.drawString("Welcome!", 75, 125);
INFO
En plus des couleurs franches, vous pouvez sélectionner des définitions de "peinture" plus complexes, comme des
images ou des teintes nuancées. Consultez le Chapitre 7 du Volume 2 traitant de AWT pour plus de détails. Si vous
utilisez un objet Graphics au lieu de Graphics2D, vous devrez avoir recours à la méthode setColor pour définir
les couleurs.
Pour spécifier la couleur d’arrière-plan (ou de fond), utilisez la méthode setBackground de la
classe Component, qui est un ancêtre de JPanel :
MyPanel p = new MyPanel();
p.setBackground(Color.PINK);
Il existe aussi une méthode setForeground. Elle spécifie la couleur par défaut utilisée pour le
dessin.
ASTUCE
Les méthodes brighter() et darker() de la classe Color produisent des versions plus vives ou plus foncées de la
couleur actuelle. La méthode brighter permet de mettre un élément en surbrillance, mais avive en réalité à peine
la couleur. Pour obtenir une couleur nettement plus visible, appelez trois fois la méthode :
c.brighter().brighter().brighter().
Java fournit des noms prédéfinis pour de nombreuses couleurs dans sa classe SystemColor. Les
constantes de cette classe encapsulent les couleurs employées pour divers éléments du système de
l’utilisateur. Par exemple,
frame.setBackground(SystemColor.window)
Livre Java .book Page 308 Jeudi, 25. novembre 2004 3:04 15
308
Au cœur de Java 2 - Notions fondamentales
affecte à l’arrière-plan du panneau la couleur utilisée par défaut par toutes les fenêtres du bureau
(l’arrière-plan est rempli avec cette couleur chaque fois que la fenêtre est repeinte). L’emploi des
couleurs de la classe SystemColor est particulièrement utile si vous voulez dessiner des éléments
d’interface dont les couleurs doivent s’accorder avec celles de l’environnement. Le Tableau 7.2
décrit les couleurs système.
Tableau 7.2 : Couleurs du système
desktop
Arrière-plan du bureau
activeCaption
Arrière-plan des titres
activeCaptionText
Texte des titres
activeCaptionBorder
Bordure des titres
inactiveCaption
Arrière-plan des titres inactifs
inactiveCaptionText
Texte des titres inactifs
inactiveCaptionBorder
Bordure des titres inactifs
window
Arrière-plan des fenêtres
windowBorder
Bordure des fenêtres
windowText
Texte à l’intérieur d’une fenêtre
menu
Arrière-plan des menus
menuText
Texte des menus
text
Arrière-plan du texte
textText
Couleur du texte
textHighlight
Arrière-plan du texte en surbrillance (marqué)
textHighlightText
Couleur du texte en surbrillance (marqué)
control
Arrière-plan des contrôles
controlText
Texte des contrôles
controlLtHighlight
Couleur de surbrillance claire des contrôles
controlHighlight
Couleur de surbrillance des contrôles
controlShadow
Ombre des contrôles
controlDkShadow
Ombre foncée des contrôles
inactiveControlText
Texte des contrôles inactifs
Livre Java .book Page 309 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
309
Tableau 7.2 : Couleurs du système (suite)
scrollbar
Arrière-plan des barres de défilement
info
Arrière-plan du texte des bulles d’aide
infoText
Couleur du texte des bulles d’aide
java.awt.Color 1.0
•
Color(int r, int g, int b)
Crée un objet couleur.
Paramètres :
r
Valeur de la composante rouge (0-255).
g
Valeur de la composante verte (0-255).
b
Valeur de la composante bleue (0-255).
java.awt.Graphics 1.0
•
void setColor(Color c)
Modifie la couleur courante. Toutes les opérations graphiques ultérieures utiliseront la nouvelle
couleur.
Paramètres :
c
Nouvelle couleur.
java.awt.Graphics2D 1.2
•
void setPaint(Paint p)
Définit les paramètres de peinture de ce contexte graphique. La classe Color implémente l’interface Paint. Vous pouvez donc utiliser cette méthode pour affecter les attributs d’une couleur.
java.awt.Component 1.0
•
void setBackground (Color c)
Spécifie la couleur d’arrière-plan.
Paramètres :
•
c
Nouvelle couleur d’arrière-plan.
void setForeground(Color c)
Spécifie la couleur d’avant-plan.
Paramètres :
c
Nouvelle couleur d’avant-plan.
Remplir des formes
Vous pouvez remplir des formes fermées (comme des rectangles et des ellipses) avec une couleur
(ou, plus généralement, avec les motifs correspondant à la configuration actuelle). Appelez simplement
fill au lieu de draw :
Rectangle2D rect = . . .;
g2.setPaint(Color.RED);
g2.fill(rect); // emplit rect avec la couleur rouge
Livre Java .book Page 310 Jeudi, 25. novembre 2004 3:04 15
310
Au cœur de Java 2 - Notions fondamentales
Le programme de l’Exemple 7.5 peint un rectangle en rouge, puis une ellipse avec les mêmes limites,
en vert foncé (voir Figure 7.12).
Figure 7.12
Rectangles
et ellipses colorés.
Exemple 7.5 : FillTest.java
import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
public class FillTest
{
public static void main(String[] args)
{
FillFrame frame = new FillFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre contenant un panneau avec des dessins
*/
class FillFrame extends JFrame
{
public FillFrame()
{
setTitle("FillTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter un panneau au cadre
Livre Java .book Page 311 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
311
FillPanel panel = new FillPanel();
add(panel);
}
public static final int DEFAULT_WIDTH = 400;
public static final int DEFAULT_HEIGHT = 400;
}
/**
Un panneau affichant des rectangles et ellipses colorés
*/
class FillPanel extends JPanel
{
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
// dessiner un rectangle
double
double
double
double
leftX = 100;
topY = 100;
width = 200;
height = 150;
Rectangle2D rect = new Rectangle2D.Double(leftX, topY,
width, height);
g2.setPaint(Color.RED);
g2.fill(rect);
// dessiner l’ellipse englobée
Ellipse2D ellipse = new Ellipse2D.Double();
ellipse.setFrame(rect);
g2.setPaint(new Color(0, 128, 128)); // bleu-vert foncé
g2.fill(ellipse);
}
}
Texte et polices
Le programme NotHelloWorld au début de ce chapitre affichait une chaîne avec la fonte par défaut.
Il est possible d’écrire un texte avec une fonte différente. Une fonte est spécifiée grâce à son nom de
police. Un nom de police se compose d’un nom de famille de polices, comme "Helvetica" et d’un
suffixe facultatif, comme "Bold" (gras) ou "Italic" (italique). Autrement dit, les polices "Helvetica",
"Helvetica Bold" et "Helvetica Italic" font toutes partie de la famille "Helvetica".
Pour connaître les fontes disponibles sur un ordinateur, appelez la méthode getAvailableFontFamilyNames de la classe GraphicsEnvironment. Cette méthode renvoie un tableau de chaînes contenant les
noms de toutes les fontes disponibles. La méthode statique getLocalGraphicsEnvironment permet
d’obtenir une instance de la classe GraphicsEnvironment qui décrit l’environnement graphique du
Livre Java .book Page 312 Jeudi, 25. novembre 2004 3:04 15
312
Au cœur de Java 2 - Notions fondamentales
système utilisateur. Le programme suivant donne en sortie une liste de tous les noms de fonte de votre
système :
import java.awt.*;
public class ListFonts
{
public static void main(String[] args)
{
String[] fontNames = GraphicsEnvironment
.getLocalGraphicsEnvironment()
.getAvailableFontFamilyNames();
for (String fontName : fontNames)
System.out.println(fontName);
}
}
Pour un système donné, la liste commencera ainsi :
Abadi MT Condensed Light
Arial
Arial Black
Arial Narrow
Arioso
Baskerville
Binner Gothic
. . .
et se poursuivra avec quelques 70 fontes supplémentaires.
INFO
La documentation JDK indique que des suffixes comme "heavy", "medium", "oblique" ou "gothic" sont des variantes au sein d’une même famille. Selon notre propre expérience, ce n’est pas le cas. Les suffixes "Bold", "Italic" et
"Bold Italic" sont effectivement reconnus comme tels, mais pas les autres.
Il n’existe malheureusement aucun moyen absolu de savoir si un utilisateur a une fonte avec un look
particulier installée. Les noms de fontes peuvent être déposés, et faire l’objet de copyright sous
certaines juridictions. La distribution des fontes implique donc souvent le versement de droits
d’auteur. Bien entendu, comme dans le cas des parfums célèbres, on trouve des imitations peu
coûteuses de certaines fontes réputées. A titre d’exemple, l’imitation d’Helvetica distribuée avec
Windows se nomme Arial.
Pour établir une ligne de base commune, AWT définit cinq noms de fontes logiques :
SansSerif
Serif
Espacement fixe
Dialog
DialogInput
Ces noms sont toujours transcrits en fontes existant dans l’ordinateur client. Par exemple, dans un
système Windows, SansSerif est transcrit en Arial.
Livre Java .book Page 313 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
313
INFO
La concordance des polices est définie dans le fichier fontconfig.properties du sous-répertoire jre/lib
de l’installation Java. Suivez http://java.sun.com/j2se/5.0/docs/guide/intl/fontconfig.html pour en savoir plus
sur ce fichier. Les versions précédentes du JDK utilisaient un fichier font.properties qui est maintenant
obsolète.
Pour écrire des caractères dans une fonte, vous devez d’abord créer un objet de la classe Font. Vous
spécifiez le nom de fonte, son style, et la taille en points. Voici un exemple de construction d’un objet
Font :
Font helvb14 = new Font("Helvetica", Font.BOLD, 14);
Le troisième paramètre est la taille en points. Le point est souvent utilisé en typographie pour indiquer la
taille d’une police. Un pouce comprend 72 points.
Le nom de fonte logique peut être employé dans le constructeur de Font. Il faut ensuite spécifier le
style (plain, bold, italic ou bold italic) dans le second paramètre du constructeur, en lui donnant une
des valeurs suivantes :
Font.PLAIN
Font.BOLD
Font.ITALIC
Font.BOLD + Font.ITALIC
Par exemple :
Font sansbold14 = new Font("SansSerif", Font.BOLD, 14)
INFO
Les versions précédentes de Java utilisaient les noms Helvetica, TimesRoman, Courier et ZapfDingbats comme
noms de fontes logiques. Pour des raisons de compatibilité amont, ces noms de fontes sont toujours traités comme
des noms de fontes logiques, bien qu’Helvetica soit en fait un nom de police et que TimesRoman et ZapfDingbats
ne soient pas des fontes — leurs noms de police sont "Times Roman" et "Zapf Dingbats".
ASTUCE
A partir de la version 1.3 du JDK, vous pouvez lire les fontes TrueType. Un flux d’entrée est nécessaire pour la fonte
— généralement à partir d’un fichier disque ou d’une adresse URL (voir le Chapitre 12 pour plus d’informations sur
les flux). Appelez ensuite la méthode statique Font.createFont :
URL url = new URL("http://www.fonts.com/Wingbats.ttf");
InputStream in = url.openStream();
Font f = Font.createFont(Font.TRUETYPE_FONT, in);
La fonte est normale (plain) avec une taille de 1 point. Employez la méthode deriveFont pour obtenir une fonte
de la taille désirée :
Font df = f.deriveFont(14.0F);
Livre Java .book Page 314 Jeudi, 25. novembre 2004 3:04 15
314
Au cœur de Java 2 - Notions fondamentales
ATTENTION
Il existe deux versions surchargées de la méthode deriveFont. L’une d’elles (ayant un paramètre float) définit
le corps de la police, l’autre (avec un paramètre int), son style. Dès lors, f.deriveFont(14) définit le style et non le
corps (le résultat est une police italique car la représentation binaire de 14 définit le type ITALIC mais non le type
BOLD).
Les fontes Java contiennent les habituels caractères ASCII et des symboles. Par exemple, si vous
affichez le caractère ’\u2297’ de la fonte Dialog, vous obtenez un caractère ⊗ représentant une
croix dans un cercle. Seuls sont disponibles les symboles définis dans le jeu de caractères Unicode.
Voici maintenant le code affichant la chaîne "Hello, World!" avec la fonte sans sérif standard de
votre système, en utilisant un style gras en 14 points :
Font sansbold14 = new Font("SansSerif", Font.BOLD, 14);
g2.setFont(sansbold14);
String message = "Hello, World!";
g2.drawString(message, 75, 100);
Nous allons maintenant centrer la chaîne dans le panneau au lieu de l’écrire à une position arbitraire.
Nous devons connaître la largeur et la hauteur de la chaîne en pixels. Ces dimensions dépendent de
trois facteurs :
m
La fonte utilisée (ici, sans sérif, bold, 14 points).
m
La chaîne (ici, "Hello, World!").
m
L’unité sur laquelle la chaîne est écrite (ici, l’écran de l’utilisateur).
Pour obtenir un objet qui représente les caractéristiques de fonte de l’écran, appelez la méthode
getFontRenderContext de la classe Graphics2D. Elle renvoie un objet de la classe FontRenderContext. Vous passez simplement cet objet à la méthode getStringBounds de la classe Font :
FontRenderContext context = g2.getFontRenderContext();
Rectangle2D bounds = f.getStringBounds(message, context);
La méthode getStringBounds renvoie un rectangle qui englobe la chaîne.
Pour interpréter les dimensions de ce rectangle, il est utile de connaître certains des termes de typographie (voir Figure 7.13). La ligne de base (baseline) est la ligne imaginaire sur laquelle s’aligne la
base des caractères comme "e". Le jambage ascendant (ascent) représente la distance entre la ligne
de base et la partie supérieure d’une lettre "longue du haut", comme "b" ou "k" ou un caractère
majuscule. Le jambage descendant (descent) représente la distance entre la ligne de base et la partie
inférieure d’une lettre "longue du bas", comme "p" ou "g".
Figure 7.13
Illustration
des termes de
typographie.
baseline
height
baseline
e b k p g
ascent
descent
leading
Livre Java .book Page 315 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
315
L’interligne (leading) est l’intervalle entre la partie inférieure d’une ligne et la partie supérieure de la
ligne suivante. La hauteur (height) est la distance verticale entre deux lignes de base successives et
équivaut à (jambage descendant + interligne + jambage ascendant).
La largeur (width) du rectangle que renvoie la méthode getStringBounds est l’étendue horizontale
de la chaîne. La hauteur du rectangle est la somme des jambages ascendant, descendant et de l’interligne. L’origine du rectangle se trouve à la ligne de base de la chaîne. La coordonnée y supérieure du
rectangle est négative. Vous pouvez obtenir les valeurs de largeur, de hauteur et des jambage ascendant
d’une chaîne, de la façon suivante :
double stringWidth = bounds.getWidth();
double stringHeight = bounds.getHeight();
double ascent = -bounds.getY();
Si vous devez connaître le jambage descendant ou l’interligne, appelez la méthode getLineMetrics
de la classe Font. Elle renvoie un objet de la classe LineMetrics, possédant des méthodes permettant
d’obtenir le jambage descendant et l’interligne :
LineMetrics metrics = f.getLineMetrics(message, context);
float descent = metrics.getDescent();
float leading = metrics.getLeading();
Le code suivant utilise toutes ces informations pour centrer une chaîne à l’intérieur de son panneau
contenant :
FontRenderContext context = g2.getFontRenderContext();
Rectangle2D bounds = f.getStringBounds(message, context);
// (x,y) = coin supérieur gauche du texte
double x = (getWidth() - bounds.getWidth()) / 2;
double y = (getHeight() - bounds.getHeight()) / 2;
// ajouter le jambage ascendant à y pour atteindre la ligne de base
double ascent = -bounds.getY();
double baseY = y + ascent;
g2.drawString(message, (int)x, (int)(baseY);
Pour comprendre le centrage, considérez que getWidth() renvoie la largeur du panneau. Une
portion de cette largeur, bounds.getWidth(), est occupée par la chaîne de message. Le reste doit
être équitablement réparti des deux côtés. Par conséquent, l’espace vide de chaque côté représente la
moitié de la différence. Le même raisonnement s’applique à la hauteur.
Enfin, le programme dessine la ligne de base et le rectangle englobant.
La Figure 7.14 montre l’affichage à l’écran ; l’Exemple 7.6 le listing du programme.
Figure 7.14
Dessin de la ligne
de base et des limites
de la chaîne.
Livre Java .book Page 316 Jeudi, 25. novembre 2004 3:04 15
316
Au cœur de Java 2 - Notions fondamentales
Exemple 7.6 : FontTest.java
import
import
import
import
java.awt.*;
java.awt.font.*;
java.awt.geom.*;
javax.swing.*;
public class FontTest
{
public static void main(String[] args)
{
FontFrame frame = new FontFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec un panneau pour un message texte
*/
class FontFrame extends JFrame
{
public FontFrame()
{
setTitle("FontTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter un panneau au cadre
FontPanel panel = new FontPanel();
add(panel);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
/**
Un panneau affichant un message centré dans une boîte.
*/
class FontPanel extends JPanel
{
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
String message = "Hello, World!";
Font f = new Font("Serif", Font.BOLD, 36);
g2.setFont(f);
// mesurer la taille du message
FontRenderContext context = g2.getFontRenderContext();
Rectangle2D bounds = f.getStringBounds(message, context);
// définir (x,y) = coin supérieur gauche du texte
Livre Java .book Page 317 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
317
double x = (getWidth() - bounds.getWidth()) / 2;
double y = (getHeight() - bounds.getHeight()) / 2;
/* ajouter jambage ascendant à y pour
atteindre la ligne de base */
double ascent = -bounds.getY();
double baseY = y + ascent;
// écrire le message
g2.drawString(message, (int)x, (int)(baseY);
g2.setPaint(Color.GRAY);
// dessiner la ligne de base
g2.draw(new Line2D.Double(x, baseY,
x + bounds.getWidth(), baseY));
// dessiner le rectangle englobant
Rectangle2D rect = new Rectangle2D.Double(x, y,
bounds.getWidth(),
bounds.getHeight());
g2.draw(rect);
}
}
java.awt.Font 1.0
Font(String name, int style, int size)
•
Crée un nouvel objet fonte.
Paramètres :
•
name
Le nom de la fonte. Il s’agit, soit d’un nom de police
(comme "Helvetica Bold"), soit d’un nom de fonte logique
(comme "Serif" ou "SansSerif").
style
Le style (Font.PLAIN, Font.BOLD, Font.ITALIC
ou Font.BOLD + Font.ITALIC).
size
La taille en points (par exemple, 12).
String getFontName()
Renvoie le nom de police (par exemple, "Helvetica Bold").
•
String getFamily()
Renvoie le nom de la famille de polices (comme "Helvetica").
•
String getName()
Renvoie le nom logique (par exemple, "SansSerif") si la fonte a été créée avec un nom de police
logique ; sinon la méthode renvoie le "nom de police" de la fonte.
•
Rectangle2D getStringBounds(String s, FontRenderContext context) 1.2
Renvoie un rectangle qui englobe la chaîne. L’origine du rectangle se trouve sur la ligne de base.
La coordonnée y supérieure du rectangle est égale à la valeur négative du jambage ascendant. La
hauteur du rectangle égale la somme des jambages ascendant et descendant et de l’interligne.
La largeur est celle de la chaîne.
Livre Java .book Page 318 Jeudi, 25. novembre 2004 3:04 15
318
•
Au cœur de Java 2 - Notions fondamentales
LineMetrics getLineMetrics(String s, FontRenderContext context) 1.2
Renvoie un objet Line pour déterminer l’étendue de la chaîne.
•
•
•
Font deriveFont(int style) 1.2
Font deriveFont(float size) 1.2
Font deriveFont(int style, float size) 1.2
Renvoient une nouvelle fonte équivalant à cette fonte, avec la taille et le style demandés.
java.awt.font.LineMetrics 1.2
•
float getAscent()
Renvoie la taille du jambage ascendant — distance entre la ligne de base et le sommet des caractères majuscules.
•
float getDescent()
Renvoie la taille du jambage descendant — distance entre la ligne de base et le bas des lettres
"longues du bas", comme "p" ou "g".
•
float getLeading()
Renvoie l’interligne — l’espace entre la partie inférieure d’une ligne de texte et la partie supérieure de la ligne suivante.
•
float getHeight()
Renvoie la hauteur totale de la fonte — distance entre deux lignes de base (jambage descendant + interligne + jambage ascendant).
java.awt.Graphics 1.0
•
void setFont(Font font)
Sélectionne une fonte pour le contexte graphique. Cette fonte sera employée dans les opérations
ultérieures d’affichage de texte.
Paramètres :
•
font
Une fonte.
void drawString(String str, int x, int y)
Dessine une chaîne avec la fonte et la couleur courantes.
Paramètres :
str
La chaîne à dessiner.
x
Coordonnée x du début de la chaîne.
y
Coordonnée y de la ligne de base de la chaîne.
java.awt.Graphics2D 1.2
•
FontRenderContext getFontRenderContext ()
Extrait un contexte de rendu de fonte qui spécifie les caractéristiques de la fonte dans ce contexte
graphique.
•
void drawString(String str, float x, float y)
Dessine une chaîne avec la fonte et la couleur courantes.
Paramètres :
str
La chaîne à dessiner.
x
Coordonnée x du début de la chaîne.
y
Coordonnée y de la ligne de base de la chaîne.
Livre Java .book Page 319 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
319
Images
Vous avez vu comment créer des images simples en traçant des lignes et des formes. Des images
complexes, telles que des photographies, sont habituellement générées en externe, par exemple avec
un scanner ou un logiciel dédié à la manipulation d’images (comme vous le verrez au Chapitre 7 du
Volume 2, il est également possible de produire une image pixel par pixel, et de stocker le résultat
dans un tableau. Cette procédure est courante pour les images fractales, par exemple).
Lorsque des images sont stockées dans des fichiers locaux ou sur Internet, vous pouvez les lire dans
une application Java et les afficher sur des objets de type Graphics. Depuis le JDK 1.4, la lecture
d’une image est très simple. Si l’image est stockée dans un fichier local, appelez
String filename = "...";
Image image = ImageIO.read(new File(filename));
Sinon vous pouvez fournir une URL :
String urlname = "...";
Image image = ImageIO.read(new URL(urlname));
La méthode de lecture déclenche une exception IOException si l’image n’est pas disponible. Nous
traiterons du sujet général de la gestion des exceptions au Chapitre 11. Pour l’instant, notre exemple
de programme se contente d’intercepter cette exception et affiche un stack trace le cas échéant.
La variable image contient alors une référence à un objet qui encapsule les données image. Vous
pouvez afficher l’image grâce à la méthode drawImage de la classe Graphics :
public void paintComponent(Graphics g)
{
. . .
g.drawImage(image, x, y, null);
}
L’Exemple 7.7 va un peu plus loin et affiche l’image en mosaïque dans la fenêtre. Le résultat ressemble à celui de la Figure 7.15. L’affichage en mosaïque est effectué dans la méthode paintComponent. Nous dessinons d’abord une copie de l’image dans le coin supérieur gauche, puis nous
appelons copyArea pour la copier dans toute la fenêtre :
for (int i = 0; i * imageWidth <= getWidth(); i++)
for (int j = 0; j * imageHeight <= getHeight(); j++)
if (i + j > 0)
g.copyArea(0, 0, imageWidth, imageHeight,
i * imageWidth, j * imageHeight);
Figure 7.15
Affichage d’une image
en mosaïque dans
une fenêtre.
Livre Java .book Page 320 Jeudi, 25. novembre 2004 3:04 15
320
Au cœur de Java 2 - Notions fondamentales
INFO
Pour charger une image avec le JDK 1.3 ou version antérieure, utilisez plutôt la classe MediaTracker. Un "pisteur
de média" peut suivre l’acquisition d’une ou plusieurs images (le terme "média" laisse supposer que cette classe peut
suivre également des fichiers audio ou d’autres supports. Ce sera peut-être le cas grâce à une extension, mais pour
l’instant la classe ne peut suivre que des images).
Nous ajoutons une image à un objet pisteur avec la commande suivante :
MediaTracker tracker = new MediaTracker(component);
Image img = Toolkit.getDefaultToolkit().getImage(name);
int id = 1; // l’ID pour pister le processus de chargement d’image
tracker.addImage(img, id);
Nous pouvons ajouter autant d’images que nous le souhaitons à un seul objet MediaTracker. Chaque image doit
avoir un numéro d’identification (ID) différent, mais nous pouvons choisir nous-mêmes ce numéro. Pour attendre le
chargement complet d’une image, nous employons des instructions comparables à celles-ci :
try { tracker.waitForID(id); }
catch (InterruptedException e) {}
Si vous désirez charger plusieurs images, vous pouvez toutes les ajouter à l’objet MediaTracker et demander au
programme d’attendre qu’elles soient toutes chargées :
try { tracker.waitForAll(); }
catch (InterruptedException e) {}
L’Exemple 7.7 présente le code source du programme de démonstration permettant d’afficher une
image. Ceci conclut notre introduction sur la programmation du graphisme dans Java. Pour découvrir
des techniques plus avancées, découvrez le graphisme et les images 2D dans le Volume 2.
Exemple 7.7 : ImageTest.java
import
import
import
import
import
java.awt.*;
java.awt.event.*;
java.io.*;
javax.imageio.*;
javax.swing.*;
public class ImageTest
{
public static void main(String[] args)
{
ImageFrame frame = new ImageFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec un panneau pour une image
*/
class ImageFrame extends JFrame
{
public ImageFrame()
{
setTitle("ImageTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
Livre Java .book Page 321 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
// ajouter un panneau au cadre
ImagePanel panel = new ImagePanel();
add(panel);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
/**
Un panneau qui affiche une image en mosaïque
*/
class ImagePanel extends JPanel
{
public ImagePanel()
{
// acquérir l’image
try
{
image = ImageIO.read(new File("blue-ball.gif"));
}
catch (IOException e)
{
e.printStackTrace();
}
}
public void paintComponent(Graphics g)
{
super.paintComponent(g);
if (image == null) return;
int imageWidth = image.getWidth(this);
int imageHeight = image.getHeight(this);
// dessiner l’image dans le coin supérieur gauche
g.drawImage(image, 0, 0, null);
// afficher l’image en mosaïque dans le panneau
for (int i = 0; i * imageWidth <= getWidth(); i++)
for (int j = 0; j * imageHeight <= getHeight(); j++)
if (i + j > 0)
g.copyArea(0, 0, imageWidth, imageHeight,
i * imageWidth, j * imageHeight);
}
private Image image;
}
javax.swing.ImageIO 1.4
•
•
static BufferedImage read(File f)
static BufferedImage read(URL u)
Renvoient une image à partir du fichier donné ou de l’URL.
321
Livre Java .book Page 322 Jeudi, 25. novembre 2004 3:04 15
322
Au cœur de Java 2 - Notions fondamentales
java.awt.Image 1.0
•
Graphics getGraphics()
Récupère un contexte graphique pour dessiner dans ce tampon d’image.
•
void flush()
Libère toutes les ressources détenues par cet objet image.
java.awt.Graphics 1.0
•
boolean drawImage(Image img, int x, int y, ImageObserver observer)
Dessine une image non mise à l’échelle. Remarque : cette méthode peut renvoyer un résultat
avant que l’image soit complètement dessinée.
Paramètres :
•
img
L’image à dessiner.
x
Coordonnée x du coin supérieur gauche.
y
Coordonnée y du coin supérieur gauche.
observer
L’objet qui doit être notifié en ce qui concerne la progression
de l’affichage (cette valeur peut être null).
boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver
observer)
Dessine une image mise à l’échelle. Le système adapte la taille de l’image afin qu’elle occupe
une zone ayant la largeur et la hauteur spécifiées. Remarque : cette méthode peut renvoyer un
résultat avant que l’image ne soit complètement dessinée.
Paramètres :
•
img
L’image à dessiner.
x
Coordonnée x du coin supérieur gauche.
y
Coordonnée y du coin supérieur gauche.
width
Largeur souhaitée pour l’image.
height
Hauteur souhaitée pour l’image.
observer
L’objet qui doit être notifié en ce qui concerne la progression
de l’affichage (cette valeur peut être null).
void copyArea(int x, int y, int width, int height, int dx, int dy)
Copie une zone rectangulaire de l’écran.
Paramètres :
•
x
Coordonnée x du coin supérieur gauche de la zone source.
y
Coordonnée y du coin supérieur gauche de la zone source.
width
Largeur de la zone source.
height
Hauteur de la zone source.
dx
Distance horizontale entre la zone source et la zone cible.
dy
Distance verticale entre la zone source et la zone cible.
void dispose()
Libère ce contexte graphique et les ressources associées du système d’exploitation. Il faut
toujours libérer les contextes graphiques obtenus par des appels à des méthodes comme
Image.getGraphics, mais pas les contextes fournis par paintComponent.
Livre Java .book Page 323 Jeudi, 25. novembre 2004 3:04 15
Chapitre 7
Programmation graphique
323
java.awt.Component 1.0
• Image createImage(int width, int height)
Crée un tampon d’image hors écran qui sera employé par le mécanisme de double tampon
(double buffering).
Paramètres :
width
Largeur de l’image.
height
Hauteur de l’image.
java.awt.MediaTracker 1.0
• MediaTracker(Component c)
Piste les images qui sont affichées dans le composant donné.
•
void addImage(Image image, int id)
Ajoute une image à la liste des images "pistées" (ou "suivies"). Le processus de chargement de
cette image démarre dès qu’elle est ajoutée à la liste.
Paramètres :
•
image
L’image à pister.
id
Identificateur employé pour les références ultérieures à cette
image.
void waitForID(int id)
Attend que toutes les images ayant l’identificateur spécifié soient chargées.
•
void waitForAll()
Attend que toutes les images "pistées" soient chargées.
Livre Java .book Page 324 Jeudi, 25. novembre 2004 3:04 15
Livre Java .book Page 325 Jeudi, 25. novembre 2004 3:04 15
8
Gestion des événements
Au sommaire de ce chapitre
✔ Introduction à la gestion des événements
✔ Hiérarchie des événements AWT
✔ Evénements sémantiques et de bas niveau
✔ Types d’événements de bas niveau
✔ Actions
✔ Multidiffusion
✔ Mise en place des sources d’événements
La gestion des événements est d’une importance capitale pour les programmes ayant une interface.
Pour implémenter des interfaces utilisateur graphiques, vous devez maîtriser la gestion des événements. Ce chapitre explique le fonctionnement du modèle d’événement AWT. Vous apprendrez à
capturer les événements en provenance de la souris et du clavier, ainsi que l’usage des éléments les
plus simples d’une interface, comme les boutons. Nous verrons en particulier comment utiliser les
événements de base générés par ces composants. Le chapitre suivant vous montrera comment intégrer à une application les autres composants proposés par Swing et examinera en détail les événements
qu’ils génèrent.
INFO
Java 1.1 a introduit un modèle d’événement très amélioré pour la programmation d’une GUI. Nous ne parlerons pas
du modèle d’événement déprécié 1.0 dans ce chapitre.
Livre Java .book Page 326 Jeudi, 25. novembre 2004 3:04 15
326
Au cœur de Java 2 - Notions fondamentales
Introduction à la gestion des événements
Tout environnement d’exploitation qui gère les interfaces utilisateur graphiques doit constamment détecter les événements tels que la pression sur une touche du clavier ou sur un bouton de
la souris. L’environnement d’exploitation en informe alors les programmes en cours d’exécution.
Chaque programme détermine ensuite s’il doit répondre à ces événements. Dans des langages
comme Visual Basic, la correspondance entre les événements et le code est évidente. Le
programmeur écrit une routine pour chaque événement digne d’intérêt et place ce code dans ce
que l’on appelle une procédure d’événement. Par exemple, un bouton nommé HelpButton serait
associé à une procédure d’événement HelpButton_Click. Le code de cette procédure s’exécute
en réponse à un clic de la souris sur ce bouton. En Visual Basic, chaque composant d’interface
répond à un ensemble fixe d’événements, et il est impossible de modifier les événements
auxquels il peut répondre.
En revanche, si vous employez un langage comme le C pur pour faire de la programmation
événementielle, vous devez écrire le code qui vérifie constamment la file d’événements (en général, ce code est imbriqué dans une boucle géante contenant une énorme instruction switch !).
Cette technique est évidemment peu élégante et difficile à programmer. Elle a cependant un
avantage : les événements auxquels l’application peut répondre ne sont pas limités comme dans
les langages qui s’efforcent de dissimuler la file d’événements au programmeur (tel Visual
Basic).
L’environnement de programmation Java a choisi une approche intermédiaire entre celle du C et
celle de Visual Basic, ce qui augmente sa complexité. Dans les limites des événements connus
d’AWT, vous contrôlez complètement la manière dont les événements sont transmis de la source
d’événement (par exemple, un bouton ou une barre de défilement) à l’écouteur d’événement (event
listener). Vous pouvez désigner n’importe quel objet comme écouteur d’événement — dans la pratique, vous choisissez un objet qui est capable de fournir une réponse appropriée à l’événement. Ce
modèle de délégation d’événement offre une flexibilité bien supérieure à celle de Visual Basic, où
l’écouteur est prédéterminé, mais il exige davantage de code et est plus difficile à analyser (tant que
son usage ne vous est pas familier).
Les sources d’événement ont des méthodes qui leur permettent d’enregistrer (ou de recenser) les
écouteurs d’événement. Lorsqu’un événement arrive à la source, celle-ci envoie une notification à
tous les objets écouteurs recensés pour cet événement.
Bien entendu, dans un langage orienté objet comme Java, l’information relative à l’événement est
encapsulée dans un objet événement. Tous les objets événement dérivent, directement ou indirectement, de la classe java.util.EventObject. Il existe évidemment des sous-classes pour chaque
type d’événement, comme ActionEvent et WindowEvent.
Différentes sources d’événement peuvent produire différentes sortes d’événements. Par exemple,
un bouton peut envoyer des objets ActionEvent alors qu’une fenêtre enverra des objets
WindowEvent.
Livre Java .book Page 327 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
327
Pour résumer, voici en gros comment les événements sont gérés avec AWT :
m
Un objet écouteur est une instance d’une classe qui implémente une interface spéciale appelée
interface écouteur (listener interface).
m
Une source d’événement est un objet qui peut recenser des objets écouteurs et leur envoyer des
objets événements.
m
Lorsqu’un événement se produit, la source d’événement envoie l’objet événement à tous les
écouteurs recensés.
m
Les objets écouteurs utilisent alors l’information contenue dans l’objet événement pour déterminer
leur réponse.
Pour recenser l’objet écouteur auprès de l’objet source, utilisez une instruction construite sur ce
modèle :
ObjetSourceEvénement.addEvénementListener(objetEcouteurEvénement);
Voici un exemple :
ActionListener listener = . . .;
JButton button = new JButton("Ok");
button.addActionListener(listener);
L’objet listener recevra désormais une notification chaque fois qu’un "événement Action" concernera le bouton button. Dans le cas d’un bouton, un événement Action est un clic de la souris sur ce
bouton.
L’exemple précédent exige que la classe à laquelle appartient l’objet écouteur implémente l’interface
appropriée (en l’occurrence, ActionListener). Comme pour toutes les interfaces Java, l’implémentation d’une interface signifie qu’il faut fournir des méthodes ayant la signature correcte. Pour implémenter l’interface ActionListener, la classe de l’écouteur doit posséder une méthode (nommée
actionPerformed) qui recevra l’objet ActionEvent comme paramètre :
class MyListener implements ActionListener
{
. . .
public void actionPerformed(ActionEvent event)
{
// la réaction à un clic sur le bouton, ici
. . .
}
}
Chaque fois que l’utilisateur clique sur le bouton, l’objet JButton crée un objet ActionEvent et
appelle listener.actionPerformed(event) en lui passant cet objet événement. Il est possible
d’ajouter plusieurs objets en tant qu’écouteurs d’une source d’événement, par exemple un bouton.
Dans ce cas, le bouton appelle les méthodes actionPerformed de tous les écouteurs, chaque fois
que l’utilisateur clique sur le bouton.
Livre Java .book Page 328 Jeudi, 25. novembre 2004 3:04 15
328
Au cœur de Java 2 - Notions fondamentales
La Figure 8.1 montre l’interaction entre la source de l’événement, l’écouteur d’événement et l’objet
événement.
Figure 8.1
Notification
d’événement.
MyPanel
new
new
JButton
MyListener
addActionListener
actionPerformed
INFO
Dans ce chapitre, nous insistons particulièrement sur la gestion des événements de l’interface utilisateur tels que les
clics sur des boutons et les déplacements de la souris. Cependant, l’architecture de gestion d’événements de base
n’est pas limitée aux interfaces utilisateur. Comme vous le verrez au chapitre suivant, les objets qui ne constituent pas
des composants d’interface utilisateur peuvent aussi envoyer des notifications d’événements aux écouteurs, généralement pour leur indiquer qu’une propriété de l’objet a changé.
Exemple : gestion d’un clic de bouton
Pour mieux comprendre le modèle de délégation d’événement, examinons ce qu’il nous faut pour
construire un simple exemple de réponse à un clic sur un bouton :
m
un panneau contenant trois boutons ;
m
trois objets écouteurs qui sont ajoutés comme écouteurs d’action des boutons.
Livre Java .book Page 329 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
329
Dans cet exemple, chaque fois que l’utilisateur clique sur un des boutons du panneau, l’objet écouteur associé reçoit un objet ActionEvent indiquant un clic sur un bouton. Dans notre exemple,
l’objet écouteur change alors la couleur d’arrière-plan du panneau.
Avant de vous montrer le programme à l’écoute des clics sur les boutons, nous devons expliquer
comment créer des boutons et les ajouter au panneau (pour d’autres informations sur les éléments
d’une interface graphique, reportez-vous au Chapitre 9).
Pour créer un bouton, nous spécifions une chaîne de libellé ou une icône (ou les deux) dans le
constructeur du bouton. Voici deux exemples :
JButton yellowButton = new JButton("Yellow");
JButton blueButton = new JButton(new ImageIcon("blue-ball.gif"));
Les boutons sont ajoutés au panneau par des appels à une méthode nommée add. Celle-ci reçoit en
paramètre le composant qui doit être ajouté au conteneur. Par exemple :
class ButtonPanel extends JPanel
{
public ButtonPanel()
{
JButton yellowButton = new JButton("Yellow");
JButton blueButton = new JButton("Blue");
JButton redButton = new JButton("Red");
add(yellowButton);
add(blueButton);
add(redButton);
}
}
Le résultat est montré à la Figure 8.2.
Figure 8.2
Un panneau contenant
des boutons.
Maintenant que vous savez ajouter des boutons à un panneau, il vous faut écrire le code qui
permet au panneau d’écouter ces boutons. Cela nécessite des classes implémentant l’interface
ActionListener qui ne possède qu’une seule méthode, nommée actionPerformed, dont voici
la signature :
public void actionPerformed(ActionEvent event)
Livre Java .book Page 330 Jeudi, 25. novembre 2004 3:04 15
330
Au cœur de Java 2 - Notions fondamentales
INFO
L’interface ActionListener utilisée dans notre exemple n’est pas limitée aux clics sur des boutons. Elle peut être
utilisée dans bien d’autres situations, par exemple :
• lors de la sélection d’un élément de menu ;
• lors de la sélection d’un élément dans une zone de liste à l’aide d’un double-clic ;
• lorsque la touche "Entrée" est activée dans un champ de texte ;
• lorsqu’un composant Timer déclenche une impulsion après l’écoulement d’un temps donné.
Nous reviendrons sur cette interface dans la suite de ce chapitre et dans le suivant.
ActionListener s’utilise de la même manière dans toutes les situations : sa méthode (unique) actionPerformed
reçoit en paramètre un objet de type ActionEvent. Cet objet événement fournit des informations sur l’événement
qui a été déclenché.
Lorsqu’un bouton est cliqué, nous voulons modifier la couleur d’arrière-plan du panneau contenant
les boutons, et nous stockons la couleur choisie dans notre classe écouteur :
class ColorAction implements ActionListener
{
public ColorAction(Color c)
{
backgroundColor = c;
}
public void actionPerformed(ActionEvent event)
{
// définir la couleur d’arrière-plan du panneau
. . .
}
private Color backgroundColor;
}
Nous construisons ensuite un objet pour chaque couleur et les définissons comme écouteurs des
boutons :
ColorAction yellowAction = new ColorAction(Color.YELLOW);
ColorAction blueAction = new ColorAction(Color.BLUE);
ColorAction redAction = new ColorAction(Color.RED);
yellowButton.addActionListener(yellowAction);
blueButton.addActionListener(blueAction);
redButton.addActionListener(redAction);
Par exemple, si un utilisateur clique sur un bouton marqué "Yellow", la méthode actionPerformed
de l’objet yellowAction est appelée. Son champ d’instance backgroundColor est défini à
Color.YELLOW, et la méthode peut alors poursuivre la définition de la couleur d’arrière-plan du
panneau.
Il reste un problème. L’objet ColorAction n’a pas accès à la variable panel. Vous pouvez résoudre
ce problème de deux façons. Vous pouvez stocker le panneau dans l’objet ColorAction et le définir
dans le constructeur de ColorAction. Ou, mieux encore, vous pouvez construire l’objet ColorAction dans une classe interne de la classe ButtonPanel. Ses méthodes peuvent alors accéder au
Livre Java .book Page 331 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
331
panneau externe automatiquement (pour plus d’informations sur les classes internes, voir le
Chapitre 6).
Nous allons adopter la seconde approche. Voici comment placer la classe ColorAction à l’intérieur
de la classe ButtonPanel :
class ButtonPanel extends JPanel
{
. . .
private class ColorAction implements ActionListener
{
. . .
public void actionPerformed(ActionEvent event)
{
setBackground(backgroundColor);
// c’est-à-dire outer.setBackground(...)
}
private Color backgroundColor;
}
}
Examinez attentivement la méthode actionPerformed. La classe ColorAction n’a pas de méthode
setBackground. Mais la classe externe ButtonPanel en possède. Les méthodes sont invoquées sur
l’objet ButtonPanel qui a construit les objets de la classe interne (notez bien, ici encore, que outer
n’est pas un mot clé du langage de programmation Java. Nous l’employons simplement comme
symbole intuitif pour la référence invisible de classe externe dans l’objet de la classe interne).
Cette situation est très courante. Les objets écouteurs d’événement ont habituellement besoin de
réaliser une action qui affecte d’autres objets. Vous pouvez souvent placer stratégiquement la classe
écouteur à l’intérieur de la classe dont l’état doit être modifié par l’écouteur.
L’Exemple 8.1 contient la totalité du programme. Chaque fois que vous cliquez sur l’un des boutons,
l’écouteur d’action approprié modifie la couleur d’arrière-plan du panneau.
Exemple 8.1 : ButtonTest.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ButtonTest
{
public static void main(String[] args)
{
ButtonFrame frame = new ButtonFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec un panneau contenant des boutons
Livre Java .book Page 332 Jeudi, 25. novembre 2004 3:04 15
332
Au cœur de Java 2 - Notions fondamentales
*/
class ButtonFrame extends JFrame
{
public ButtonFrame()
{
setTitle("ButtonTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter le panneau au cadre
ButtonPanel panel = new ButtonPanel();
add(panel);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
/**
Un panneau avec trois boutons.
*/
class ButtonPanel extends JPanel
{
public ButtonPanel()
{
// créer les boutons
JButton yellowButton = new JButton("Yellow");
JButton blueButton = new JButton("Blue");
JButton redButton = new JButton("Red");
// ajouter les boutons au panneau
add(yellowButton);
add(blueButton);
add(redButton);
// créer les actions des boutons
ColorAction yellowAction = new ColorAction(Color.YELLOW);
ColorAction blueAction = new ColorAction(Color.BLUE);
ColorAction redAction = new ColorAction(Color.RED);
// associer les actions aux boutons
yellowButton.addActionListener(yellowAction);
blueButton.addActionListener(blueAction);
redButton.addActionListener(redAction);
}
/**
Un écouteur d’action qui définit la couleur
d’arrière-plan du panneau.
*/
private class ColorAction implements ActionListener
{
public ColorAction(Color c)
Livre Java .book Page 333 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
333
{
backgroundColor = c;
}
public void actionPerformed(ActionEvent event)
{
setBackground(backgroundColor);
}
private Color backgroundColor;
}
}
javax.swing.JButton 1.2
•
JButton(String label)
Construit un bouton. La chaîne du label peut être du texte pur, ou HTML à partir du JDK 1.3 ;
par exemple, "<html><b>Ok</b></html>".
Paramètres :
•
label
Le texte du libellé affiché sur le bouton.
JButton(Icon icon)
Construit un bouton.
Paramètres :
•
icon
L’icône affichée sur le bouton.
JButton(String label, Icon icon)
Construit un bouton.
Paramètres :
label
Le texte du libellé affiché sur le bouton.
iconL’icône affichée sur le bouton.
java.awt.Container 1.0
•
Component add(Component c)
Ajoute le composant c à ce conteneur.
javax.swing.ImageIcon 1.2
•
ImageIcon(String filename)
Construit une icône dont l’image est stockée dans un fichier. L’image est chargée automatiquement
avec un pisteur de média (media tracker, voir Chapitre 7).
Etre confortable avec les classes internes
Certains détestent les classes internes, car ils ont l’impression qu’une prolifération de classes et
d’objets ralentit les programmes. Voyons ce qu’il en est. Vous n’avez pas besoin d’une nouvelle
classe pour chaque composant de l’interface utilisateur. Dans notre exemple, les trois boutons partagent la même classe écouteur. Bien sûr, chacun d’eux a un objet écouteur séparé, mais ces objets ne
sont pas gros. Ils contiennent chacun une valeur de couleur et une référence au panneau. Et la solution traditionnelle, avec des instructions if . . . else, référence les mêmes objets couleur que
ceux stockés par les écouteurs d’action, comme des variables locales et non comme des champs
d’instance.
Livre Java .book Page 334 Jeudi, 25. novembre 2004 3:04 15
334
Au cœur de Java 2 - Notions fondamentales
Il est temps d’avoir recours aux classes internes. Il est recommandé d’utiliser des classes internes
dédiées pour les gestionnaires d’événement plutôt que de transformer les classes existantes en écouteurs.
Nous pensons que même les classes internes anonymes ont leur place.
Voici un bon exemple de la façon dont les classes internes anonymes peuvent réellement simplifier
votre code. Si vous examinez le code de l’Exemple 8.1, vous verrez que chaque bouton requiert le
même traitement :
1. construire le bouton avec un libellé ;
2. ajouter le bouton au panneau ;
3. construire un écouteur d’action avec la couleur appropriée ;
4. ajouter cet écouteur d’action.
Implémentons une méthode "assistante" (helper) pour simplifier ces tâches :
void makeButton(String name, Color backgroundColor)
{
JButton button = new JButton(name);
add(button);
ColorAction action = new ColorAction(backgroundColor);
button.addActionListener(action);
}
Le constructeur de ButtonPanel devient alors simplement :
public ButtonPanel()
{
makeButton("yellow", Color.YELLOW);
makeButton("blue", Color.BLUE);
makeButton("red", Color.RED);
}
Vous pouvez encore simplifier. Notez que la classe ColorAction n’est nécessaire qu’une fois, dans
la méthode makeButton. Vous pouvez donc en faire une classe anonyme :
void makeButton(String name, final Color backgroundColor)
{
JButton button = new JButton(name);
add(button);
button.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
setBackground(backgroundColor);
}
});
}
Le code de l’écouteur d’action a été simplifié. La méthode actionPerformed fait simplement référence à la variable paramètre backgroundColor (comme pour toutes les variables locales auxquelles
on accède dans la classe interne, le paramètre doit être déclaré comme final).
Aucun constructeur explicite n’est nécessaire. Comme vous avez vu au Chapitre 6, le mécanisme de
classe interne génère automatiquement un constructeur qui stocke toutes les variables final locales
qui sont utilisées dans l’une des méthodes de la classe interne.
Livre Java .book Page 335 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
335
ASTUCE
Les classes internes anonymes peuvent paraître déroutantes. Mais vous pouvez vous habituer à les déchiffrer en vous
entraînant à analyser le code de la façon suivante :
button.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
setBackground(backgroundColor);
}
});
C’est-à-dire que l’action du bouton consiste à définir une couleur d’arrière-plan. Tant que le gestionnaire d’événement est constitué simplement de quelques instructions, cela reste très lisible, en particulier si vous ne vous préoccupez pas des mécanismes de classe interne.
ASTUCE
Le JDK 1.4 introduit un mécanisme qui vous permet de spécifier des écouteurs d’événement simples sans programmer de classes internes. Supposons par exemple que vous ayez un bouton intitulé Load dont le gestionnaire d’événement contient un seul appel de méthode :
frame.loadData();
Vous pouvez bien entendu utiliser une classe interne anonyme :
loadButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
frame.loadData();
}
});
Or la classe EventHandler peut créer automatiquement cet écouteur, par l’appel
EventHandler.create(ActionListener.class, frame, "loadData")
Vous devrez bien sûr installer malgré tout le gestionnaire :
loadButton.addActionListener((ActionListener)
EventHandler.create(ActionListener.class, frame, "loadData"));
Le transtypage est nécessaire car la méthode create renvoie un Object. Une future version du JDK profitera peutêtre pleinement des types génériques, pour rendre cette méthode encore plus commode.
Si l’écouteur d’événement appelle une méthode avec un seul paramètre, dérivé du gestionnaire d’événement, vous
pouvez utiliser une autre forme de la méthode create. Par exemple, l’appel
EventHandler.create(ActionListener.class, frame, "loadData", "source.text")
équivaut à
new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
frame.loadData(((JTextField) event.getSource()).getText());
}
}
Livre Java .book Page 336 Jeudi, 25. novembre 2004 3:04 15
336
Au cœur de Java 2 - Notions fondamentales
Vous remarquerez que le gestionnaire d’événement transforme les noms de la source des propriétés et le texte en
appels de méthode getSource et getText, à l’aide de la convention JavaBeans (pour en savoir plus sur les propriétés
et les composants JavaBeans, consultez le Volume 2).
Toutefois, dans la pratique, cette situation n’est pas fréquente, et il n’existe pas de mécanisme pour fournir des paramètres qui ne soient pas dérivés de l’objet événement.
Transformer des composants en écouteurs d’événement
Vous êtes tout à fait libre de désigner n’importe quel objet d’une classe qui implémente l’interface
ActionListener comme écouteur du bouton. Nous préférons utiliser les objets d’une nouvelle
classe qui a été expressément créée pour réaliser les actions souhaitées. De nombreux programmeurs
ne sont pas très à l’aise avec les classes internes et choisissent une stratégie différente. Ils retrouvent
le composant qui est modifié en conséquence de l’événement, amènent ce composant à implémenter
l’interface ActionListener et ajoutent une méthode actionPerformed. Dans notre exemple, vous
pouvez transformer ButtonPanel en écouteur d’action :
class ButtonPanel extends JPanel implements ActionListener
{
. . .
public void actionPerformed(ActionEvent event)
{
// définir la couleur d’arrière-plan
. . .
}
}
Puis le panneau se définit lui-même comme l’écouteur des trois boutons :
yellowButton.addActionListener(this);
blueButton.addActionListener(this);
redButton.addActionListener(this);
Notez que, maintenant, les trois boutons n’ont plus d’écouteurs individuels. Ils partagent un unique
objet écouteur, le panneau avec les boutons. Par conséquent, la méthode actionPerformed doit
déterminer sur quel bouton il a été cliqué.
La méthode getSource de la classe EventObject, la superclasse de toutes les autres classes
d’événement, va vous indiquer la source de chaque événement. La source de l’événement est l’objet
qui a généré l’événement et notifié l’écouteur :
Object source = event.getSource();
La méthode actionPerformed peut alors vérifier lequel des boutons était la source :
if (source == yellowButton) . . .
else if (source == blueButton) . . .
else if (source == redButton ) . . .
Cette approche requiert bien entendu que vous conserviez les références aux boutons en tant que
champs d’instance dans le panneau qui les contient.
Comme vous pouvez le voir, transformer le panneau du bouton en écouteur d’action n’est pas forcément plus simple que définir une classe interne. Cela devient aussi vraiment encombré lorsque le
panneau contient plusieurs éléments d’interface utilisateur.
Livre Java .book Page 337 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
337
ATTENTION
Certains programmeurs utilisent une méthode différente pour déterminer la source de l’événement dans un objet
écouteur partagé par plusieurs sources. La classe ActionEvent possède une méthode getActionCommand qui
renvoie la chaîne de commande associée à cette action. Pour les boutons, il s’avère que la chaîne de commande est,
par défaut, le label du bouton. Si vous adoptez cette approche, une méthode actionPerformed contiendra un
code comme celui-ci :
String command = event.getActionCommand();
if (command.equals("Yellow")) . . .;
else if (command.equals("Blue")) . . .;
else if (command.equals("Red")) . . .;
Nous vous recommandons de ne pas adopter cette approche. Bien sûr, se fier aux libellés des boutons est dangereux.
Il est très facile de baptiser un bouton "Gray" puis d’épeler la chaîne de manière légèrement différente dans le test :
if (command.equals("Grey")) . . .
De plus, les labels de boutons posent des problèmes si vous internationalisez votre application. Pour réaliser la
version allemande avec les libellés de boutons "Gelb", "Blau" et "Rot", vous devrez modifier à la fois les labels de
boutons et les chaînes dans la méthode actionPerformed.
java.util.EventObject 1.1
•
Object getSource()
Renvoie une référence sur l’objet où l’événement s’est produit.
java.awt.event.ActionEvent 1.1
•
String getActionCommand()
Renvoie la chaîne de commande associée à cet événement action. Si l’événement a été déclenché
par un bouton, la chaîne de commande contient le libellé du bouton, à moins d’un changement
intervenu par le biais de la méthode setActionCommand.
java.beans.EventHandler 1.4
•
•
static Object create(Class listenerInterface, Object target, String action)
•
static Object create(Class listenerInterface, Object target, String action,
String eventProperty, String listenerMethod)
static Object create(Class listenerInterface, Object target, String action,
String eventProperty)
Ces méthodes construisent un objet d’une classe proxy qui implémente l’interface donnée. La
méthode nommée ou toutes les méthodes de l’interface réalisent l’action donnée sur l’objet
cible.
L’action peut être un nom de méthode ou une propriété de la cible. S’il s’agit d’une propriété, sa
méthode setter est exécutée. Par exemple, une action "text" est transformée en appel de la
méthode setText.
La propriété d’événement est constituée d’un ou de plusieurs noms de propriété séparés par un
point. La première propriété est lue depuis le paramètre de la méthode écouteur. La deuxième est
lue à partir de l’objet de résultat, etc. Le résultat définitif devient le paramètre de l’action. Par
exemple, la propriété "source.text" est transformée en appels aux méthodes getSource et
getText.
Livre Java .book Page 338 Jeudi, 25. novembre 2004 3:04 15
338
Au cœur de Java 2 - Notions fondamentales
Exemple : modification du "look and feel"
Par défaut, les programmes Swing utilisent l’aspect et le comportement (look and feel) Metal. Il y a
deux moyens de choisir un look and feel différent. Le premier consiste à placer, dans les répertoires
jre/lib, un fichier swing.properties qui donne à la propriété swing.defaultlaf de la classe le
look and feel que vous désirez employer. Par exemple :
swing.defaultlaf=com.sun.java.swing.plaf.motif.MotifLookAndFeel
Notez que le look and feel Metal est situé dans le package javax.swing. Les autres look and feel se
trouvent dans le package com.sun.java et ne doivent pas nécessairement être présents dans chaque
implémentation de Java. Actuellement, pour des raisons de copyright, les look and feel Windows et
Mac sont uniquement fournis avec les versions Windows ou Mac de l’environnement d’exécution de
Java.
ASTUCE
Une petite astuce permet de vérifier la présence des look and feel disponibles. Comme les lignes débutant par le
caractère # sont ignorées dans les fichiers de propriétés, vous pouvez spécifier plusieurs look and feel dans le fichier
swing.properties et sélectionner celui que vous désirez en supprimant le caractère #, de la façon suivante :
#swing.defaultlaf=javax.swing.plaf.metal.MetalLookAndFeel
swing.defaultlaf=com.sun.java.swing.plaf.motif.MotifLookAndFeel
#swing.defaultlaf=com.sun.java.swing.plaf.windows.WindowsLookAndFeel
Vous devez relancer votre programme pour modifier le look and feel de cette manière. Un programme Swing ne lit
qu’une seule fois le fichier swing.properties, au démarrage.
La deuxième méthode consiste à modifier dynamiquement le look and feel. Appelez la méthode
statique UIManager.setLookAndFeel et passez-lui le nom du look and feel souhaité. Appelez
ensuite la méthode statique SwingUtilities.updateComponentTreeUI pour actualiser l’ensemble
des composants. Vous n’avez besoin de fournir qu’un seul composant à cette méthode ; elle trouvera
tous les autres. La méthode UIManager.setLookAndFeel peut lancer un certain nombre d’exceptions si le look and feel recherché n’est pas trouvé ou si une erreur survient durant son chargement.
Comme d’habitude, nous vous conseillons de ne pas vous préoccuper des exceptions pour l’instant ;
elles seront détaillées au Chapitre 11.
Voici un exemple permettant de passer au look and feel Motif :
String plaf = "com.sun.java.swing.plaf.motif.MotifLookAndFeel";
try
{
UIManager.setLookAndFeel(plaf);
SwingUtilities.updateComponentTreeUI(panel);
}
catch(Exception e) { e.printStackTrace(); }
Pour énumérer toutes les implémentations de look and feel installées, appelez
UIManager.LookAndFeelInfo[] infos = UIManager.getInstalledLookAndFeels();
Vous obtenez ensuite le nom et le nom de classe pour chaque look and feel sous la forme
String name = infos[i].getName();
String className = infos[i].getClassName();
Livre Java .book Page 339 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
339
L’Exemple 8.2 est un programme de démonstration qui montre comment changer de look and feel
(voir Figure 8.3). Ce programme ressemble beaucoup à celui de l’Exemple 8.1. Conformément aux
explications de la précédente section, nous employons une méthode assistante makeButton et une
classe interne anonyme pour spécifier l’action du bouton, c’est-à-dire le changement de look and
feel.
Il y a une subtilité dans ce programme. La méthode actionPerformed de la classe écouteur d’action
interne doit pouvoir passer la référence this de la classe outer PlafPanel à la méthode updateComponentTreeUI. Souvenez-vous, nous avons vu au Chapitre 6 que le pointeur this de l’objet
outer doit avoir pour préfixe le nom de la classe externe :
SwingUtilities.updateComponentTreeUI( PlafPanel.this);
Figure 8.3
Modification
du look and feel.
Exemple 8.2 : PlafTest.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class PlafTest
{
public static void main(String[] args)
{
PlafFrame frame = new PlafFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec un panneau et des boutons pour
changer le "look and feel"
*/
class PlafFrame extends JFrame
Livre Java .book Page 340 Jeudi, 25. novembre 2004 3:04 15
340
Au cœur de Java 2 - Notions fondamentales
{
public PlafFrame()
{
setTitle("PlafTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter un panneau au cadre
PlafPanel panel = new PlafPanel();
add(panel);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
/**
Un panneau avec des boutons pour changer le look and feel
*/
class PlafPanel extends JPanel
{
public PlafPanel()
{
UIManager.LookAndFeelInfo[] infos =
UIManager.getInstalledLookAndFeels();
for (UIManager.LookAndFeelInfo info : infos)
makeButton(info.getName(), info.getClassName());
}
/**
Construit un bouton pour changer le look and feel.
@param name Le nom du bouton
@param plafName Le nom de la classe look and feel
*/
void makeButton(String name, final String plafName)
{
// ajouter un bouton au panneau
JButton button = new JButton(name);
add(button);
// définir l’action du bouton
button.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
// action du bouton : passer au nouveau look and feel
try
{
UIManager.setLookAndFeel(plafName);
Livre Java .book Page 341 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
341
SwingUtilities.updateComponentTreeUI
(PlafPanel.this);
}
catch(Exception e) { e.printStackTrace(); }
}
});
}
}
Exemple : capture des événements de fenêtre
Tous les événements ne sont pas aussi simples à gérer que les clics de boutons. Voici un exemple
plus complexe que nous avons déjà mentionné au Chapitre 7. Avant l’apparition de l’option EXIT_
ON_CLOSE, dans le JDK 1.3, les programmeurs devaient quitter manuellement le programme lors de
la fermeture du cadre principal. Dans un programme professionnel, vous le ferez également pour que
le programme ne soit fermé qu’après vous être assuré que l’utilisateur ne perdra pas de travail. Par
exemple, vous pouvez souhaiter afficher une boîte de dialogue lorsque l’utilisateur ferme le cadre,
pour l’avertir si un travail non sauvegardé risque d’être perdu, et ne sortir qu’après confirmation de
l’utilisateur.
Quand l’utilisateur tente de fermer un cadre, l’objet JFrame est la source d’un événement
WindowEvent. Nous devons avoir un objet écouteur approprié et l’ajouter à la liste des écouteurs
de fenêtre :
WindowListener listener = . . .;
frame.addWindowListener(listener);
L’écouteur de fenêtre doit être un objet d’une classe implémentant l’interface WindowListener, qui
possède sept méthodes. Le cadre les appelle en réponse aux sept événements distincts qui peuvent se
produire dans une fenêtre. Voici l’interface complète de WindowListener :
public interface WindowListener
{
void windowOpened(WindowEvent e);
void windowClosing(WindowEvent e);
void windowClosed(WindowEvent e);
void windowIconified(WindowEvent e);
void windowDeiconified(WindowEvent e);
void windowActivated(WindowEvent e);
void windowDeactivated(WindowEvent e);
}
INFO
Pour savoir si une fenêtre a été maximisée, vous devez installer un WindowStateListener. Voyez les prochaines
notes API pour en savoir plus.
Comme toujours en Java, toute classe qui implémente une interface doit implémenter toutes les
méthodes de cette interface ; dans ce cas, cela signifie que les sept méthodes doivent être implémentées.
Une seule nous intéresse en l’occurrence : la méthode windowClosing.
Livre Java .book Page 342 Jeudi, 25. novembre 2004 3:04 15
342
Au cœur de Java 2 - Notions fondamentales
Nous pouvons bien sûr définir une classe qui implémente l’interface, ajouter un appel à
System.exit(0) dans la méthode windowClosing et fournir des blocs vides pour les six autres
méthodes :
class Terminator implements WindowListener
{
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
public
public
public
public
public
public
void
void
void
void
void
void
windowOpened(WindowEvent e) {}
windowClosed(WindowEvent e) {}
windowIconified(WindowEvent e) {}
windowDeiconified(WindowEvent e) {}
windowActivated(WindowEvent e) {}
windowDeactivated(WindowEvent e) {}
}
Classes adaptateurs
Il est fastidieux d’écrire les signatures de six méthodes qui ne font rien. Pour simplifier la tâche
du programmeur, chacune des interfaces AWT possédant plusieurs méthodes est accompagnée
d’une classe adaptateur qui implémente toutes les méthodes de l’interface en leur attribuant des
instructions vides. Par exemple, la classe WindowAdapter possède sept méthodes qui ne font
rien. Cela signifie que la classe adaptateur satisfait automatiquement aux exigences techniques
imposées par Java pour l’implémentation de l’interface écouteur qui lui est associée. Nous
pouvons étendre la classe adaptateur afin de spécifier les réactions souhaitées pour certains
événements, mais sans avoir besoin de répondre explicitement à tous les événements de l’interface (une interface comme ActionListener, qui ne possède qu’une seule méthode, n’a pas
besoin de classe adaptateur).
Profitons de cette caractéristique et utilisons l’adaptateur de Window. Nous pouvons étendre la classe
WindowAdapter, héritant ainsi de six méthodes qui ne font rien, et nous contenter de surcharger la
méthode WindowClosing :
class Terminator extends WindowAdapter
{
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
}
Nous pouvons maintenant recenser un objet de type Terminator en tant qu’écouteur d’événement :
WindowListener listener = new Terminator();
frame.addWindowListener(listener);
Chaque fois que le cadre déclenchera un événement de fenêtre, il passera cet événement à l’objet
listener en appelant une de ses sept méthodes (voir Figure 8.4). Six d’entre elles ne feront rien ; la
méthode windowClosing appelle System.exit(0) pour terminer l’exécution de l’application.
Livre Java .book Page 343 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
343
ATTENTION
Si vous avez mal orthographié le nom d’une méthode lors de l’extension d’une classe d’adaptateur, le compilateur
n’interceptera pas votre erreur. Si vous définissez par exemple une méthode windowIsClosing dans une classe
WindowAdapter, vous obtenez une classe avec huit méthodes, et la méthode windowClosing ne fait rien.
Figure 8.4
Un écouteur de fenêtre.
MyFrame
new
Terminator
addWindowListener
windowClosing
windowClosed
La création d’une classe écouteur dérivée de WindowAdapter représente une nette amélioration,
mais nous pouvons aller encore plus loin, car il n’est pas nécessaire de donner un nom à l’objet
listener. Ecrivons simplement :
frame.addWindowListener(new Terminator());
Et pourquoi s’arrêter en si bon chemin ? Nous pouvons faire de la classe écouteur une classe interne
anonyme du cadre :
frame.addWindowListener(new
WindowAdapter()
{
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
});
Voici les opérations effectuées par cet extrait de code :
m
Il définit une classe anonyme qui étend la classe WindowAdapter.
Livre Java .book Page 344 Jeudi, 25. novembre 2004 3:04 15
344
Au cœur de Java 2 - Notions fondamentales
m
Il ajoute une méthode windowClosing à cette classe anonyme (cette méthode termine l’application).
m
Il hérite des six autres méthodes "vides" de WindowAdapter.
m
Il crée un objet de cette classe. L’objet est lui-même anonyme.
m
Il passe cet objet à la méthode addWindowListener.
Comme indiqué, la syntaxe pour l’utilisation de classes internes anonymes demande une certaine
habitude. L’intérêt est que le code résultant est aussi court que possible.
java.awt.event.WindowListener 1.1
• void windowOpened(WindowEvent e)
Cette méthode est appelée lorsque la fenêtre a été ouverte.
•
void windowClosing(WindowEvent e)
Cette méthode est appelée lorsque l’utilisateur a émis une commande de gestionnaire de fenêtre
pour fermer la fenêtre. Sachez que la fenêtre ne se fermera qu’avec un appel à sa méthode hide
ou dispose.
•
void windowClosed(WindowEvent e)
Cette méthode est appelée lorsque la fenêtre s’est fermée.
•
void windowIconified(WindowEvent e)
Cette méthode est appelée une fois que la fenêtre a été transformée en icône.
•
void windowDeiconified(WindowEvent e)
Cette méthode est appelée lorsque la fenêtre n’est plus à l’état d’icône.
•
void windowActivated(WindowEvent e)
Cette méthode est appelée lorsque la fenêtre est devenue active. Seul un cadre ou une boîte de
dialogue peut être actif. Généralement, le gestionnaire de fenêtres décore la fenêtre active, par
exemple en surlignant sa barre de titre.
•
void windowDeactivated(WindowEvent e)
Cette méthode est appelée lorsque la fenêtre a été désactivée.
java.awt.event.WindowStateListener 1.4
• void windowStateChanged(WindowEvent event)
Cette méthode est appelée lorsque la fenêtre a été maximisée, transformée en icône ou restaurée
à sa taille normale.
java.awt.event.WindowEvent 1.1
• int getNewState() 1.4
• int getOldState() 1.4
Ces méthodes renvoient l’ancien état et le nouvel état d’une fenêtre dans un événement de modification de l’état de la fenêtre. L’entier renvoyé correspond à l’une des valeurs suivantes :
Frame.NORMAL
Frame.ICONIFIED
Frame.MAXIMIZED_HORIZ
Frame.MAXIMIZED_VERT
Frame.MAXIMIZED_BOTH
Livre Java .book Page 345 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
345
Hiérarchie des événements AWT
Après vous avoir donné un avant-goût de la gestion des événements, nous allons étudier la gestion
des événements Java d’une manière plus générale. Comme nous l’avons déjà écrit, la gestion des
événements est orientée objet et tous les événements descendent de la classe EventObject du
package java.util (la superclasse commune ne s’appelle pas Event, car ce nom est employé pour
désigner la classe d’événement dans l’ancien modèle. Bien que celui-ci soit déprécié, ses classes
font toujours partie de la bibliothèque Java).
La classe EventObject possède une sous-classe AWTEvent qui est le parent de toutes les classes
d’événements AWT. La Figure 8.5 présente le schéma de l’héritage des événements AWT.
Certains composants Swing génèrent des objets événements qui appartiennent à d’autres types qui
sont directement dérivés d’EventObject (et pas d’AWTEvent).
Figure 8.5
Event
Object
Schéma
de l’héritage
des classes
d’événements
AWT.
AWT Event
Action
Event
Adjustment
Event
Component
Event
Item
Event
Focus
Event
Input
Event
Paint
Event
Key
Event
Mouse
Event
MouseWheel
Event
Window
Event
Livre Java .book Page 346 Jeudi, 25. novembre 2004 3:04 15
346
Au cœur de Java 2 - Notions fondamentales
Un objet événement encapsule des informations sur l’événement que la source d’événement
communique aux écouteurs. En cas de besoin, nous pouvons alors analyser l’objet événement qui a
été passé à l’objet écouteur, comme nous l’avons fait à l’aide des méthodes getSource et getActionCommand dans l’exemple consacré aux boutons.
Certaines classes d’événements AWT ne sont d’aucune utilité pratique pour le programmeur Java.
Par exemple, AWT insère des objets PaintEvent dans la file d’événements, mais ces objets ne sont
pas envoyés aux écouteurs. Les programmeurs Java n’écoutent pas les événements de dessin ; ils
doivent surcharger la méthode paintComponent pour contrôler le dessin d’un composant. AWT
génère également plusieurs événements qui ne sont nécessaires qu’aux programmeurs système, afin
de fournir les systèmes de saisie des langues idéographiques, les robots de test automatisé, etc. Nous
ne traiterons pas de ces types d’événements spécialisés. Nous omettrons enfin les événements associés
aux composants AWT obsolètes.
Voici une liste des types d’événements AWT souvent utilisés.
ActionEvent
AdjustmentEvent
FocusEvent
ItemEvent
KeyEvent
MouseEvent
MouseWheelEvent
WindowEvent
Nous verrons des exemples d’utilisation de ces types d’événements dans ce chapitre et dans le
suivant.
La package javax.swing.event contient d’autres événements spécifiques aux composants Swing.
Nous les étudierons au Chapitre 9.
Les interfaces suivantes permettent d’écouter ces événements. :
ActionListener
AdjustmentListener
FocusListener
ItemListener
KeyListener
Mouse Listener
MouseMotionListener
MouseWheelListener
WindowListener
WindowFocusListener
WindowStateListener
Vous avez déjà rencontré dans cet ouvrage les interfaces ActionListener et WindowListener.
Bien que le package javax.swing.event contienne d’autres interfaces écouteur spécifiques
aux composants Swing, il utilise les écouteurs AWT de base pour le traitement général des
événements.
Plusieurs interfaces écouteur AWT — celles qui possèdent plusieurs méthodes — sont accompagnées d’une classe adaptateur qui implémente toutes les méthodes de l’interface afin qu’elles
n’accomplissent aucune action (les autres interfaces n’ont qu’une seule méthode et il est donc
inutile d’employer des classes adaptateur dans ce cas). Voici les classes adaptateur les plus
souvent utilisées :
FocusAdapter
KeyAdapter
MouseAdapter
MouseMotionAdapter
WindowAdapter
Il faut manifestement connaître un grand nombre de classes et d’interfaces — ce qui peut paraître
insurmontable à première vue. Heureusement, le principe est simple. Une classe qui désire recevoir
Livre Java .book Page 347 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
347
des événements doit implémenter une interface écouteur. Elle se recense auprès de la source
d’événement, puis elle reçoit les événements souhaités et les traite grâce aux méthodes de l’interface
écouteur.
INFO C++
Les programmeurs ayant une formation C/C++ se demandent peut-être pourquoi une telle prolifération d’objets, de
méthodes et d’interfaces est nécessaire à la gestion des événements. Les programmeurs C++ sont habitués à
programmer les interfaces utilisateur graphiques à l’aide de callbacks, avec des pointeurs génériques ou des handles.
Cela ne fonctionnerait pas en Java. Le modèle d’événement Java est fortement typé : le compilateur s’assure que les
événements ne sont envoyés qu’aux objets qui sont capables de les gérer.
Evénements sémantiques et de bas niveau
AWT fait une distinction utile entre événements de bas niveau et événements sémantiques. Un
événement sémantique exprime ce que fait l’utilisateur, par exemple "cliquer sur un bouton" ; en
conséquence, un événement ActionEvent est sémantique. Les événements de bas niveau sont
ceux qui rendent l’action possible. Dans le cas d’un clic sur un bouton de l’interface utilisateur,
cela représente une pression sur le bouton de la souris, des déplacements du pointeur de la souris,
puis un relâchement du bouton de la souris (mais seulement si le pointeur se trouve encore dans
la zone du bouton affiché sur l’écran). Ce peut être également une pression sur une touche du
clavier, au cas où l’utilisateur sélectionne le bouton avec la touche de tabulation puis l’active
avec la barre d’espacement. De la même manière, un ajustement de la position d’une barre de
défilement est un événement sémantique, mais le déplacement de la souris est un événement
de bas niveau.
Voici les classes d’événements sémantiques les plus utilisées dans le package java.awt.event :
m
ActionEvent (pour un clic de bouton, une sélection d’un élément de menu ou de liste, ou une
pression de la touche Entrée dans un champ de texte).
m
AdjustmentEvent (l’utilisateur a déplacé le curseur d’une barre de défilement).
m
ItemEvent (l’utilisateur a fait une sélection dans un groupe de cases à cocher ou dans une liste).
Il y a cinq classes d’événements de bas niveau :
m
KeyEvent (une touche du clavier a été pressée ou relâchée).
m
MouseEvent (le bouton de la souris a été enfoncé ou relâché ; le pointeur de la souris a été
déplacé ou glissé).
m
MouseWheelEvent (la molette de la souris a été tournée).
m
FocusEvent (un composant a obtenu ou perdu le focus). Voyez la section "Evénements de focalisation" dans ce chapitre pour en savoir plus.
m
WindowEvent (l’état de la fenêtre a changé).
Livre Java .book Page 348 Jeudi, 25. novembre 2004 3:04 15
348
Au cœur de Java 2 - Notions fondamentales
Le Tableau 8.1 montre les interfaces écouteur, les événements et les sources d’événements les plus
importants de AWT.
Tableau 8.1 : Les éléments employés pour la gestion des événements
Interface
Méthodes
Paramètres/accesseurs
Evénements
générés par
ActionListener
actionPerformed
ActionEvent
AbstractButton
• getActionCommand
JComboBox
• getModifiers
JTextField
Timer
AdjustmentListener
adjustmentValueChanged
AdjustmentEvent
JScrollbar
• getAdjustable
• getAdjustmentType
• getValue
ItemListener
itemStateChanged
ItemEvent
AbstractButton
• getItem
JComboBox
• getItemSelectable
• getStateChange
FocusListener
KeyListener
focusGained
FocusEvent
focusLost
• isTemporary
keyPressed
KeyEvent
keyReleased
• getKeyChar
keyTyped
• getKeyCode
Component
Component
• getKeyModifiersText
• getKeyText
• isActionKey
MouseListener
mousePressed
MouseEvent
mouseReleased
• getClickCount
mouseEntered
• getX
mouseExited
• getY
mouseClicked
• getPoint
• translatePoint
Component
Livre Java .book Page 349 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
349
Tableau 8.1 : Les éléments employés pour la gestion des événements (suite)
Interface
Méthodes
Paramètres/accesseurs
Evénements
générés par
MouseMotionListener
mouseDragged
MouseEvent
Component
MouseWheelEvent
Component
mouseMoved
MouseWheelListener
mouseWheelMoved
• getWheelRotation
• getScrollAmount
WindowListener
windowClosing
WindowEvent
windowOpened
• getWindow
Window
windowIconified
windowDeiconified
windowClosed
windowActivated
windowDeactivated
WindowFocusListener
WindowStateListener
windowGainedFocus
WindowEvent
windowLostFocus
• getOppositeWindow
windowStateChanged
WindowEvent
Window
Window
• getOldState
• getNewState
Résumé de la gestion des événements
Nous revenons au mécanisme de délégation d’événement afin de nous assurer que vous avez bien
compris les relations existant entre les classes, les interfaces écouteurs et les classes adaptateurs.
Les sources d’événement sont des composants de l’interface utilisateur, des fenêtres et des
menus. Le système d’exploitation notifie à une source d’événement les activités qui peuvent
l’intéresser, comme les mouvements de la souris ou les frappes au clavier. La source d’événement décrit la nature de l’événement dans un objet événement. Elle stocke également une liste
d’écouteurs — des objets qui souhaitent être prévenus quand l’événement se produit (voir
Figure 8.6). La source d’événement appelle alors la méthode appropriée de l’interface écouteur
afin de fournir des informations sur l’événement aux divers écouteurs recensés. Pour cela, la
source passe l’objet événement adéquat à la méthode de la classe écouteur. L’écouteur analyse
l’objet événement pour obtenir des informations détaillées. Par exemple, nous pouvons utiliser la
méthode getSource pour connaître la source ou les méthodes getX et getY de la classe MouseEvent pour connaître la position actuelle de la souris.
Livre Java .book Page 350 Jeudi, 25. novembre 2004 3:04 15
350
Au cœur de Java 2 - Notions fondamentales
Sachez qu’il existe des interfaces MouseListener et MouseMotionListener séparées, pour des
raisons d’efficacité. Il se produit de nombreux événements lorsque l’utilisateur déplace la souris. Un
écouteur qui s’intéresse uniquement aux clics de la souris ne doit pas être inutilement prévenu de
tous les déplacements de la souris.
Figure 8.6
Relation entre sources
d’événement
et écouteurs.
Event
source
1… *
<<set of one or more>>
Event
listener
<<implements>>
Listener
interface
Tous les événements de bas niveau héritent de ComponentEvent. Cette classe dispose d’une
méthode, nommée getComponent, qui indique le composant ayant généré l’événement ; vous
pouvez employer getComponent à la place de getSource. La méthode getComponent renvoie la
même valeur que getSource, mais l’a déjà transtypée en Component. Par exemple, si un événement
clavier a été déclenché à la suite d’une frappe dans un champ de texte, getComponent renvoie une
référence à ce champ de texte.
java.awt.event.ComponentEvent 1.0
•
Component getComponent()
Renvoie une référence au composant qui est la source de l’événement. Cette méthode correspond
à (Component)getSource().
Types d’événements de bas niveau
Dans les sections suivantes, nous allons examiner plus en détail les événements qui ne sont pas
liés à des composants spécifiques, en particulier les événements relatifs au clavier et à la souris. Le
chapitre suivant abordera les événements sémantiques générés par les composants d’interface
utilisateur.
Evénements du clavier
Quand l’utilisateur appuie sur une touche du clavier, un événement KeyEvent avec KEY_PRESSED de
l’ID est généré. S’il relâche une touche, cela déclenche un événement KeyEvent avec KEY_
RELEASED. Ces événements sont interceptés par les méthodes keyPressed et keyReleased de toute
classe qui implémente l’interface KeyListener. Utilisez ces méthodes pour intercepter la saisie.
Une troisième méthode, keyTyped, combine les deux méthodes et renvoie les caractères qui ont été
tapés.
La meilleure façon de voir ce qui se passe est d’utiliser un exemple. Mais, avant cela, nous devons
encore étudier un peu la terminologie. Java établit une distinction entre les caractères et les codes de
Livre Java .book Page 351 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
351
touche virtuels. Ces derniers sont repérés par un préfixe VK_, comme VK_A ou VK_SHIFT. Les codes
de touche virtuels correspondent aux touches du clavier. Ainsi, VK_A représente la touche marquée A.
Il n’y a pas de code pour les minuscules, car un clavier ne dispose pas de touches spéciales pour
celles-ci.
INFO
Les codes de touche virtuels sont comparables aux codes de touche (ou codes scan) du clavier des PC.
Supposons que l’utilisateur tape un "A" majuscule de la manière habituelle, en enfonçant la touche
Maj (ou Shift) en même temps que la touche A. Cette manipulation déclenche cinq événements pour
Java. Voici les actions et les événements associés :
1. appui sur la touche Maj (keyPressed appelée pour VK_SHIFT) ;
2. appui sur la touche A (keyPressed appelée pour VK_A) ;
3. frappe de "A" (keyTyped appelée pour un "A") ;
4. relâchement de la touche A (keyReleased appelée pour VK_A) ;
5. relâchement de la touche Maj (keyReleased appelée pour VK_SHIFT).
En revanche, si l’utilisateur tape un "a" minuscule, seuls trois événements sont déclenchés :
1. appui sur la touche A (keyPressed appelée pour VK_A) ;
2. frappe de "a" (keyTyped appelée pour un "a") ;
3. relâchement de la touche A (keyReleased appelée pour VK_A).
Ainsi, la méthode keyTyped indique quel caractère a été tapé ("A" ou "a"), alors que keyPressed et
keyReleased indiquent quelle touche a été enfoncée par l’utilisateur.
Pour travailler avec keyPressed et keyReleased, il faut d’abord vérifier le code de touche :
public void keyPressed(KeyEvent event)
{
int keyCode = event.getKeyCode();
. . .
}
Le code de touche équivaut à une de ces constantes, définies dans la classe KeyEvent :
VK_A . . . VK_Z
VK_0 . . . VK_9
VK_COMMA, VK_PERIOD, VK_SLASH, VK_SEMICOLON, VK_EQUALS
VK_OPEN_BRACKET, VK_BACK_SLASH, VK_CLOSE_BRACKET
VK_BACK_QUOTE, VK_QUOTE
VK_GREATER, VK_LESS, VK_UNDERSCORE, VK_MINUS
VK_AMPERSAND, VK_ASTERISK, VK_AT, VK_BRACELEFT, VK_BRACERIGHT
VK_LEFT_PARENTHESIS, VK_RIGHT_PARENTHESIS
VK_CIRCUMFLEX, VK_COLON, VK_NUMBER_SIGN, VK_QUOTEDBL
VK_EXCLAMATION_MARK, VK_INVERTED_EXCLAMATION_MARK
VK_DEAD_ABOVEDOT, VK_DEAD_ABOVERING, VK_DEAD_ACUTE
VK_DEAD_BREVE
VK_DEAD_CARON, VK_DEAD_CEDILLA, VK_DEAD_CIRCUMFLEX
VK_DEAD_DIAERESIS
VK_DEAD_DOUBLEACUTE, VK_DEAD_GRAVE, VK_DEAD_IOTA, VK_DEAD_MACRON
Livre Java .book Page 352 Jeudi, 25. novembre 2004 3:04 15
352
Au cœur de Java 2 - Notions fondamentales
VK_DEAD_OGONEK, VK_DEAD_SEMIVOICED_SOUND, VK_DEAD_TILDE VK_DEAD_VOICED_SOUND
VK_DOLLAR, VK_EURO_SIGN
VK_SPACE, VK_ENTER, VK_BACK_SPACE, VK_TAB, VK_ESCAPE
VK_SHIFT, VK_CONTROL, VK_ALT, VK_ALT_GRAPH, VK_META
VK_NUM_LOCK, VK_SCROLL_LOCK, VK_CAPS_LOCK
VK_PAUSE, VK_PRINTSCREEN
VK_PAGE_UP, VK_PAGE_DOWN, VK_END, VK_HOME, VK_LEFT, VK_UP VK_RIGHT VK_DOWN
VK_F1 . . .VK_F24
VK_NUMPAD0 . . . VK_NUMPAD9
VK_KP_DOWN, VK_KP_LEFT, VK_KP_RIGHT, VK_KP_UP
VK_MULTIPLY, VK_ADD, VK_SEPARATER [sic], VK_SUBTRACT, VK_DECIMAL VK_DIVIDE
VK_DELETE, VK_INSERT
VK_HELP, VK_CANCEL, VK_CLEAR, VK_FINAL
VK_CONVERT, VK_NONCONVERT, VK_ACCEPT, VK_MODECHANGE
VK_AGAIN, VK_ALPHANUMERIC, VK_CODE_INPUT, VK_COMPOSE, VK_PROPS
VK_STOP
VK_ALL_CANDIDATES, VK_PREVIOUS_CANDIDATE
VK_COPY, VK_CUT, VK_PASTE, VK_UNDO
VK_FULL_WIDTH, VK_HALF_WIDTH
VK_HIRAGANA, VK_KATAKANA, VK_ROMAN_CHARACTERS
VK_KANA, VK_KANJI
VK_JAPANESE_HIRAGANA, VK_JAPANESE_KATAKANA, VK_JAPANESE_ROMAN
VK_WINDOWS, VK_CONTEXT_MENU
VK_UNDEFINED
Pour connaître l’état actuel des touches Shift (Maj), Ctrl, Alt et Meta, il est bien sûr possible d’intercepter les événements correspondants : VK_SHIFT, VK_CONTROL, VK_ALT et VK_META. Mais cette
technique est ennuyeuse. Il est plus simple d’utiliser les méthodes isShiftDown, isControlDown,
isAltDown et isMetaDown (les claviers Sun et Macintosh disposent d’une touche META spéciale.
Sur un clavier Sun, la touche est signalée par un carreau. Sur Macintosh, elle est marquée d’une
pomme et d’une feuille de trèfle).
Par exemple, le code qui suit détermine si l’utilisateur a appuyé sur les touches Maj et Flèche à
droite :
public void keyPressed(KeyEvent event)
{
int keyCode = event.getKeyCode();
if (keyCode == keyEvent.VK_RIGHT && event.isShiftDown())
{
. . .
}
}
A l’intérieur de la méthode keyTyped, getKeyChar est appelée pour obtenir le caractère effectivement
tapé.
INFO
Toutes les touches ne déclenchent pas un appel à keyTyped. Seules les touches produisant un caractère Unicode
peuvent être interceptées par la méthode keyTyped. Il faut employer keyPressed pour intercepter les touches
fléchées et les autres touches de commande.
Livre Java .book Page 353 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
353
L’Exemple 8.3 montre comment gérer la frappe au clavier. Le programme est une simple implémentation du jeu de dessin Etch-A-Sketch™, présenté à la Figure 8.7.
Figure 8.7
Un petit programme
de dessin.
Vous déplacez un crayon à l’aide des touches fléchées du clavier. Si vous maintenez simultanément
la touche Maj enfoncée, le crayon se déplacera davantage. Au cas où vous seriez habitué à l’éditeur
vi, vous pouvez utiliser respectivement les touches h, j, k, l et H, J, K, L. Les touches fléchées sont
interceptées avec la méthode keyPressed et les caractères, avec la méthode keyTyped.
Notez une petite caractéristique technique. Normalement, un panneau ne peut pas recevoir les
événements associés aux touches du clavier. Pour supprimer cette limitation, nous appelons la
méthode setFocusable. Nous verrons le concept du focus du clavier plus loin dans ce chapitre.
Exemple 8.3 : Sketch.java
import
import
import
import
import
java.awt.*;
java.awt.geom.*;
java.util.*;
java.awt.event.*;
javax.swing.*;
public class Sketch
{
public static void main(String[] args)
{
SketchFrame frame = new SketchFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec un panneau pour dessiner une figure
*/
class SketchFrame extends JFrame
{
public SketchFrame()
{
setTitle("Sketch");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter le panneau au cadre
Livre Java .book Page 354 Jeudi, 25. novembre 2004 3:04 15
354
Au cœur de Java 2 - Notions fondamentales
SketchPanel panel = new SketchPanel();
add(panel);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
/**
Un panneau pour dessiner avec le clavier.
*/
class SketchPanel extends JPanel
{
public SketchPanel()
{
last = new Point2D.Double(100, 100);
lines = new ArrayList<Line2D>();
KeyHandler listener = new KeyHandler();
addKeyListener(listener);
setFocusable(true);
}
/**
Ajoute un nouveau segment de ligne au dessin.
@param dx Le mouvement dans la direction x
@param dy Le mouvement dans la direction y
*/
public void add(int dx, int dy)
{
// calculer le nouveau point final
Point2D end = new Point2D.Double(last.getX() + dx,
last.getY() + dy);
// ajouter le segment de ligne
Line2D line = new Line2D.Double(last, end);
lines.add(line);
repaint();
// mémoriser le nouveau point final
last = end;
}
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
// dessiner toutes les lignes
for (Line2D l : lines)
g2.draw(l);
}
private Point2D last;
private ArrayList<Line2D> lines;
private static final int SMALL_INCREMENT = 1;
private static final int LARGE_INCREMENT = 5;
private class KeyHandler implements KeyListener
Livre Java .book Page 355 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
355
{
public void keyPressed(KeyEvent event)
{
int keyCode = event.getKeyCode();
// définir la distance
int d;
if (event.isShiftDown())
d = LARGE_INCREMENT;
else
d = SMALL_INCREMENT;
// ajouter un segment de ligne
if (keyCode == KeyEvent.VK_LEFT) add(-d, 0);
else if (keyCode == KeyEvent.VK_RIGHT) add(d, 0);
else if (keyCode == KeyEvent.VK_UP) add(0, -d);
else if (keyCode == KeyEvent.VK_DOWN) add(0, d);
}
public void keyReleased(KeyEvent event) {}
public void keyTyped(KeyEvent event)
{
char keyChar = event.getKeyChar();
// définir la distance
int d;
if (Character.isUpperCase(keyChar))
{
d = LARGE_INCREMENT;
keyChar = Character.toLowerCase(keyChar);
}
else
d = SMALL_INCREMENT;
// ajouter un segment de ligne
if (keyChar == ’h’) add(-d, 0);
else if (keyChar == ’l’) add(d, 0);
else if (keyChar == ’k’) add(0, -d);
else if (keyChar == ’j’) add(0, d);
}
}
}
java.awt.event.KeyEvent 1.1
•
char getKeyChar()
Renvoie le caractère tapé par l’utilisateur.
•
int getKeyCode()
Renvoie le code de touche virtuel de cet événement du clavier.
•
boolean isActionKey()
Renvoie true si la touche de cet événement est une touche "d’action". Les touches d’action sont
les suivantes : Origine (Home), Fin (End), Page précédente (Page up), Page suivante (Page down),
Flèche en haut (Up), Flèche en bas (Down), Flèche à gauche (Left), Flèche à droite (Right),
touches de fonction F1 à F24, Impression écran (Print Screen), Arrêt défilement (Scroll Lock),
Livre Java .book Page 356 Jeudi, 25. novembre 2004 3:04 15
356
Au cœur de Java 2 - Notions fondamentales
Verrouillage majuscule (Caps Lock), Verrouillage numérique (Num Lock), Pause, Insert,
Suppression (Delete), Entrée (Enter), Retour arrière (Backspace) et Touche de tabulation (Tab).
•
static String getKeyText(int keyCode)
Renvoie une chaîne décrivant le code de touche. Par exemple, getKeyText(KeyEvent.VK_END)
renvoie la chaîne "End" (ou "Fin" si votre version de Java est localisée).
•
static String getKeyModifiersText (int modifiers)
Renvoie une chaîne décrivant les touches de modification, comme Maj ou Ctrl+Maj.
Paramètres :
modifiers
L’état du modificateur indiqué par getModifiers.
java.awt.event.InputEvent 1.1
•
int getModifiers()
Renvoie un entier dont les bits décrivent l’état des modificateurs Shift, Control, Alt et Meta.
Cette méthode s’applique à la fois aux événements du clavier et de la souris. Pour savoir si un bit
est défini, comparez la valeur renvoyée avec un des masques binaires SHIFT_MASK, CTRL_MASK,
ALT_MASK, META_GRAPH_MASK, META_MASK ou utilisez une des méthodes suivantes :
•
•
•
•
•
boolean isAltDown()
boolean isControlDown()
boolean isMetaDown()
boolean isShiftDown()
boolean isAltGraphDown() 1.2
Ces méthodes renvoient true si la touche de modification correspondante était enfoncée lorsque
l’événement a été généré.
Evénements de la souris
Il n’est pas nécessaire de gérer explicitement les événements de la souris si vous désirez seulement
que l’utilisateur puisse cliquer sur un bouton ou sur un menu. Ces opérations sont gérées de façon
interne par les divers composants de l’interface utilisateur et traduites en événements sémantiques
appropriés. Cependant, si vous voulez permettre à l’utilisateur de dessiner avec la souris, il vous
faudra intercepter les mouvements, les clics et les opérations de glisser-déplacer de la souris.
Nous allons vous proposer un éditeur graphique simplifié qui permettra à l’utilisateur de placer, de
déplacer et d’effacer des carrés sur une grille (voir Figure 8.8).
Figure 8.8
Un programme de test
de la souris.
Livre Java .book Page 357 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
357
Lorsque l’utilisateur clique sur un bouton de la souris, trois méthodes de l’écouteur sont appelées :
mousePressed quand le bouton est enfoncé, mouseReleased quand il est relâché et, enfin, mouseClicked. Vous pouvez ignorer les deux premières méthodes si vous n’êtes intéressé que par des clics
"complets". En utilisant getX et getY sur l’argument MouseEvent, vous pouvez obtenir les coordonnées x et y du pointeur de la souris au moment du clic. Si vous souhaitez faire une distinction entre
clic simple et double-clic (voire triple-clic), employez la méthode getClickCount.
Certains concepteurs d’interfaces infligent à leurs utilisateurs des combinaisons de clic de souris et
de touches, comme Ctrl+Maj+clic. Cette technique est selon nous assez répréhensible, mais, si cela
vous plaît, vous découvrirez que l’interception des modifications de boutons de souris et de clavier
est très compliquée. Dans l’API d’origine, deux des masques de bouton égalent deux masques de
modification de clavier, à savoir :
BUTTON2_MASK == ALT_MASK
BUTTON3_MASK == META_MASK
Ceci permet aux utilisateurs qui disposent d’une souris à un bouton de simuler les autres boutons de
la souris en maintenant enfoncées des touches de modification. Toutefois, depuis le JDK 1.4, une
nouvelle approche est recommandée. Il existe maintenant des masques :
BUTTON1_DOWN_MASK
BUTTON2_DOWN_MASK
BUTTON3_DOWN_MASK
SHIFT_DOWN_MASK
CTRL_DOWN_MASK
ALT_DOWN_MASK
ALT_GRAPH_DOWN_MASK
META_DOWN_MASK
La méthode getModifiersEx signale précisément les modificateurs de souris et de clavier d’un
événement de souris.
Notez que, sous Windows, BUTTON3_DOWN_MASK est employé pour déterminer l’état du bouton droit
(non primaire). Voici un exemple permettant de savoir si le bouton droit est enfoncé :
if ((event.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) !=0)
... // code du clic droit
Dans notre programme de démonstration, nous fournissons à la fois une méthode mousePressed et
une méthode mouseClicked. Lorsque vous cliquez sur un pixel qui ne se trouve pas à l’intérieur
d’un des carrés déjà dessinés, un nouveau carré est ajouté dans la fenêtre. Ce mécanisme est implémenté dans la méthode mousePressed afin que l’utilisateur obtienne une réponse immédiate et n’ait
pas besoin d’attendre le relâchement du bouton. Lors d’un double-clic à l’intérieur d’un carré existant, celui-ci est effacé. Ce traitement du double-clic est implémenté dans la méthode mouseClicked,
car nous devons connaître le nombre de clics :
public void mousePressed(MouseEvent event)
{
current = find(event.getPoint());
if (current == null) // pas à l’intérieur d’un carré
add(event.getPoint());
}
Livre Java .book Page 358 Jeudi, 25. novembre 2004 3:04 15
358
Au cœur de Java 2 - Notions fondamentales
public void mouseClicked(MouseEvent event)
{
current = find(event.getPoint());
if (current != null && event.getClickCount() >= 2)
remove(current);
}
Quand la souris passe au-dessus d’une fenêtre, celle-ci reçoit un flot d’événements de mouvement de souris qui sont ignorés par la plupart des applications. Notre programme de démonstration intercepte malgré tout ces événements afin de donner au pointeur une forme différente (une
croix) lorsqu’il se trouve au-dessus d’un carré. Nous employons pour cela la méthode getPredefinedCursor de la classe Cursor. Le Tableau 8.2 présente les constantes utilisables avec cette
méthode et les pointeurs tels qu’ils apparaissent sous Windows (notez que plusieurs de ces pointeurs se ressemblent, mais vous devrez vérifier leur véritable aspect sur la plate-forme que vous
utiliserez).
Tableau 8.2 : Exemples de pointeurs Java
Icône
Constante
DEFAULT_CURSOR
CROSSHAIR_CURSOR
HAND_CURSOR
MOVE_CURSOR
TEXT_CURSOR
WAIT_CURSOR
N_RESIZE_CURSOR
Livre Java .book Page 359 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
359
Tableau 8.2 : Exemples de pointeurs Java (suite)
Icône
Constante
NE_RESIZE_CURSOR
E_RESIZE_CURSOR
SE_RESIZE_CURSOR
S_RESIZE_CURSOR
SW_RESIZE_CURSOR
W_RESIZE_CURSOR
NW_RESIZE_CURSOR
ASTUCE
Vous trouverez des images de pointeurs dans le répertoire jre/lib/images/cursors/. Le fichier
cursors.properties définit le pointeur "point chaud" (hot spot). Il s’agit du point exact auquel s’exécute
l’action de la souris. Par exemple, si le pointeur a la forme d’une loupe, le point chaud se trouve au centre de la
lentille.
Voici la méthode mouseMoved de MouseMotionListener dans notre exemple de programme :
public void mouseMoved(MouseEvent event)
{
if (find(event.getPoint()) == null)
setCursor(Cursor.getDefaultCursor());
else
setCursor(Cursor.getPredefinedCursor
(Cursor.CROSSHAIR_CURSOR));
}
Livre Java .book Page 360 Jeudi, 25. novembre 2004 3:04 15
360
Au cœur de Java 2 - Notions fondamentales
INFO
Vous pouvez définir vos propres types de pointeurs grâce à la méthode createCustomCursor de la classe
Toolkit :
Toolkit tk = Toolkit.getDefaultToolkit();
Image img = tk.getImage("dynamite.gif");
Cursor dynamiteCursor = tk.createCustomCursor(img,
new Point(10, 10), "dynamite stick");
Le premier paramètre de createCustomCursor détermine l’image du pointeur. Le deuxième spécifie le décalage
de son "point chaud". Le troisième est une chaîne qui le décrit. Elle peut être employée pour le support d’accessibilité, par exemple, afin d’indiquer la forme du pointeur à un utilisateur ayant des problèmes de vue ou ne se trouvant
pas directement en face de l’écran.
Si l’utilisateur appuie sur un bouton de la souris pendant que celle-ci se déplace, des appels à
mouseDragged sont générés à la place des appels à mouseMoved. Notre programme de démonstration permet ainsi de faire glisser un carré se trouvant sous le pointeur. Le carré en cours de déplacement est simplement mis à jour pour qu’il se trouve centré sous la position de la souris. Puis la grille
est redessinée pour montrer la nouvelle position de la souris :
public void mouseDragged(MouseEvent event)
{
if (current >= null)
{
int x = event.getX();
int y = event.getY();
current.setFrame(
x - SIDELENGTH / 2,
y - SIDELENGTH / 2,
SIDELENGTH,
SIDELENGTH);
repaint();
}
}
INFO
La méthode mouseMoved n’est appelée que tant que la souris reste à l’intérieur du composant. En revanche, la
méthode mouseDragged continue à être appelée même pendant que la souris est tirée en dehors du composant.
Deux autres méthodes sont utilisées pour la gestion des événements de souris : mouseEntered et
mouseExited. Elles sont appelées respectivement quand la souris entre dans un composant et quand
elle le quitte.
Nous devons encore expliquer comment écouter les événements de souris. Les clics sont signalés par
la méthode mouseClicked, qui fait partie de l’interface MouseListener. Comme de nombreuses
applications ne s’intéressent qu’aux clics de la souris — et pas à ses mouvements — et comme les
événements de déplacement se produisent très fréquemment, les événements de glisser-déplacer de
la souris sont définis dans une interface séparée appelée MouseMotionListener.
Dans notre programme, les deux types d’événements de la souris nous intéressent. Deux classes
internes sont définies : MouseHandler et MouseMotionHandler. La classe MouseHandler étend la
Livre Java .book Page 361 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
361
classe MouseAdapter, car elle ne définit que deux des cinq méthodes de MouseListener. La classe
MouseMotionHandler implémente MouseMotionListener et définit les deux méthodes de cette
interface. Le listing du programme est décrit dans l’Exemple 8.4.
Exemple 8.4 : MouseTest.java
import
import
import
import
import
java.awt.*;
java.awt.event.*;
java.util.*;
java.awt.geom.*;
javax.swing.*;
public class MouseTest
{
public static void main(String[] args)
{
MouseFrame frame = new MouseFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec un panneau pour tester
les opérations de la souris
*/
class MouseFrame extends JFrame
{
public MouseFrame()
{
setTitle("MouseTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter un panneau au cadre
MousePanel panel = new MousePanel();
add(panel);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
/**
Un panneau avec des opérations de la souris
pour l’ajout et la suppression de carrés.
*/
class MousePanel extends JPanel
{
public MousePanel()
{
squares = new ArrayList<Rectangle2D>();
current = null;
addMouseListener(new MouseHandler());
addMouseMotionListener(new MouseMotionHandler());
}
Livre Java .book Page 362 Jeudi, 25. novembre 2004 3:04 15
362
Au cœur de Java 2 - Notions fondamentales
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
// dessiner tous les carrés
for (Rectangle2D r : squares)
g2.draw(r);
}
/**
Trouve le premier carré contenant un point.
@param p Un point
@return Le premier carré contenant p
*/
public Rectangle2D find(Point2D p)
{
for (Rectangle2D r : squares)
{
if (r.contains(p)) return r;
}
return null;
}
/**
Ajoute un carré à la collection.
@param p Le centre du carré
*/
public void add(Point2D p)
{
double x = p.getX();
double y = p.getY();
current = new Rectangle2D.Double(
x - SIDELENGTH / 2,
y - SIDELENGTH / 2,
SIDELENGTH,
SIDELENGTH);
squares.add(current);
repaint();
}
/**
Supprime un carré de la collection.
@param s Le carré à supprimer
*/
public void remove(Rectangle2D s)
{
if (s == null) return;
if (s == current) current = null;
squares.remove(s);
repaint();
}
Livre Java .book Page 363 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
private static final int SIDELENGTH = 10;
private ArrayList<Rectangle2D> squares;
private Rectangle2D current;
// le carré contenant le pointeur de la souris
private class MouseHandler extends MouseAdapter
{
public void mousePressed(MouseEvent event)
{
/* ajouter un nouveau carré si le pointeur
n’est pas à l’intérieur d’un carré */
current = find(event.getPoint());
if (current == null)
add(event.getPoint());
}
public void mouseClicked(MouseEvent event)
{
// supprimer le carré courant si double-clic dessus
current = find(event.getPoint());
if (current != null && event.getClickCount() >= 2)
remove(current);
}
}
private class MouseMotionHandler
implements MouseMotionListener
{
public void mouseMoved(MouseEvent event)
{
/* définit le pointeur sous forme de croix
s’il est à l’intérieur d’un carré */
if (find(event.getPoint()) == null)
setCursor(Cursor.getDefaultCursor());
else
setCursor(Cursor.getPredefinedCursor
(Cursor.CROSSHAIR_CURSOR));
}
public void mouseDragged(MouseEvent event)
{
if (current != null)
{
int x = event.getX();
int y = event.getY();
/* tirer le carré courant pour
le centrer à la position (x, y) */
current.setFrame(
x - SIDELENGTH / 2,
y - SIDELENGTH / 2,
SIDELENGTH,
SIDELENGTH);
repaint();
}
}
}
}
363
Livre Java .book Page 364 Jeudi, 25. novembre 2004 3:04 15
364
Au cœur de Java 2 - Notions fondamentales
java.awt.event.MouseEvent 1.1
•
•
•
int getX()
int getY()
Point getPoint()
Renvoient les coordonnées x (horizontale) et y (verticale) ou le point auquel s’est produit
l’événement dans le système de coordonnées de la source, à partir du coin supérieur gauche du
composant.
•
void translatePoint(int x, int y)
Déplace les coordonnées de l’événement de x unités horizontalement et de y unités verticalement.
•
int getClickCount()
Renvoie le nombre de clics de souris consécutifs associés à l’événement (l’intervalle qui détermine
deux clics "consécutifs" dépend du système).
java.awt.event.InputEvent 1.1
•
int getModifiersEx() 1.4
Renvoie les modificateurs étendus ou "bas" de cet événement. Utilisez les valeurs de masque
suivantes pour tester la valeur renvoyée :
BUTTON1_DOWN_MASK
BUTTON2_DOWN_MASK
BUTTON3_DOWN_MASK
SHIFT_DOWN_MASK
CTRL_DOWN_MASK
ALT_DOWN_MASK
ALT_GRAPH_DOWN_MASK
META_DOWN_MASK
•
static String getModifiersExText(int modifiers) 1.4
Renvoie une chaîne comme "Maj+Button1" décrivant les modificateurs étendus ou "bas" dans le
jeu de balises donné.
java.awt.Toolkit 1.0
•
public Cursor createCustomCursor (Image image, Point hotSpot, String name) 1.2
Crée un nouvel objet pointeur personnalisé.
Paramètres :
image
L’image à afficher lorsque le pointeur est actif.
hotSpot
Le point chaud du pointeur (par exemple l’extrémité
d’une flèche ou le centre d’une croix).
name
Une description du pointeur, pour prendre en charge
des options d’accessibilité particulières.
java.awt.Component 1.0
•
public void setCursor(Cursor cursor) 1.1
Attribue au pointeur l’image de l’un des pointeurs prédéfinis spécifiés par le paramètre cursor.
Livre Java .book Page 365 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
365
Evénements de focalisation
Lorsque vous utilisez une souris, vous pouvez pointer sur n’importe quel objet à l’écran. Mais, lorsque vous tapez, les frappes sur les touches doivent concerner un objet spécifique. Le gestionnaire de
fenêtre (tel que Windows ou X Window) dirige toutes les frappes de touche vers la fenêtre active.
Souvent, la fenêtre active se distingue par une barre de titre en surbrillance. Une seule fenêtre peut
être active à la fois.
Supposons maintenant que la fenêtre active soit contrôlée par un programme Java. La fenêtre Java
reçoit les frappes du clavier et les dirige à son tour vers un composant particulier. Ce composant est
désigné comme ayant le focus. Tout comme la fenêtre active se distingue généralement d’une façon
ou d’une autre, la plupart des composants Swing fournissent un indice visuel pour indiquer qu’ils
ont le focus. Un champ de texte contient une barre clignotante, un bouton comprend un rectangle
autour du libellé, etc. Lorsqu’un champ de texte a le focus, vous pouvez taper du texte dedans.
Lorsqu’un bouton a le focus, vous pouvez l’activer en appuyant sur la barre d’espace du clavier.
Un seul composant dans une fenêtre peut avoir le focus à un moment donné. Un composant peut
perdre le focus si l’utilisateur sélectionne un autre composant, qui obtient la focalisation. Un
composant obtient le focus lorsque l’utilisateur clique dessus avec la souris. L’utilisateur peut
également employer la touche de tabulation pour faire passer la focalisation d’un composant à un
autre — parmi ceux capables de recevoir la focalisation de saisie. Par défaut, l’ordre de focalisation (ou ordre de tabulation) des composants Swing va de la gauche vers la droite et de haut en
bas, selon leur position dans leur conteneur. Cet ordre peut être modifié, comme nous le verrons
dans le prochain chapitre.
INFO
Malheureusement, dans les versions précédentes du JDK, la gestion de la focalisation présentait quelques problèmes,
avec plus de 100 bogues signalés. Les raisons étaient doubles. La focalisation des composants interagit avec la focalisation des fenêtres, qui revient au système de fenêtrage. Certains comportements de focalisation montraient ainsi
des variations dépendantes de la plate-forme. De plus, le code d’implémentation s’était apparemment mis hors de
contrôle, en particulier avec l’ajout d’une architecture insatisfaisante de tabulation de focalisation dans le JDK 1.3.
Winston Churchill a déclaré un jour : "Les Américains feront toujours le bon choix… après avoir épuisé toutes les
alternatives." Apparemment, ceci vaut également pour l’équipe Java. Ils ont enfin fait le bon choix dans le JDK 1.4,
après avoir totalement réimplémenté le code de gestion du focus et fourni une description complète du comportement attendu (avec une explication des dépendances inévitables de la plate-forme).
Vous trouverez les spécifications de focalisation à l’adresse http://java.sun.com/j2se/1.4/docs/api/java/awt/docfiles/FocusSpec.html.
Heureusement, la plupart des programmeurs d’application n’ont pas trop à s’inquiéter de la gestion
de la focalisation. Avant le JDK 1.4, il fallait, pour intercepter les événements de focalisation de
composant, généralement vérifier les erreurs ou valider les données. Supposons que vous ayez un
champ de texte contenant un numéro de carte de crédit. Lorsque l’utilisateur a terminé de modifier le
champ et passe à un autre, vous interceptez l’événement de focalisation perdu. Si le format de la
carte de crédit n’a pas été mis en forme correctement, vous pouvez afficher un message d’erreur et
redonner la focalisation au champ de la carte de crédit. Toutefois, le JDK 1.4 possède des mécanismes de validation robustes qui sont plus simples à programmer. Nous traiterons de la validation au
Chapitre 9.
Livre Java .book Page 366 Jeudi, 25. novembre 2004 3:04 15
366
Au cœur de Java 2 - Notions fondamentales
Certains composants, comme les labels ou les panneaux, n’obtiennent pas le focus par défaut car
on suppose qu’ils ne sont utilisés que pour la décoration ou le groupage. Vous devez écraser ce
paramètre par défaut si vous implémentez un programme de dessin avec des panneaux qui affichent des éléments en réponse aux frappes sur le clavier. A partir du JDK 1.4, vous pouvez simplement
appeler :
panel.setFocusable(true);
INFO
Dans les versions plus anciennes du JDK, vous deviez surcharger la méthode isFocusTraversable du composant
pour obtenir le même effet. Toutefois, l’ancienne implémentation de focalisation disposait de concepts séparés pour
obtenir le focus et réaliser la séquence de tabulation. Cette distinction entraînait des comportements déroutants, et
elle a maintenant été supprimée. La méthode isFocusTraversable est aujourd’hui dépréciée.
Dans le reste de cette section, nous verrons des détails de l’événement de focalisation que vous
pouvez tout à fait ignorer, à moins que vous n’ayez un besoin particulier, exigeant un contrôle précis
de la gestion de la focalisation.
Dans le JDK 1.4, vous pouvez facilement retrouver :
m
le propriétaire du focus, c’est-à-dire le composant qui a le focus ;
m
la fenêtre focalisée, la fenêtre qui contient le propriétaire du focus ;
m
la fenêtre active, le cadre ou la boîte de dialogue qui contient le propriétaire du focus.
La fenêtre focalisée est généralement la même que la fenêtre active. Vous n’obtiendrez un résultat
différent que lorsque le propriétaire du focus est contenu dans une fenêtre de haut niveau sans décoration de cadre, comme un menu contextuel.
Pour obtenir cette information, récupérez tout d’abord le gestionnaire de focalisation du clavier :
KeyboardFocusManager manager =
KeyboardFocusManager.getCurrentKeyboardFocusManager();
Puis appelez :
Component owner = manager.getFocusOwner();
Window focused = manager.getFocusedWindow();
Window active = manager.getActiveWindow();
// un cadre ou une boîte de dialogue
En ce qui concerne la notification des changements de focalisation, vous devez installer des écouteurs de focalisation dans les composants ou les fenêtres. Un écouteur de focalisation de composant
doit implémenter l’interface FocusListener avec deux méthodes, focusGained et focusLost. Ces
méthodes sont déclenchées lorsque le composant obtient ou perd la focalisation. Chacune de ces
méthodes possède un paramètre FocusEvent. Il existe plusieurs méthodes très utiles pour cette
classe. La méthode getComponent renvoie le composant qui a reçu ou perdu la focalisation, tandis
que la méthode isTemporary renvoie true si le changement de focus n’a été que temporaire. Un
changement temporaire de focus se produit lorsqu’un composant perd la focalisation et qu’il la récupère
automatiquement. C’est le cas, par exemple, lorsque l’utilisateur sélectionne une autre fenêtre active.
Mais, dès qu’il revient dans la fenêtre, ce composant retrouve la focalisation.
Livre Java .book Page 367 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
367
Le JDK 1.4 introduit la livraison des événements de focalisation de fenêtre. Vous ajoutez un
WindowFocusListener à une fenêtre et implémentez les méthodes windowGainedFocus et
windowLostFocus.
Depuis le JDK 1.4, vous pouvez retrouver le composant ou la fenêtre "opposé" au moment du
transfert du focus. Lorsqu’un composant ou une fenêtre perd le focus, son opposé est le composant ou la fenêtre qui le récupère. A l’inverse, lorsqu’un composant ou une fenêtre prend le focus,
son opposé est celui qui l’a perdu. La méthode getOppositeComponent de la classe FocusEvent
signale le composant opposé, et getOppositeWindow de la classe WindowEvent signale la fenêtre
opposée.
Avec la programmation, vous pouvez déplacer le focus sur un autre composant en appelant la
méthode requestFocus de la classe Component. Toutefois, le comportement dépend intrinsèquement de la plate-forme si le composant n’est pas contenu dans la fenêtre ayant actuellement la
focalisation. Pour permettre aux programmeurs de développer un code indépendant de la plateforme, le JDK 1.4 ajoute la méthode requestFocusInWindow à la classe Component. Cette
méthode réussit uniquement si le composant est contenu dans la fenêtre ayant le focus.
INFO
On ne peut pas supposer qu’un composant a le focus simplement si requestFocus ou requestFocusInWindow
renvoie true. Attendez que l’événement FOCUS_GAINED soit délivré pour vous en assurer.
INFO
Certains programmeurs sont déroutés par l’événement FOCUS_LOST et tentent d’empêcher un autre composant de
prendre le focus en le réclamant dans le gestionnaire focusLost. Mais, à ce moment-là, le focus est déjà perdu. Pour
intercepter le focus dans un composant donné, installez un "écouteur de changement susceptible de veto" dans
KeyboardFocusManager et placez le veto sur la propriété focusOwner. Consultez le Chapitre 8 du Volume 2 pour
en savoir plus sur le veto des propriétés.
java.awt.Component 1.0
•
void requestFocus()
Demande la focalisation pour ce composant.
•
boolean requestFocusInWindow() 1.4
Demande la focalisation pour ce composant. Renvoie false si ce composant n’est pas contenu
dans la fenêtre focalisée ou si la requête ne peut pas être traitée pour une autre raison. Renvoie
true s’il est probable que la requête soit respectée.
•
•
void setFocusable(boolean b) 1.4
boolean isFocusable() 1.4
Définissent ou récupèrent l’état de ce composant qui peut être focalisé. Si b vaut true, ce
composant peut obtenir le focus.
•
boolean isFocusOwner() 1.4
Renvoie true si ce composant a actuellement le focus.
Livre Java .book Page 368 Jeudi, 25. novembre 2004 3:04 15
368
Au cœur de Java 2 - Notions fondamentales
java.awt.KeyboardFocusManager 1.4
•
static KeyboardFocusManager getCurrentKeyboardFocusManager()
Récupère le gestionnaire de focus actuel.
•
Component getFocusOwner()
Récupère le composant qui détient le focus ou null si ce gestionnaire de focus ne gère pas le
composant qui a le focus.
•
Window getFocusedWindow()
Récupère la fenêtre qui contient le composant qui détient le focus ou null si ce gestionnaire de
focus ne gère pas le composant qui a le focus.
•
Window getActiveWindow()
Récupère la boîte de dialogue ou le cadre qui contient la fenêtre focalisée ou null si ce gestionnaire de focus ne gère pas la fenêtre focalisée.
java.awt.Window() 1.0
•
boolean isFocused() 1.4
Renvoie true si cette fenêtre est la fenêtre focalisée.
•
boolean isActive() 1.4
Renvoie true si ce cadre ou cette boîte de dialogue est la fenêtre active. Les barres de titre des
cadres et des boîtes de dialogue actifs sont généralement marquées par le gestionnaire de fenêtre.
java.awt.event.FocusEvent 1.1
•
Component getOppositeComponent() 1.4
Renvoie le composant qui a perdu le focus dans le gestionnaire focusGained ou le composant
qui a obtenu le focus dans le gestionnaire focusLost.
java.awt.event.WindowEvent 1.4
•
Window getOppositeWindow() 1.4
Renvoie la fenêtre qui a perdu le focus dans le gestionnaire windowGainedFocus, la fenêtre qui
a obtenu le focus dans le gestionnaire windowLostFocus, la fenêtre qui a été désactivée dans le
gestionnaire windowActivated ou la fenêtre qui a été activée dans le gestionnaire windowDeactivated.
•
void windowGainedFocus(WindowEvent event)
Appelée lorsque la fenêtre source de l’événement a obtenu le focus.
•
void windowLostFocus(WindowEvent event)
Appelée lorsque la fenêtre source de l’événement a perdu le focus.
Actions
Il existe souvent plusieurs moyens d’activer la même commande. L’utilisateur peut choisir une
même fonction par l’intermédiaire d’un menu, d’un raccourci clavier ou d’un bouton dans une barre
d’outils. Cela est facile à réaliser dans le modèle d’événement AWT : vous liez tous les événements
au même écouteur. Par exemple, supposons que blueAction soit un écouteur d’action dont la
Livre Java .book Page 369 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
369
méthode actionPerformed change la couleur d’arrière-plan en bleu. Vous pouvez attacher le même
objet comme écouteur de plusieurs sources d’événement :
m
un bouton de la barre d’outils libellé "Blue" ;
m
une option de menu baptisée "Blue" ;
m
une combinaison de touches Crtl+B.
Puis chaque commande de changement de couleur est gérée d’une seule manière, quelle que soit
l’action qui l’a déclenchée : un clic sur un bouton, un choix de menu ou une frappe au clavier.
Le package Swing fournit un mécanisme très pratique pour encapsuler des commandes et les attacher à plusieurs sources d’événement : il s’agit de l’interface Action. Une action est un objet qui
encapsule :
m
une description de la commande (comme une chaîne de texte et une icône facultative) ;
m
les paramètres nécessaires à l’exécution de la commande (dans notre cas, la couleur désirée).
Une interface Action possède les méthodes suivantes :
void actionPerformed(ActionEvent event)
void setEnabled(boolean b)
boolean isEnabled()
void putValue(String key, Object value)
Object getValue(String key)
void addPropertyChangeListener(PropertyChangeListener listener)
void removePropertyChangeListener(PropertyChangeListener listener)
La première méthode est la méthode habituelle de l’interface ActionListener : en fait, l’interface
Action est dérivée de ActionListener. Par conséquent, il est possible d’utiliser un objet Action
partout où un objet ActionListener est attendu.
Les deux méthodes suivantes permettent d’activer ou de désactiver l’action, et de vérifier si elle est
activée. Lorsqu’une action attachée à un menu ou à une barre d’outils est désactivée, l’option correspondante apparaît en grisé.
Les méthodes putValue et getValue sont employées pour stocker et récupérer un couple arbitraire
nom/valeur dans l’objet Action. Il existe deux chaînes prédéfinies — Action.NAME et
Action.SMALL_ICON — pour faciliter le stockage des noms et des icônes dans un objet Action :
action.putValue(Action.NAME, "Blue");
action.putValue(Action.SMALL_ICON,
new ImageIcon("blue-ball.gif"));
Le Tableau 8.3 décrit les noms des actions prédéfinies.
Tableau 8.3 : Noms des actions prédéfinies
Nom
Valeur
NAME
Nom de l’action ; affiché sur les boutons et les options de menu.
SMALL_ICON
Emplacement de stockage d’une petite icône ; pour affichage sur un bouton,
une option de menu ou dans la barre d’outils.
SHORT_DESCRIPTION
Courte description de l’icône ; pour affichage dans une bulle d’aide.
Livre Java .book Page 370 Jeudi, 25. novembre 2004 3:04 15
370
Au cœur de Java 2 - Notions fondamentales
Tableau 8.3 : Noms des actions prédéfinies (suite)
Nom
Valeur
LONG_DESCRIPTION
Description détaillée de l’icône ; pour utilisation potentielle dans l’aide en
ligne. Aucun composant Swing n’utilise cette valeur.
MNEMONIC_KEY
Abréviation mnémonique ; pour affichage dans une option de menu (voir
Chapitre 9).
ACCELERATOR_KEY
Emplacement pour le stockage d’un raccourci clavier. Aucun composant
Swing n’utilise cette valeur.
ACTION_COMMAND_KEY
Utilisée dans la méthode registerKeyboardAction, maintenant obsolète.
DEFAULT
Propriété fourre-tout. Aucun composant Swing n’utilise cette valeur.
Si l’objet Action est ajouté à un menu ou à une barre d’outils, le nom et l’icône sont automatiquement récupérés et affichés dans l’option du menu ou sur le bouton de la barre d’outils. La valeur de
SHORT_DESCRIPTION s’affiche dans une bulle d’aide.
Les deux dernières méthodes de l’interface Action permettent aux autres objets — en particulier les menus ou barres d’outils qui ont déclenché l’action — de recevoir une notification
lorsque les propriétés de l’objet Action sont modifiées. Par exemple, si un menu est recensé en
tant qu’écouteur de changement de propriété d’un objet Action, et que l’objet Action soit
ensuite désactivé, le menu est prévenu et peut alors afficher en grisé la rubrique correspondant
à l’action. Les écouteurs de changement de propriété sont une construction générique intégrée
au modèle de composant des Java beans. Vous en saurez plus sur les beans et leurs propriétés au
Volume 2.
Notez que Action est une interface et non une classe. Toute classe implémentant cette interface doit
donc implémenter les sept méthodes citées. Heureusement, un bon samaritain a implémenté toutes
ces méthodes — sauf actionPerformed — dans une classe nommée AbstractAction, qui se
charge de stocker les couples nom/valeur et de gérer les écouteurs de changement de propriété. Il ne
vous reste plus qu’à étendre AbstractAction et à écrire une méthode actionPerformed.
Nous allons construire un objet Action capable d’exécuter la commande de changement de couleur.
Nous allons lui fournir le nom de la commande, une icône et la couleur souhaitée. Nous allons stocker la couleur dans la table des couples nom/valeur proposée par la classe AbstractAction. Voici le
code de la classe ColorAction. Le constructeur spécifie le couple nom/valeur et la méthode
actionPerformed se charge de modifier la couleur :
public class ColorAction extends AbstractAction
{
public ColorAction(String name, Icon icon, Color c)
{
putValue(Action.NAME, name);
putValue(Action.SMALL_ICON, icon);
putValue("color", c);
putValue(Action.SHORT_DESCRIPTION, "Set panel color to "
+ name.toLowerCase());
}
Livre Java .book Page 371 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
371
public void actionPerformed(ActionEvent event)
{
Color c = (Color)getValue("color");
setBackground(c);
}
}
Notre programme de test crée trois objets de cette classe, comme celui-ci :
Action blueAction = new ColorAction("Blue",
new ImageIcon("blue-ball.gif"),Color.BLUE);
Associons maintenant cette action à un bouton. C’est une opération aisée, car il existe un constructeur de
JButton qui accepte un objet Action :
JButton blueButton = new JButton(blueAction);
Ce constructeur lit le nom et l’icône dans l’action, affecte la courte description à la bulle d’aide et
définit l’action en tant qu’écouteur. Vous pouvez voir l’icône et la bulle d’aide à la Figure 8.9.
Vous verrez dans le prochain chapitre qu’il est aussi très simple d’ajouter la même action à un menu.
Figure 8.9
Les boutons affichent les
icônes provenant des
objets Action.
Il nous reste à associer les objets Action au clavier pour que les actions soient réalisées lorsque
l’utilisateur tape des commandes au clavier. Nous nous trouvons alors devant une certaine
complexité technique. Les frappes de touches sont notifiées au composant qui détient le focus, mais
notre application est constituée de plusieurs composants : trois boutons dans un panneau. A tout
moment, n’importe lequel des trois boutons peut détenir le focus. Cela signifie que chacun des trois
boutons doit gérer les événements du clavier et être à l’écoute des combinaisons de touches Ctrl+Y,
Ctrl+B et Ctrl+R.
Ce problème est courant et les concepteurs de Swing ont imaginé une solution pratique pour le
résoudre.
INFO
En fait, dans la version 1.2 de JDK, il existait deux solutions différentes pour lier les touches aux actions : la méthode
registerKeyboardAction de la classe JComponent et le concept KeyMap pour les commandes JTextComponent. En ce qui concerne le JDK version 1.3, ces deux mécanismes sont unifiés. Cette section décrit l’approche
commune.
Pour associer les actions à des frappes de touches, nous devons d’abord créer des objets de la classe
KeyStroke. Il s’agit d’une classe très pratique qui encapsule la description d’une touche. Pour générer un objet KeyStroke, nous n’appelons pas un constructeur, mais nous employons la méthode
Livre Java .book Page 372 Jeudi, 25. novembre 2004 3:04 15
372
Au cœur de Java 2 - Notions fondamentales
statique getKeyStroke de la classe KeyStroke. Nous lui spécifions le code de touche virtuel et les
indicateurs (comme les combinaisons de touches Maj et Ctrl) :
KeyStroke ctrlBKey = KeyStroke.getKeyStroke(KeyEvent.VK_B,
InputEvent.CTRL_MASK);
Il existe une méthode pratique qui vous permet de définir la combinaison de touches sous forme de
chaîne :
KeyStroke ctrlBKey = KeyStroke.getKeyStroke("ctrl B");
Chaque JComponent a trois affectations d’entrée qui associent les objets KeyStroke aux actions.
Les trois affectations correspondent à trois conditions différentes (voir Tableau 8.4).
Tableau 8.4 : Conditions d’affectations d’entrée
Indicateur
Invoquer l’action
WHEN_FOCUSED
Quand ce composant a le focus clavier.
WHEN_ANCESTOR_OF_FOCUSED_
COMPONENT
Quand ce composant contient le composant qui a le focus clavier.
WHEN_IN_FOCUSED_WINDOW
Quand ce composant se trouve dans la même fenêtre que le
composant qui a le focus clavier.
Le traitement des frappes au clavier vérifie ces affectations dans l’ordre suivant :
1. Vérifie l’indicateur WHEN_FOCUSED du composant ayant le focus de saisie. Si la combinaison de
touches existe, exécute l’action correspondante. Si l’action est activée, stoppe le traitement.
2. En commençant par le composant ayant le focus de saisie, vérifie les indicateurs WHEN_
ANCESTOR_OF_FOCUSED_COMPONENT de ses composants parents. Dès qu’une correspondance
avec la combinaison de touches est trouvée, exécute l’action correspondante. Si l’action est activée, stoppe le traitement.
3. Recherche tous les composants visibles et activés dans la fenêtre avec le focus d’entrée, ayant
cette combinaison de touches enregistrée dans un indicateur WHEN_IN_FOCUSED_WINDOW. Donne
à ces composants (dans l’ordre de leur enregistrement) une chance d’exécuter l’action correspondante. Dès que la première action activée est exécutée, stoppe le traitement. Cette partie du
processus est toutefois peu fiable, si une combinaison de touches apparaît dans plus d’un indicateur
WHEN_IN_FOCUSED_WINDOW.
Vous obtenez une affectation d’entrée du composant à l’aide de la méthode getInputMap, par
exemple :
InputMap imap = panel.getInputMap(JComponent.WHEN_FOCUSED);
La condition WHEN_FOCUSED signifie que cette affectation est consultée lorsque le composant courant
a le focus clavier. Dans notre cas, ce n’est pas l’affectation que nous voulons. L’un des boutons, et
non le panneau, a le focus de saisie. L’un des deux autres choix d’affectation fonctionne très bien
pour l’insertion de la combinaison de touches relative au changement de couleur. Dans notre exemple
de programme, nous utilisons WHEN_ANCESTOR_OF_FOCUSED_COMPONENT.
Livre Java .book Page 373 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
373
InputMap n’affecte pas directement les objets KeyStroke aux objets Action. L’affectation est faite
vers des objets arbitraires, et une seconde affectation, implémentée par la classe ActionMap, affecte
les objets aux actions. Cela facilite le partage des mêmes actions parmi les combinaisons de touches
provenant de différentes affectations d’entrée.
Chaque composant a donc trois affectations d’entrée et une affectation d’action. Pour les associer, vous devez proposer des noms pour les actions. Voici comment attacher une touche à une
action :
imap.put(KeyStroke.getKeyStroke("ctrl Y"), "panel.yellow");
ActionMap amap = panel.getActionMap();
amap.put("panel.yellow", yellowAction);
Il est courant d’employer la chaîne "none" pour une action qui ne fait rien. Il est ainsi facile de
désactiver une touche :
imap.put(KeyStroke.getKeyStroke("ctrl C"), "none");
ATTENTION
La documentation JDK suggère d’utiliser le nom de l’action comme clé de recherche d’action. Ce n’est pas forcément
une bonne idée. Le nom de l’action est affiché sur les boutons et dans les options de menu ; il peut par conséquent
changer selon l’humeur du concepteur de l’interface utilisateur ou être traduit en cas de localisation du programme.
De telles chaînes, instables, ne sont pas de bons choix pour une recherche. Nous vous recommandons donc de choisir,
pour les actions, des noms indépendants de ceux affichés.
Pour résumer, voici comment procéder pour réaliser la même action en réponse à un clic de bouton,
un choix de menu ou une frappe de touches :
1. Créez une classe qui étend la classe AbstractAction. Vous devez pouvoir utiliser la même
classe pour plusieurs actions connexes.
2. Créez un objet de la classe action.
3. Créez un bouton ou une option de menu à partir de l’objet action. Le constructeur lira le texte de
libellé et l’icône dans l’objet action.
4. Pour les actions pouvant être déclenchées par des frappes de touches, vous devez réaliser des
étapes supplémentaires. Localisez d’abord le composant de plus haut niveau dans la fenêtre, tel
qu’un panneau qui contient tous les autres composants.
5. Extrayez l’affectation d’entrée WHEN_ANCESTOR_OF_FOCUSED_COMPONENT de ce composant de
haut niveau. Créez un objet KeyStroke pour la combinaison de touches désirée. Créez un objet
touche d’action, tel qu’une chaîne qui décrit votre action. Ajoutez la paire (frappe de touches,
touche d’action) dans l’affectation d’entrée.
6. Enfin, extrayez l’affectation d’entrée du composant de plus haut niveau. Ajoutez la paire (touche
d’action, objet action) à l’affectation.
L’Exemple 8.5 montre le code complet du programme qui affecte à la fois les boutons et les frappes
de touches aux objets action. Testez-le — le fait de cliquer sur les boutons ou de taper Ctrl+Y,
Ctrl+B ou Ctrl+R, modifie la couleur du panneau.
Livre Java .book Page 374 Jeudi, 25. novembre 2004 3:04 15
374
Au cœur de Java 2 - Notions fondamentales
Exemple 8.5 : ActionTest.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ActionTest
{
public static void main(String[] args)
{
ActionFrame frame = new ActionFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec un panneau pour faire la démonstration
des actions de changement de couleur.
*/
class ActionFrame extends JFrame
{
public ActionFrame()
{
setTitle("ActionTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter un panneau au cadre
ActionPanel panel = new ActionPanel();
add(panel);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
/**
Un panneau avec des boutons et des raccourcis clavier
pour modifier la couleur d’arrière-plan.
*/
class ActionPanel extends JPanel
{
public ActionPanel()
{
// définir les actions
Action yellowAction = new ColorAction("Yellow",
new ImageIcon("yellow-ball.gif"),
Color.YELLOW);
Action blueAction = new ColorAction("Blue",
new ImageIcon("blue-ball.gif"),
Color.BLUE);
Livre Java .book Page 375 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
Action redAction = new ColorAction("Red",
new ImageIcon("red-ball.gif"),
Color.RED);
// ajouter les boutons pour ces actions
add(new JButton(yellowAction));
add(new JButton(blueAction));
add(new JButton(redAction));
// associer des noms aux touches Y, B et R
InputMap imap = getInputMap(
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
imap.put(KeyStroke.getKeyStroke("ctrl Y"), "panel.yellow");
imap.put(KeyStroke.getKeyStroke("ctrl B"), "panel.blue");
imap.put(KeyStroke.getKeyStroke("ctrl R"), "panel.red");
// associer les noms aux actions
ActionMap amap = getActionMap();
amap.put("panel.yellow", yellowAction);
amap.put("panel.blue", blueAction);
amap.put("panel.red", redAction);
}
public class ColorAction extends AbstractAction
{
/**
Construit une action couleur.
@param name Le nom du bouton
@param icon L’icône du bouton
@param c La couleur d’arrière-plan
*/
public ColorAction(String name, Icon icon, Color c)
{
putValue(Action.NAME, name);
putValue(Action.SMALL_ICON, icon);
putValue(Action.SHORT_DESCRIPTION,
"Set panel color to " + name.toLowerCase());
putValue("color", c);
}
public void actionPerformed(ActionEvent event)
{
Color c = (Color)getValue("color");
setBackground(c);
repaint();
}
}
}
375
Livre Java .book Page 376 Jeudi, 25. novembre 2004 3:04 15
376
Au cœur de Java 2 - Notions fondamentales
java.swing.Action 1.2
•
void setEnabled(boolean b)
Active ou désactive cette action.
•
boolean isEnabled()
Renvoie true si l’action est activée.
•
void putValue(String key, Object value)
Place une paire nom/valeur dans l’objet Action.
Paramètres :
•
key
Le nom de la fonctionnalité à stocker avec l’objet action.
Cela peut être n’importe quelle chaîne, mais il en existe
quatre prédéfinies (voir Tableau 8.3).
value
L’objet associé au nom.
Object getValue(String key)
Renvoie la valeur extraite d’un couple nom/valeur.
javax.swing.JMenu 1.2
•
JMenuItem add(Action a)
Ajoute au menu un élément (une option) qui invoque l’action a lorsqu’il est sélectionné ; renvoie
l’élément de menu ajouté.
javax.swing.KeyStroke 1.2
•
static KeyStroke getKeyStroke(char keyChar)
Crée un objet KeyStroke qui encapsule une frappe de touche correspondant à un événement
KEY_TYPED.
•
•
static KeyStroke getKeyStroke(int keyCode, int modifiers)
static KeyStroke getKeyStroke(int keyCode, int modifiers, boolean onRelease)
Créent un objet KeyStroke qui encapsule une frappe de touche correspondant à un événement
KEY_PRESSED ou KEY_RELEASED (touche appuyée ou relâchée).
Paramètres :
keyCode
La valeur de la touche virtuelle.
modifiers
Toute combinaison de InputEvent.SHIFT_MASK,
InputEvent.CTRL_MASK, InputEvent.ALT_MASK,
InputEvent.META_MASK
onRelease vaut true si la frappe de touches doit être reconnue lorsque la touche est relâchée.
•
static KeyStroke getKeyStroke(String description)
Crée un objet KeyStroke à partir d’une description en clair. La description est une séquence de
mots clés séparés par des espaces, au format suivant :
1. Les mots clés correspondant à shift control ctrl meta alt button1 button2 button3
sont traduits en masque binaire approprié.
2. La chaîne typed doit être suivi d’une chaîne d’un seul caractère, par exemple, "typed a".
Livre Java .book Page 377 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
377
3. Un mot clé pressed ou released indique une touche appuyée ou relâchée. (Appuyée est la
valeur par défaut).
4. Sinon la chaîne, lorsqu’elle est préfixée par VK_, doit correspondre à une constante KeyEvent ;
ainsi, "INSERT" correspond à KeyEvent.VK_INSERT.
Par exemple, "released ctrl Y" correspond à :
getKeyStroke(KeyEvent.VK_Y, Event.CTRL_MASK, true)
javax.swing.JComponent 1.2
•
ActionMap getActionMap() 1.3
Renvoie l’affectation d’action qui affecte les frappes de touches aux touches d’action.
•
InputMap getInputMap(int flag) 1.3
Extrait l’affectation d’entrée qui affecte les touches d’action aux objets action.
Paramètres :
flag
Une condition du focus de saisie pour déclencher l’action,
dont les valeurs figurent au Tableau 8.4.
Multidiffusion
Dans la section précédente, plusieurs sources d’événement notifiaient le même écouteur d’événement. Nous allons maintenant étudier le mécanisme inverse. Toutes les sources d’événement AWT
prennent en charge un modèle de multidiffusion (multicasting) pour les écouteurs. Cela signifie
que le même événement peut être envoyé à plusieurs objets écouteurs. La multidiffusion se révèle
utile si un événement est potentiellement intéressant pour plusieurs écouteurs. Il suffit d’ajouter
plusieurs écouteurs à une source d’événement pour donner à chacun d’eux une chance de réagir
aux événements.
ATTENTION
Selon la documentation JDK, "L’API ne garantit pas l’ordre dans lequel les événements sont diffusés à plusieurs écouteurs recensés auprès d’une même source". En particulier, n’implémentez pas une logique qui dépend de l’ordre de
livraison.
Nous allons présenter une petite application utilisant la multidiffusion. Nous allons créer un cadre
capable de générer d’autres fenêtres à l’aide d’un bouton "New" et de toutes les fermer grâce à un
bouton "Close all" (voir Figure 8.10).
L’écouteur du bouton New de MulticastPanel est l’objet newListener construit dans le constructeur
de MulticastPanel — il construit un nouveau cadre chaque fois que le bouton est cliqué.
Mais le bouton Close all de MulticastPanel a plusieurs écouteurs. A chaque exécution du
constructeur BlankFrame, il ajoute un autre écouteur d’action au bouton Close all. Chacun de ces
écouteurs est responsable de la fermeture d’un unique cadre dans sa méthode actionPerformed.
Lorsque l’utilisateur clique sur le bouton Close all, chacun des écouteurs est activé, et chacun d’eux
ferme son cadre.
Livre Java .book Page 378 Jeudi, 25. novembre 2004 3:04 15
378
Au cœur de Java 2 - Notions fondamentales
En outre, la méthode actionPerformed supprime l’écouteur du bouton Close all car il n’est plus
nécessaire une fois le cadre fermé.
Figure 8.10
Toutes les fenêtres sont à
l’écoute de la commande
Tout fermer.
L’Exemple 8.6 contient le code source de l’application.
Exemple 8.6 : MulticastTest.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class MulticastTest
{
public static void main(String[] args)
{
MulticastFrame frame = new MulticastFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec des boutons pour créer et fermer
des cadres secondaires
*/
class MulticastFrame extends JFrame
{
public MulticastFrame()
Livre Java .book Page 379 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
{
setTitle("MulticastTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter un panneau au cadre
MulticastPanel panel = new MulticastPanel();
add(panel);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
/**
Un panneau avec des boutons pour créer et fermer des cadres.
*/
class MulticastPanel extends JPanel
{
public MulticastPanel()
{
// ajouter un bouton "New"
JButton newButton = new JButton("New");
add(newButton);
final JButton closeAllButton = new JButton("Close all");
add(CloseAllButton);
ActionListener newListener = new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
BlankFrame frame = new BlankFrame(closeAllButton);
frame.setVisible(true);
}
};
newButton.addActionListener(newListener);
}
}
/**
Un cadre vide qui peut être fermé par un clic sur un bouton.
*/
class BlankFrame extends JFrame
{
/**
Construit un cadre vide
@param closeButton Le bouton pour fermer ce cadre
*/
public BlankFrame(final JButton closeButton)
{
counter++;
setTitle("Frame " + counter);
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
setLocation(SPACING * counter, SPACING * counter);
379
Livre Java .book Page 380 Jeudi, 25. novembre 2004 3:04 15
380
Au cœur de Java 2 - Notions fondamentales
closeListener = new
ActionListener()
{
closeButton.removeActionListener(closeListener);
dispose();
}
};
closeButton.addActionListener(closeListener);
}
Private ActionListener closeListener;
public static final int DEFAULT_WIDTH = 200;
public static final int DEFAULT_HEIGHT = 150;
public static final int SPACING = 40;
private static int counter = 0;
}
Implémenter des sources d’événements
Dans la dernière section de ce chapitre, nous verrons comment implémenter une classe qui génère
ses propres événements et notifie les écouteurs intéressés. Ceci est parfois nécessaire lorsque vous
utilisez des composants Swing avancés. Il est aussi intéressant de voir ce qui se passe en coulisse
lorsque vous ajoutez un écouteur à un composant.
Notre source d’événement sera PaintCountPanel, qui compte la fréquence d’appel de la méthode
paintComponent. A chaque fois que le compte est incrémenté, PaintCountPanel avertit tous les
écouteurs. Dans notre exemple de programme, nous attacherons un seul écouteur qui actualise le
titre du cadre (voir Figure 8.11).
Lorsque vous définissez une source d’événement, trois ingrédients sont nécessaires :
m
Un type d’événement. Nous pourrions définir notre propre classe d’événement, mais nous utilisons
simplement la classe PropertyChangeEvent existante.
m
Une interface d’écouteur d’événement. Une fois de plus, nous pourrions définir notre propre
interface, mais nous utiliserons l’interface PropertyChangeListener existante. Cette interface
possède une seule méthode :
public void propertyChange(PropertyChangeEvent event)
m
Des méthodes pour ajouter et supprimer des écouteurs. Nous fournirons deux méthodes dans
la classe PaintCountPanel :
public void addPropertyChangeListener(PropertyChangeListener listener)
public void removePropertyChangeListener(PropertyChangeListener listener)
Comment s’assurer que les événements sont envoyés aux parties intéressées ? C’est là la responsabilité de la source de l’événement. Elle doit construire un objet d’événement et le passer aux écouteurs
recensés dès qu’un événement survient.
La gestion des événements est une tâche commune, et les concepteurs de Swing fournissent une
classe commode, intitulée EventListenerList, pour faciliter l’implémentation des méthodes et
ainsi ajouter et supprimer des écouteurs et déclencher des événements. La classe s’occupe des
détails gênants qui peuvent survenir lorsque plusieurs threads tentent d’ajouter, de supprimer ou
de distribuer simultanément des événements.
Livre Java .book Page 381 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
381
Certaines sources d’événements acceptant les écouteurs de plusieurs types, chaque écouteur de la
liste des écouteurs d’événements est associé à une classe particulière. Les méthodes add et remove
sont destinées à l’implémentation des méthodes addXxxListener. Par exemple :
public void addPropertyChangeListener(PropertyChangeListener listener)
{
listenerList.add(PropertyChangeListener.class, listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener)
{
listenerList.remove(PropertyChangeListener.class, listener);
}
INFO
Vous vous demandez peut-être pourquoi EventListenerList ne vérifie pas simplement l’interface implémentée
par l’objet écouteur. Or un objet peut implémenter plusieurs interfaces. Il est possible, par exemple, que listener
implémente à la fois PropertyChangeListener et l’interface ActionListener, mais un programmeur peut choisir de ne l’ajouter que sous forme de PropertyChangeListener en appelant addPropertyChangeListener.
EventListenerList doit respecter ce choix.
Dès que la méthode paintComponent est appelée, la classe PaintCountPanel construit un objet
PropertyChangeEvent indiquant la source de l’événement, le nom de la propriété et l’ancienne et la
nouvelle valeur de la propriété. Elle appelle ensuite la méthode d’aide firePropertyChangeEvent :
public void paintComponent(Graphics g)
{
int oldPaintCount = paintCount;
paintCount++;
firePropertyChangeEvent(new PropertyChangeEvent(this,
"paintCount", oldPaintCount, paintCount));
super.paintComponent(g);
}
La méthode firePropertyChangeEvent localise tous les écouteurs recensés et appelle leurs méthodes
propertyChange :
public void firePropertyChangeEvent(PropertyChangeEvent event)
{
EventListener[] listeners =
listenerList.getListeners(PropertyChangeListener.class);
for (EventListener l : listeners)
((PropertyChangeListener) l).propertyChange(event);
}
L’Exemple 8.7 montre le code source du programme d’exemple qui écoute un PaintCountPanel.
Le constructeur du cadre ajoute un écouteur de changement de propriété qui actualise le titre du cadre :
panel.addPropertyChangeListener(new
PropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent event)
{
setTitle("EventSourceTest - " + event.getNewValue());
}
});
Livre Java .book Page 382 Jeudi, 25. novembre 2004 3:04 15
382
Au cœur de Java 2 - Notions fondamentales
Ceci termine notre discussion sur la gestion des événements. Dans le prochain chapitre, vous en
saurez plus sur les composants de l’interface utilisateur. Bien entendu, pour programmer des interfaces utilisateur, vous exploiterez vos connaissances de la gestion d’événements, en capturant les
événements générés par les composants de l’interface utilisateur.
Figure 8.11
Comptage de la fréquence
de dessin du panneau.
Exemple 8.7 : EventSourceTest.java
import
import
import
import
import
java.awt.*;
java.awt.event.*;
java.util.*;
javax.swing.*;
java.beans.*;
public class EventSourceTest
{
public static void main(String[] args)
{
EventSourceFrame frame = new EventSourceFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre qui contient un panneau avec des dessins
*/
class EventSourceFrame extends JFrame
{
public EventSourceFrame()
{
setTitle("EventSourceTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter le panneau au cadre
final PaintCountPanel panel = new PaintCountPanel();
add(panel);
panel.addPropertyChangeListener(new
PropertyChangeListener()
{
Livre Java .book Page 383 Jeudi, 25. novembre 2004 3:04 15
Chapitre 8
Gestion des événements
public void propertyChange(PropertyChangeEvent event)
{
setTitle("EventSourceTest - " + event.getNewValue());
}
});
}
public static final int DEFAULT_WIDTH = 400;
public static final int DEFAULT_HEIGHT = 200;
}
/**
Un panneau qui compte la fréquence du dessin.
*/
class PaintCountPanel extends JPanel
{
public void paintComponent(Graphics g)
{
int oldPaintCount = paintCount;
paintCount++;
firePropertyChangeEvent(new PropertyChangeEvent(this,
"paintCount", oldPaintCount, paintCount));
super.paintComponent(g);
}
/**
Ajoute un écouteur de changement
@param listener L’écouteur à ajouter
*/
public void addPropertyChangeListener(PropertyChangeListener listener)
{
listenerList.add(PropertyChangeListener.class, listener);
}
/**
Supprime un écouteur de changement
@param listener L’écouteur à supprimer
*/
public void removePropertyChangeListener(PropertyChangeListener listener)
{
listenerList.remove(PropertyChangeListener.class, listener);
}
public void firePropertyChangeEvent(PropertyChangeEvent event)
{
EventListener[] listeners =
listenerList.getListeners(PropertyChangeListener.class);
for (EventListener l : listeners)
((PropertyChangeListener) l).propertyChange(event);
}
public int getPaintCount()
{
return paintCount;
}
private int paintCount;
}
383
Livre Java .book Page 384 Jeudi, 25. novembre 2004 3:04 15
384
Au cœur de Java 2 - Notions fondamentales
javax.swing.event.EventListenerList 1.2
•
void add(Class t, EventListener l)
Ajoute un écouteur d’événement et sa classe à la liste. La classe est stockée de sorte que les
méthodes de déclenchement d’événements puissent appeler chacun des événements. Une utilisation
ordinaire se trouve dans une méthode addXxxListener :
public void addXxxListener(XxxListener l)
{
listenerList.add(XxxListener.class, l);
}
Paramètres
•
t
Le type d’écouteur.
l
L’écouteur.
void remove(Class t, EventListener l)
Supprime un écouteur d’événement et sa classe de la liste. Une utilisation générale se trouve
dans une méthode removeXxxListener :
public void removeXxxListener(XxxListener l)
{
listenerList.remove(XxxListener.class, l);
}
Paramètres
•
t
Le type d’écouteur.
l
L’écouteur.
EventListener[] getListeners(Class t) 1.3
Renvoie un tableau de tous les écouteurs du type donné. Le tableau est garanti être non null.
•
Object[] getListenerList()
Renvoie un tableau dont les éléments ayant un indice pair sont des classes d’écouteurs et dont les
éléments ayant un indice impair sont des objets écouteur. Le tableau est garanti comme étant non
null.
java.beans.PropertyChangeEvent 1.1
•
PropertyChangeEvent(Object source, String name, Object oldValue, Object newValue)
Construit un événement de changement de propriété.
Paramètres
source
La source de l’événement, c’est-à-dire l’objet qui signale
un changement de propriété.
name
Le nom de la propriété.
oldValue
La valeur de la propriété avant le changement.
newValue
La valeur de la propriété après le changement.
java.beans.PropertyChangeListener 1.1
•
void propertyChange(PropertyChangeEvent event)
Appelée lorsqu’une valeur de propriété a changé.
Livre Java .book Page 385 Jeudi, 25. novembre 2004 3:04 15
9
Swing et les composants
d’interface utilisateur
Au sommaire de ce chapitre
✔ L’architecture Modèle-Vue-Contrôleur des boutons Swing
✔ Introduction à la gestion de mise en forme
✔ Entrée de texte
✔ Composants du choix
✔ Menus
✔ Mise en forme sophistiquée
✔ Boîtes de dialogue
Le chapitre précédent a illustré essentiellement l’emploi du modèle d’événements de Java. Vous avez
accompli les premières étapes de l’élaboration d’une interface utilisateur. Ce chapitre vous présentera les principaux outils nécessaires à la création d’interfaces dotées d’un plus grand nombre de
fonctionnalités.
Nous commencerons par les caractéristiques de l’architecture sous-jacente de la bibliothèque
Swing ; ainsi, vous pourrez en utiliser efficacement les composants les plus avancés. Nous poursuivrons avec les éléments graphiques de cette bibliothèque les plus couramment employés, tels que les
champs de saisie de texte, les boutons radio et les menus. Vous étudierez aussi l’exploitation des
fonctionnalités de gestion de mise en forme utilisées avec Java pour pouvoir disposer ces composants dans une fenêtre, indépendamment du style de look and feel adopté pour l’interface utilisateur.
Pour finir, vous aborderez l’implémentation des boîtes de dialogue avec Swing.
Ce chapitre couvre tous les composants Swing de base, tels que les outils de texte, les boutons et les
curseurs. Il s’agit de ceux auxquels vous recourrez le plus souvent. Les composants Swing plus
sophistiqués sont étudiés au Volume 2.
Livre Java .book Page 386 Jeudi, 25. novembre 2004 3:04 15
386
Au cœur de Java 2 - Notions fondamentales
L’architecture Modèle-Vue-Contrôleur
Avant de décrire l’architecture sur laquelle s’appuie l’emploi des composants Swing, réfléchissons
un peu à tous les éléments qui entrent dans la constitution d’un composant d’interface utilisateur
comme un bouton, une case à cocher, un champ de texte, ou un contrôle d’arborescence sophistiqué.
Chaque composant possède trois caractéristiques :
m
son contenu, tel que l’état d’un bouton (activé ou non) ou la valeur d’un champ de texte ;
m
son apparence (couleur, taille, etc.) ;
m
son comportement (réaction aux événements).
Même un composant apparemment simple, comme un bouton, cache une interaction relativement
complexe entre ces caractéristiques. L’apparence d’un bouton dépend évidemment du style que l’on
souhaite donner. Le style Metal est visuellement différent du style Windows ou Motif. Cette apparence dépend aussi de l’état du bouton : lorsqu’il est pressé, il doit être redessiné pour afficher un
aspect différent. Cet état découle des événements qu’il reçoit.
Bien sûr, lorsque vous vous servez d’un bouton dans vos programmes, vous le considérez simplement comme un bouton, sans réfléchir davantage à ses rouages internes. C’est au programmeur qui
l’a implémenté que revient le travail de réflexion. Il se doit de mettre en œuvre tous les composants
d’une interface pour qu’ils puissent fonctionner quel que soit le look and feel de l’interface installé.
Pour cela, les concepteurs de Swing se sont tournés vers une architecture bien connue : Modèle-VueContrôleur. A l’instar d’autres architectures, elle repose sur l’un des principes de la conception
orientée objet (voir Chapitre 5), qui préconisait de ne pas trop assigner de responsabilités à un seul
objet. Evitez qu’une seule classe de boutons ne soit chargée de tout faire. Au contraire, associez le
style d’un composant à un seul objet et stockez-en le contenu dans un autre objet. L’architecture
Modèle-Vue-Contrôleur (MVC) nous enseigne comment réaliser cette opération en implémentant
trois classes séparées :
m
le modèle qui stocke le contenu ;
m
la vue qui affiche le contenu ;
m
le contrôleur qui gère l’entrée utilisateur.
L’architecture prévoit précisément de quelle façon ces trois objets interagissent. Le modèle conserve
la valeur du composant et ne possède pas d’interface utilisateur. Pour un bouton, le contenu, ou sa
valeur, est assez ordinaire : juste un petit ensemble d’indicateurs qui signalent s’il est pressé ou non,
actif ou inactif, etc. Pour un champ de texte, le contenu est un peu plus intéressant. C’est un objet de
type chaîne qui contient le texte courant. Il ne s’agit pas de la vue du contenu ; si celui-ci est plus
grand que le champ, l’utilisateur n’en voit qu’une partie (voir Figure 9.1).
Figure 9.1
model
Modèle et vue
d’un champ
de texte.
view
"The quick brown fox jumps over the lazy dog"
Livre Java .book Page 387 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
387
Le modèle doit implémenter des méthodes pour modifier le contenu et découvrir ce qu’il représente.
Par exemple, un modèle texte possède des méthodes pour ajouter ou supprimer des caractères du
contenu actuel et renvoyer la valeur courante sous forme d’une chaîne. Encore une fois, gardez à
l’esprit que le modèle est complètement non visuel. Il incombe à la vue de dessiner les données qui
sont stockées dans le modèle.
INFO
Le choix du terme "modèle" est peut-être malheureux, car un modèle est souvent interprété comme étant une représentation d’un concept abstrait. Les concepteurs de voitures ou d’avions construisent des modèles pour simuler les
vraies machines. Cette analogie peut être confondante lorsque l’on pense à l’architecture Modèle-Vue-Contrôleur.
Ici, le modèle conserve l’intégralité du contenu et la vue en donne une représentation visuelle partielle ou complète.
Une meilleure analogie serait de prendre un modèle qui pose pour un artiste. Il revient à ce dernier d’observer le
premier et d’en créer une vue. Selon l’artiste, la vue pourrait être un portrait figuratif, une peinture impressionniste
ou encore un dessin cubiste représentant l’esprit en d’étranges contorsions.
Un des avantages de l’architecture Modèle-Vue-Contrôleur est de pouvoir posséder plusieurs vues,
chacune montrant une partie ou un aspect différents du contenu. Par exemple, un éditeur HTML peut
offrir deux vues simultanées d’un même contenu, l’une WYSIWYG et l’autre brute avec les balises
(voir Figure 9.2). Lorsque le modèle est mis à jour par l’intermédiaire du contrôleur de l’une des
vues, il en avise les deux vues associées. Lorsque celles-ci reçoivent la notification, elles s’actualisent automatiquement. Bien sûr, pour un simple composant d’interface, tel qu’un bouton, vous
n’obtiendrez pas plusieurs vues du même modèle.
Figure 9.2
Deux vues
séparées du
même modèle.
model
P
OL
LI
LI
LI
WYSIWG
view
1.
2.
3.
tag
view
<P>
<OL>
<LI>
<LI>
<LI>
</P>
</LI>
</LI>
</LI>
</OL>
Le contrôleur gère les événements d’entrée utilisateur tels que les clics de souris ou les frappes au clavier.
Lorsqu’ils se produisent, le contrôleur décide de les traduire en changements dans le modèle ou la vue.
Livre Java .book Page 388 Jeudi, 25. novembre 2004 3:04 15
388
Au cœur de Java 2 - Notions fondamentales
Par exemple, si l’utilisateur tape un caractère dans un champ de texte, le contrôleur appelle la
commande d’insertion de caractère du modèle. Le modèle commande ensuite à la vue de se mettre à
jour. Celle-ci ignore toujours pourquoi le texte a changé. Mais si l’utilisateur appuie sur une touche,
le contrôleur peut indiquer à la vue de défiler. Ce défilement n’a aucun effet sur le texte sous-jacent,
le modèle ne sait donc jamais que cet événement s’est produit.
Modèles de conception
Lors de la résolution d’un problème, vous n’ébauchez généralement pas une solution à partir des
principes de base. Vous êtes plus vraisemblablement guidé par l’expérience, ou vous pouvez
demander à d’autres experts un conseil. Les modèles de conception constituent un mode de
présentation structuré de cette expertise.
Au cours des dernières années, les concepteurs de logiciels ont commencé à rassembler de tels
modèles. Les pionniers dans ce domaine ont été inspirés par les modèles de conception architecturale de Christopher Alexander. Dans son livre The Timeless Way of Building (Oxford University
Press 1979), il fournit une collection de modèles pour la conception des espaces de vie publics et
privés. En voici un exemple typique :
Espace fenêtre
Tout le monde aime les places près des fenêtres, les baies vitrées, ou encore avec de larges rebords
de faible hauteur, avec des fauteuils confortables à proximité. Une pièce qui ne dispose pas d’un
tel espace ne vous permettra pas de vous sentir bien installé ou parfaitement à l’aise.
La pièce dépourvue d’une fenêtre qui permettrait d’aménager un tel "espace de vie" soumettra
son occupant à un dilemme :
1. S’asseoir et être confortablement installé.
2. Etre proche de la lumière.
Si les espaces confortables — ceux où vous avez envie de vous asseoir — sont éloignés des fenêtres,
il n’existe alors aucune possibilité de résoudre ce conflit.
Par conséquent, dans chaque pièce où vous séjournez souvent, transformez au moins une fenêtre
en un "espace fenêtre".
Figure 9.3
Un espace fenêtre.
low
sill
place
Chaque modèle, dans le catalogue d’Alexander ainsi que dans les catalogues de modèles logiciels,
suit un format particulier. Il décrit tout d’abord un contexte, une situation qui donne lieu à un
problème de conception. Celui-ci est ensuite expliqué, généralement sous la forme d’un ensemble
de forces en conflit. Finalement, la solution indique une configuration qui équilibre ces forces.
Dans le modèle "espace fenêtre", le contexte est une pièce dans laquelle vous passez un certain
temps dans la journée. Les forces en conflit sont le fait que vous souhaitez vous asseoir confortablement, et le fait d’être attiré par la lumière. La solution est de fabriquer un "espace fenêtre".
Livre Java .book Page 389 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
389
Dans l’architecture Modèle-Vue-Contrôleur, le contexte est un système d’interface utilisateur qui
présente des informations et reçoit les entrées de l’utilisateur. Il y a plusieurs forces. Il peut y avoir
plusieurs représentations visuelles des mêmes données qui doivent être mises à jour ensemble. La
représentation visuelle peut changer, par exemple pour s’adapter à divers look and feel standard.
Les mécanismes d’interaction peuvent aussi changer, par exemple pour gérer des commandes
vocales. La solution consiste à répartir les responsabilités en trois composants séparés qui interagissent : le modèle, la vue et le contrôleur.
Bien sûr, cette architecture est plus complexe que le modèle "espace fenêtre" ; il est nécessaire d’étudier
certains détails pour que cette répartition des responsabilités puisse fonctionner correctement.
L’architecture Modèle-Vue-Contrôleur n’est pas le seul modèle utilisé dans la conception de Java. Par
exemple, le mécanisme de gestion d’événements d’AWT suit le modèle d’écouteurs d’événement.
Un aspect important des modèles de conception est qu’ils font maintenant partie de la culture. Les
programmeurs à travers le monde comprennent si vous parlez de l’architecture Modèle-VueContrôleur ou du modèle d’écouteurs d’événement. Les modèles d’architecture sont un moyen
efficace de discuter des problèmes de conception.
La Figure 9.4 illustre les interactions entre les objets modèle, vue et contrôleur.
Figure 9.4
Interactions entre
les objets modèles, vue
et contrôleur.
Controller
View
Model
paint view
read content
update content
content changed
update view
Livre Java .book Page 390 Jeudi, 25. novembre 2004 3:04 15
390
Au cœur de Java 2 - Notions fondamentales
En tant que programmeur utilisant les composants Swing, vous n’avez pas besoin de penser à
l’architecture Modèle-Vue-Contrôleur. Chaque interface dispose d’une classe enveloppe, ou
wrapper, comme JButton ou JTextField qui stocke le modèle et la vue. Lorsque vous souhaitez connaître le contenu d’un composant, par exemple d’un champ de texte, la classe enveloppe
transmet cette requête à la vue. Toutefois, il existe des occasions où la classe ne fonctionne pas
de manière idéale pour la transmission des commandes. Vous devez alors lui demander de récupérer
le modèle pour agir directement avec ce dernier. Vous n’avez pas besoin de travailler directement avec la vue, car cette tâche incombe au code qui est lié au style d’interface implémenté
(LookAndFeel).
Outre la possibilité d’exécuter l’action appropriée, cette architecture permet aux concepteurs
d’implémenter un style d’interface dynamique, le plaf ou pluggable look and feel. Le modèle d’un
bouton ou d’un champ de texte est indépendant du style d’interface, mais sa représentation visuelle
dépend totalement de la conception d’une interface de style particulier. Le contrôleur peut également
varier. Par exemple, sur une machine contrôlée par la voix, le contrôleur doit faire face à un ensemble d’événements totalement différent de celui d’un ordinateur standard muni d’un clavier et d’une
souris. En séparant le modèle sous-jacent de l’interface utilisateur, les concepteurs de Swing peuvent
réutiliser le code des modèles et même passer d’un look and feel à un autre au cours d’un programme
en cours d’exécution.
Bien sûr, les modèles sont prévus pour servir de guides ; vous n’êtes pas forcé de les suivre à la
lettre. Ils ne sont pas applicables dans toutes les situations. Par exemple, vous pouvez éprouver des
difficultés à suivre le modèle "espace de fenêtre" (voir plus haut) pour réorganiser votre studette. De
la même manière, les concepteurs Swing se sont aperçus que dans la réalité, l’implémentation d’un
style dynamique d’interface ne permet pas toujours une réalisation propre de l’architecture ModèleVue-Contrôleur. Les modèles sont faciles à distinguer ; chaque composant d’interface possède une
classe de modèle. Mais les responsabilités de la vue et du contrôleur ne sont pas toujours clairement
séparées et sont réparties à travers un certain nombre de classes différentes. Bien sûr, en tant qu’utilisateur de ces classes, vous ne serez pas concerné par tout cela. En fait, comme nous l’avons signalé,
vous n’aurez pas à vous soucier des modèles ; vous pourrez simplement exploiter les classes enveloppes du composant.
Une analyse Modèle-Vue-Contrôleur des boutons Swing
Vous avez déjà appris à utiliser des boutons au chapitre précédent sans avoir à vous préoccuper
des objets contrôleur, modèle ou vue. Les boutons comptent parmi les éléments d’interface les
plus simples ; nous les utiliserons donc pour nous familiariser avec cette architecture. Vous
rencontrerez des types de classes et d’interfaces similaires pour les composants Swing plus
sophistiqués.
Pour la plupart des composants, la classe modèle implémente une interface dont le nom se
termine par Model, d’où l’interface appelée ButtonModel. Les classes l’implémentant peuvent
définir l’état des divers types de boutons. En fait, les boutons ne sont pas aussi complexes et la
bibliothèque Swing contient une unique classe appelée DefaultButtonModel qui implémente
cette interface.
Livre Java .book Page 391 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
391
Pour connaître le type des données qui sont gérées par un modèle de boutons, examinez les méthodes
de l’interface ButtonModel. Le Tableau 9.1 présente les méthodes d’accès (accessor).
Tableau 9.1 : Les méthodes d’accès de l’interface ButtonModel
getActionCommand()
La chaîne de commande d’action associée à ce bouton
getMnemonic()
Le raccourci clavier pour ce bouton
isArmed()
true si le bouton a été pressé et que la souris est toujours au-dessus du bouton
isEnabled()
true si le bouton peut être sélectionné
isPressed()
true si le bouton de commande a été pressé, mais que le bouton de la souris
n’a pas encore été relâché
isRollover()
true si la souris se trouve au-dessus du bouton
isSelected()
true si l’état du bouton a été basculé (pour les cases à cocher et les boutons
radio)
Chaque objet JButton stocke un objet modèle de bouton que vous pouvez récupérer :
JButton button = new JButton("Blue");
ButtonModel model = button.getModel();
Dans la pratique, vous n’avez pas à vous en préoccuper — les détails de l’état du bouton n’intéressent que la vue qui le dessine. Les informations importantes, telles que savoir si un bouton est activé,
sont disponibles au moyen de la classe JButton (bien sûr, c’est l’objet JButton qui demande à son
modèle d’obtenir ces informations).
Examinez l’interface ButtonModel pour noter ce qui manque. Le modèle ne conserve pas le
libellé ou l’icône du bouton. Il est impossible de savoir ce qui se trouve en surface d’un bouton
par un simple examen de son modèle. En fait, comme vous le constaterez dans la section relative
aux groupes de boutons radio, ce côté épuré de la conception est une source d’ennuis pour le
programmeur.
Il importe également de signaler que le même modèle, en l’occurrence DefaultButtonModel, est
utilisé pour les boutons de commande, les boutons radio, les cases à cocher et même les options de
menu. Bien sûr, chacun de ces types de boutons possède des vues et des contrôleurs différents. Lorsque le style d’interface Metal est employé, l’objet JButton utilise une classe appelée BasicButtonUI pour la vue, et une classe appelée ButtonUIListener comme contrôleur. En général, chaque
composant Swing possède un objet vue associé qui se termine par les lettres UI. Mais tous les
composants Swing ne possèdent pas d’objets contrôleur dédiés.
Après cette brève introduction sur les particularités sous-jacentes de JButton, vous vous interrogez
sans doute sur la nature réelle de cet objet : en fait, il s’agit d’une classe enveloppe dérivée de la
classe JComponent qui contient l’objet DefaultButtonModel, certaines données de vue — comme
le libellé et les icônes — et l’objet BasicButtonUI, responsable de la vue du bouton.
Livre Java .book Page 392 Jeudi, 25. novembre 2004 3:04 15
392
Au cœur de Java 2 - Notions fondamentales
Introduction à la gestion de mise en forme
Avant de poursuivre avec d’autres composants Swing, comme les champs de texte et les boutons
radio, nous devons brièvement traiter de la façon dont les composants peuvent être disposés dans un
cadre. A la différence de VisualBasic, le JDK ne possède pas de concepteur de formulaire. Vous
devez écrire du code pour positionner les composants d’interface.
Bien sûr, si vous disposez d’un environnement de développement qui accepte Java, il possédera
probablement un outil de mise en forme pour automatiser certaines de ces tâches, ou la totalité.
Néanmoins, il importe de maîtriser le processus interne de telles opérations, car même le meilleur de
ces outils nécessitera une intervention manuelle.
Commençons par réexaminer le programme du dernier chapitre qui utilise des boutons pour modifier
la couleur de fond d’une fenêtre (voir Figure 9.5).
Figure 9.5
Un panneau
avec trois boutons.
Nous avons construit ce programme de la façon suivante :
1. Nous avons défini l’apparence de chaque bouton en passant au constructeur une chaîne pour le
libellé, par exemple :
JButton yellowButton = new JButton("Yellow");
2. Puis nous avons ajouté les boutons individuels à un panneau, par exemple avec la ligne :
Panel.add(yellowButton);
3. Ensuite, nous avons ajouté les gestionnaires d’événement nécessaires, comme :
yellowButton.addActionListener(listener);
Que se passe-t-il si nous augmentons le nombre de boutons ? La Figure 9.6 montre ce qui se
produit avec six boutons dans la fenêtre. Comme vous pouvez le constater, ils sont centrés horizontalement sur une ligne, et lorsqu’il n’y a plus de place, une nouvelle ligne est commencée.
De plus, les boutons restent centrés même lorsque l’utilisateur redimensionne la fenêtre (voir
Figure 9.7).
Un concept très élégant permet cette mise en forme dynamique. Tous les composants dans un conteneur sont positionnés par un gestionnaire de mise en forme (Layout). Dans notre exemple, les
boutons sont gérés par FlowLayout, le gestionnaire par défaut pour un panneau JPanel.
Livre Java .book Page 393 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
393
Figure 9.6
Une fenêtre avec six boutons gérés par le gestionnaire FlowLayout.
Figure 9.7
Le changement de taille
de la fenêtre provoque la
réorganisation automatique des boutons.
Ce gestionnaire aligne horizontalement les composants jusqu’à ce qu’il n’y ait plus de place et
commence une nouvelle ligne.
Si l’utilisateur modifie la taille du conteneur, le gestionnaire de mise en forme réorganise automatiquement les composants en fonction de l’espace disponible.
Vous pouvez choisir la disposition des composants sur chaque ligne. Par défaut, ils sont centrés horizontalement par rapport au conteneur. Les autres choix sont un alignement sur la gauche ou sur la
droite du conteneur. Pour exploiter un de ces alignements, spécifiez les constantes symboliques LEFT
ou RIGHT dans le constructeur de l’objet FlowLayout :
panel setLayout(new FlowLayout(FlowLayout.LEFT);
INFO
Normalement, vous laissez le gestionnaire contrôler simplement les intervalles verticaux et horizontaux entre les
différents composants. Vous pouvez toutefois imposer un intervalle horizontal ou vertical spécifique à l’aide d’une
autre version du constructeur du gestionnaire (voir les notes API).
java.awt.Container 1.0
•
setLayout(LayoutManager m)
Configure le gestionnaire de mise en forme pour ce conteneur.
Livre Java .book Page 394 Jeudi, 25. novembre 2004 3:04 15
394
Au cœur de Java 2 - Notions fondamentales
java.awt.FlowLayout 1.0
•
FlowLayout(int align)
Construit un nouveau gestionnaire FlowLayout avec l’alignement spécifié.
Paramètres :
•
align
L’une des constantes d’alignement LEFT, CENTER ou RIGHT
(gauche, centré ou droite).
FlowLayout(int align, int hgap, int vgap)
Construit un nouveau gestionnaire FlowLayout avec l’alignement et les intervalles horizontaux
et verticaux spécifiés.
Paramètres :
align
L’une des constantes d’alignement LEFT, CENTER ou RIGHT.
hgap
L’intervalle horizontal en pixels à utiliser (les valeurs
négatives provoquent un chevauchement).
vgap
L’intervalle vertical en pixels à utiliser (les valeurs négatives
provoquent un chevauchement).
Gestionnaire BorderLayout
Java fournit plusieurs gestionnaires de mise en forme ; vous pouvez même construire vos
propres gestionnaires. Nous aborderons tous ces aspects plus loin dans ce chapitre. Toutefois,
afin de vous donner immédiatement des exemples plus intéressants, nous devons brièvement
décrire un autre gestionnaire appelé BorderLayout. Il s’agit du gestionnaire par défaut du
panneau de contenu contentPane de chaque JFrame. A la différence de FlowLayout qui
contrôle complètement la position de chaque composant, BorderLayout vous permet de choisir
l’emplacement de chaque composant par l’intermédiaire d’une constante de positionnement
s’inspirant des points cardinaux : au centre, au nord, au sud, à l’est ou à l’ouest du panneau
conteneur (voir Figure 9.8).
Figure 9.8
Le gestionnaire BorderLayout.
North
West
Center
East
South
Par exemple :
panel.setLayout(new BorderLayout());class MyPanel extends JPanel
panel.add(yellowButton, BorderLayout.SOUTH);
Livre Java .book Page 395 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
395
Les composants en bordure sont placés en premier et l’espace disponible restant est occupé par le
centre. Lorsque le conteneur est redimensionné, la densité des composants en lisière ne change pas,
mais le composant central voit ses dimensions modifiées. Vous ajoutez un composant en spécifiant
une constante CENTER, NORTH, SOUTH, EAST ou WEST de la classe BorderLayout. Toutes ces positions
ne doivent pas nécessairement être occupées. Si vous ne fournissez aucune chaîne, la constante
"Center" est utilisée.
INFO
Les constantes BorderLayout sont définies comme des chaînes. Par exemple, BorderLayout.SOUTH est défini
comme la chaîne "South". De nombreux programmeurs préfèrent utiliser directement les chaînes, plus courtes ; par
exemple frame.add(component, "South"). Cependant, si vous faites une erreur en écrivant la chaîne, le compilateur ne pourra pas la détecter.
A la différence du gestionnaire FlowLayout qui préserve la taille des composants, BorderLayout en
augmente la taille pour remplir l’espace disponible.
A l’instar des gestionnaires FlowLayout, si vous souhaitez spécifier un intervalle entre les différentes
régions, vous pouvez le faire dans le constructeur de la classe BorderLayout.
Comme indiqué précédemment, l’objet conteneur d’une fenêtre JFrame possède un gestionnaire
BorderLayout. Jusqu’à présent, nous n’en avons pas tiré avantage ; nous n’avons ajouté les
panneaux que dans la zone par défaut, le centre. Mais vous pouvez aussi ajouter des composants
dans les autres zones :
frame.add(yellowButton, BorderLayout.SOUTH);
Ce fragment de code pose toutefois un problème que nous verrons dans la prochaine section.
java.awt.Container 1.0
•
void add(Component c, Object constraints) 1.1
Ajoute un composant dans le conteneur.
Paramètres :
c
Le composant à ajouter.
constraints
Un identifiant accepté par le gestionnaire de mise en forme.
java.awt.BorderLayout 1.0
•
BorderLayout(int hgap, int vgap)
Construit un nouveau gestionnaire BorderLayout avec les intervalles horizontaux et verticaux
spécifiés entre les composants.
Paramètres :
hgap
L’intervalle horizontal en pixels à utiliser (les valeurs
négatives provoquent un chevauchement).
vgap
L’intervalle vertical en pixels à utiliser (les valeurs négatives
provoquent un chevauchement).
Livre Java .book Page 396 Jeudi, 25. novembre 2004 3:04 15
396
Au cœur de Java 2 - Notions fondamentales
Panneaux
Un gestionnaire BorderLayout n’est pas très utile en tant que tel. La Figure 9.9 illustre ce qui se
produit lorsque vous utilisez le fragment de code ci-dessus. Le bouton a été agrandi pour remplir
toute la région sud de la fenêtre. Si vous en ajoutiez un autre dans la même région, il déplacerait le
premier.
Une méthode couramment employée pour contourner ce problème est l’usage des panneaux supplémentaires. Les panneaux agissent comme des petits conteneurs pour les éléments d’interface, qui
peuvent à leur tour être disposés dans un panneau plus grand soumis au contrôle d’un gestionnaire
de mise en forme. Par exemple, vous pouvez avoir un panneau dans la zone sud pour les boutons et
un autre au centre pour le texte. En imbriquant les panneaux et en utilisant une combinaison de
gestionnaires BorderLayout et FlowLayout, vous pouvez obtenir un positionnement relativement
précis des différents composants. Cette façon de procéder est certainement suffisante pour élaborer
un prototype ; c’est aussi la méthode que nous adopterons pour les programmes cités en exemple
dans la première partie de ce chapitre. Reportez-vous à la section sur le gestionnaire GridBagLayout
pour découvrir un moyen de positionnement plus précis des composants.
Figure 9.9
Un seul bouton géré par
un gestionnaire BorderLayout.
Par exemple, examinez la Figure 9.10. Les trois boutons à la base de la fenêtre sont contenus dans un
panneau qui est placé au sud du conteneur.
Figure 9.10
Un panneau placé au sud
du cadre.
Supposons que vous vouliez ajouter un panneau avec trois boutons (voir Figure 9.10). Créez d’abord
un nouvel objet JPanel avant l’ajout de chaque bouton. Le gestionnaire de mise en forme par défaut
pour un panneau est FlowLayout, ce qui convient dans notre cas. Vous ajouterez ensuite les boutons
individuels au moyen de la méthode add déjà étudiée. Comme vous ajoutez des boutons à un
panneau et ne changez pas le gestionnaire de mise en forme par défaut FlowLayout, la position et la
taille des boutons sont soumises à son contrôle. Ainsi, ils resteront centrés par rapport au panneau et
Livre Java .book Page 397 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
397
ne s’agrandiront pas pour occuper toute sa surface. Voici un extrait de code qui permet d’ajouter un
panneau avec trois boutons à l’extrémité sud du cadre :
JPanel panel = new JPanel();
panel.add(yellowButton);
panel.add(blueButton);
panel.add(redButton);
frame.add(panel, BorderLayout.SOUTH);
INFO
Les frontières du panneau sont invisibles à l’utilisateur. Les panneaux ne constituent qu’un mécanisme d’organisation
pour le concepteur de l’interface utilisateur.
Pour mémoire, la classe JPanel utilise par défaut un gestionnaire de mise en forme FlowLayout.
Pour un conteneur JPanel, vous pouvez fournir un objet gestionnaire de mise en forme différent
dans le constructeur. Même si la plupart des autres conteneurs ne possèdent pas de constructeur, tous
les conteneurs disposent de la méthode setLayout pour définir un gestionnaire de mise en forme
autre que celui par défaut du conteneur.
javax.swing.JPanel 1.2
•
JPanel(LayoutManager m)
Définit le gestionnaire de mise en forme pour le panneau.
Disposition des grilles
La disposition des grilles organise les composants, un peu à la manière des lignes et des colonnes
d’un tableur. Toutefois, toutes les lignes et les colonnes de la grille ont une taille identique. Le
programme de calculatrice (voir Figure 9.11) emploie la disposition de grille pour en organiser les
boutons. Lorsque vous redimensionnez la fenêtre, les boutons s’agrandissent et diminuent tout en
conservant des tailles identiques.
Figure 9.11
Une calculette.
Dans le constructeur de l’objet GridLayout, vous spécifiez le nombre de lignes et de colonnes
requis :
panel.setLayout(new GridLayout(5, 4));
Livre Java .book Page 398 Jeudi, 25. novembre 2004 3:04 15
398
Au cœur de Java 2 - Notions fondamentales
A l’image des gestionnaires BorderLayout et FlowLayout, vous pouvez aussi spécifier les intervalles
verticaux et horizontaux souhaités :
panel.setLayout(new GridLayout(5, 4, 3, 3));
Les deux derniers paramètres de ce constructeur spécifient la taille des intervalles horizontaux et
verticaux (en pixels) entre les composants.
Vous ajoutez les composants en commençant par la première entrée sur la première ligne, puis la
deuxième sur la même ligne, etc. :
panel.add(new JButton("1"));
panel.add(new JButton("2"));
L’Exemple 9.1 présente le code source du programme de la calculette. Il s’agit d’une calculatrice
ordinaire, et non d’une version qui utilise la notation "polonaise inversée", si populaire dans les
didacticiels Java. Dans ce programme, nous appelons la méthode pack après avoir ajouté le composant au cadre. Cette méthode emploie les tailles préférées de tous les composants pour calculer la
largeur et la hauteur du cadre.
Bien sûr, peu d’applications possèdent une mise en forme aussi rigide que la façade d’une calculatrice. Dans la pratique, de petites grilles (contenant généralement une seule ligne ou colonne)
permettent d’organiser des zones partielles d’une fenêtre. Si vous voulez disposer d’une ligne de
boutons de taille identique, vous pouvez les placer à l’intérieur d’un panneau géré par un gestionnaire GridLayout avec une seule ligne.
Exemple 9.1 : Calculator.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Calculator
{
public static void main(String[] args)
{
CalculatorFrame frame = new CalculatorFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec un panneau pour une calculatrice.
*/
class CalculatorFrame extends JFrame
{
public CalculatorFrame()
{
setTitle("Calculator");
CalculatorPanel panel = new CalculatorPanel();
add(panel);
pack();
}
}
Livre Java .book Page 399 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
/**
Un panneau avec les boutons de la calculatrice
et l’affichage des résultats.
*/
class CalculatorPanel extends JPanel
{
public CalculatorPanel()
{
setLayout(new BorderLayout());
result = 0;
lastCommand = "=";
start = true;
// ajouter la zone d’affichage
display = new JButton("0");
display.setEnabled(false);
add(display, BorderLayout.NORTH);
ActionListener insert = new InsertAction();
ActionListener command = new CommandAction();
// ajouter les boutons dans une grille 4 x 4
panel = new JPanel();
panel.setLayout(new GridLayout(4, 4));
addButton("7",
addButton("8",
addButton("9",
addButton("/",
insert);
insert);
insert);
command);
addButton("4",
addButton("5",
addButton("6",
addButton("*",
insert);
insert);
insert);
command);
addButton("1",
addButton("2",
addButton("3",
addButton("-",
insert);
insert);
insert);
command);
addButton("0",
addButton(".",
addButton("=",
addButton("+",
insert);
insert);
command);
command);
add(panel, BorderLayout.CENTER);
}
/**
ajoute un bouton au panneau central.
@param label Le libellé du bouton
@param listener L’écouteur du bouton
*/
399
Livre Java .book Page 400 Jeudi, 25. novembre 2004 3:04 15
400
Au cœur de Java 2 - Notions fondamentales
private void addButton(String label, ActionListener listener)
{
JButton button = new JButton(label);
button.addActionListener(listener);
panel.add(button);
}
/**
Cette action insère la chaîne d’action du bouton
à la fin du texte d’affichage.
*/
private class InsertAction implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
String input = event.getActionCommand();
if (start)
{
display.setText("");
start = false;
}
display.setText(display.getText() + input);
}
}
/**
Cette action exécute la commande indiquée par la chaîne
d’action du bouton.
*/
private class CommandAction implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
String command = event.getActionCommand();
if (start)
{
if (command.equals("-"))
{
display.setText(command);
start = false;
}
else
lastCommand = command;
}
else
{
calculate(Double.parseDouble(display.getText()));
lastCommand = command;
start = true;
}
}
}
/**
Exécute le calcul en attente.
@param x La valeur à cumuler avec le résultat précédent.
*/
public void calculate(double x)
Livre Java .book Page 401 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
401
{
if (lastCommand.equals("+")) result += x;
else if (lastCommand.equals("-")) result -= x;
else if (lastCommand.equals("*")) result *= x;
else if (lastCommand.equals("/")) result /= x;
else if (lastCommand.equals("=")) result = x;
display.setText("" + result);
}
private
private
private
private
private
JButton display;
JPanel panel;
double result;
String lastCommand;
boolean start;
}
java.awt.GridLayout 1.0
•
GridLayout(int row, int cols)
Construit une nouvelle grille GridLayout.
Paramètres :
•
rows
Le nombre de lignes dans la grille.
columns
Le nombre de colonnes dans la grille.
GridLayout(int rows, int columns, int hgap, int vgap)
Construit une nouvelle grille GridLayout avec des intervalles horizontaux et verticaux entre les
composants.
Paramètres :
rows
Le nombre de lignes dans la grille.
columns
Le nombre de colonnes dans la grille.
hgap
L’intervalle horizontal en pixels (les valeurs négatives
provoquent un chevauchement).
vgap
L’intervalle vertical en pixels (les valeurs négatives provoquent
un chevauchement).
java.awt.Window 1.0
•
void pack()
Redimensionne la fenêtre, en prenant en compte les tailles préférées de ses composants.
Entrée de texte
Nous sommes prêts pour l’introduction des composants Swing d’interface utilisateur. Vous pouvez
utiliser les composants JTextField et JTextArea pour collecter les entrées de texte. Un champ de
texte n’accepte qu’une ligne de texte, et les zones de texte peuvent en accueillir plusieurs. Ces deux
classes dérivent d’une classe appelée JTextComponent. Vous ne pourrez pas construire vous-même
un objet JTextComponent, car il s’agit d’une classe abstract. En revanche, cas fréquent avec Java
lors de la lecture de la documentation sur les API, vous constaterez que les méthodes recherchées se
trouveront plus souvent dans la classe parent JTextComponent que dans les classes dérivées. Par
exemple, les méthodes qui permettent de récupérer ou de définir le texte dans un champ ou une zone
de texte sont en réalité des méthodes de la classe JTextComponent.
Livre Java .book Page 402 Jeudi, 25. novembre 2004 3:04 15
402
Au cœur de Java 2 - Notions fondamentales
javax.swing.text.JTextComponent 1.2
•
void setText(String t)
Modifie le texte d’un composant texte.
Paramètre :
•
t
Le nouveau texte.
String getText()
Renvoie le texte contenu dans le composant texte.
•
void setEditable(boolean b)
Détermine si l’utilisateur peut modifier le contenu de l’objet JTextComponent.
Champs de texte
La méthode pour ajouter un champ de texte dans une fenêtre consiste à l’ajouter à un panneau ou un
conteneur, comme pour un bouton :
JPanel panel = new JPanel();
JTextField textField = new JTextField("Default input", 20).
panel.add(textField);
Ce code ajoute un champ de texte et l’initialise avec la chaîne "Default input". Le second paramètre de ce constructeur en définit la largeur. Dans notre exemple, la largeur est de 20 "colonnes".
Malheureusement, une colonne est une unité de mesure assez imprécise. Elle représente la largeur
attendue d’un caractère dans la police employée pour le texte. Si vous prévoyez des entrées utilisateur d’au plus n caractères, vous êtes supposé indiquer n en tant que largeur de colonne. Dans la
pratique, cette mesure ne donne pas de très bons résultats et vous devrez ajouter 1 ou 2 à la longueur
d’entrée maximale prévue. Gardez aussi à l’esprit que le nombre de colonnes n’est qu’une suggestion pour AWT pour indiquer une taille de préférence. Si le gestionnaire de mise en forme a besoin
d’agrandir ou de réduire le champ de texte, il peut en ajuster la taille. La largeur de colonne que vous
définissez dans le constructeur de la classe JTextField ne limite pas pour autant le nombre de
caractères que l’utilisateur peut taper. Il peut introduire des chaînes plus longues ; toutefois, la vue
de l’entrée défile lorsque le texte dépasse la longueur du champ, ce qui est assez irritant pour l’utilisateur ; prévoyez donc généreusement l’espace nécessaire. Si vous devez redéfinir le nombre de
colonnes durant l’exécution, vous pouvez le faire avec la méthode setColumns.
ASTUCE
Après avoir changé la taille d’un champ de texte avec la méthode setColumns, vous devez appeler la méthode
revalidate du conteneur :
textField.setColumns(10);
panel.revalidate();
Cette méthode recalcule la taille et la disposition de tous les composants dans un conteneur. Après son utilisation, le
gestionnaire de mise en forme redimensionne le conteneur avec la taille du champ modifiée.
La méthode revalidate appartient à la classe JComponent. Elle ne redimensionne pas immédiatement le composant, elle le signale simplement pour un redimensionnement ultérieur. Cette approche évite des calculs répétitifs
lorsqu’il faut redimensionner plusieurs composants. Si vous souhaitez toutefois recalculer tous les composants dans
un JFrame, vous devez appeler la méthode validate (JFrame n’étend pas JComponent).
Livre Java .book Page 403 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
403
En général, vous autorisez l’utilisateur à ajouter du texte (ou à modifier le texte existant) dans les
champs de texte ; ceux-ci apparaissent donc vides le plus souvent lors du premier affichage. Pour
qu’un champ soit vide, il suffit de ne pas passer de chaîne pour le paramètre concerné du constructeur de
JTextField :
JTextField textField=new JTextField(20);
Vous pouvez modifier le contenu du champ de texte à tout moment avec la méthode setText de la
classe parent TextComponent mentionnée dans la section précédente. Par exemple :
textField.setText("Hello!");
De plus, vous pouvez déterminer ce que l’utilisateur a tapé en appelant la méthode getText. Elle
renvoie tout ce que l’utilisateur a entré, y compris les espaces en tête et en fin de chaîne. Vous pouvez
les supprimer grâce à la méthode trim lors de la récupération de l’entrée :
String Text = textField.getText().trim();
Pour modifier la police du texte saisi par l’utilisateur, appelez la méthode setFont.
javax.swing.JTextField 1.2
•
JTextField(int cols)
Construit un JTextField vide avec un nombre spécifié de colonnes.
Paramètres :
•
cols
Le nombre de colonnes dans le champ
JTextField(String text, int cols)
Construit un nouveau JTextField avec une chaîne initiale et le nombre spécifié de colonnes.
Paramètres :
•
text
Le texte à afficher
cols
Le nombre de colonnes
void setColumns(int cols)
Indique au champ texte le nombre de colonnes à utiliser.
Paramètres :
cols
Le nombre de colonnes
javax.swing.JComponent 1.2
•
void revalidate()
Entraîne un nouveau calcul de la position et de la taille d’un composant.
java.awt.Component 1.0
•
void validate()
Recalcule la position et la taille d’un composant. Si le composant est un conteneur, les positions
et tailles de ses composants sont recalculées.
Etiquettes et composants d’étiquetage
Les étiquettes (ou libellés) sont des composants qui contiennent du texte. Ils ne possèdent pas
d’ornements, telle une bordure, et ne réagissent pas aux entrées de l’utilisateur. Vous pouvez
employer une étiquette pour identifier des composants comme les composants de texte, qui,
Livre Java .book Page 404 Jeudi, 25. novembre 2004 3:04 15
404
Au cœur de Java 2 - Notions fondamentales
contrairement aux boutons, n’ont pas de libellé. Pour associer une étiquette à un composant,
procédez ainsi :
1. Construisez un composant JLabel avec le texte voulu.
2. Placez-le suffisamment près du composant à identifier pour éviter toute ambiguïté.
Le constructeur d’un objet JLabel permet de spécifier le texte initial ou l’icône et, en option,
l’alignement du contenu. Vous pouvez utiliser l’interface SwingConstants pour spécifier l’alignement grâce aux constantes suivantes qu’elle définit : LEFT, RIGHT, CENTER, NORTH, EAST, etc. La
classe JLabel est l’une des classes Swing qui implémentent cette interface. Par conséquent, vous
pouvez spécifier que le texte d’une étiquette soit aligné sur la droite au moyen de l’une des deux
méthodes suivantes :
JLabel label = new JLabel("Minutes", SwingConstants.RIGHT);
ou :
JLabel label = new JLabel("Minutes", JLabel.RIGHT);
Les méthodes setText et setIcon vous permettent de définir le texte et l’icône de l’étiquette au
moment de l’exécution.
ASTUCE
A partir de la version 1.3 du JDK, vous pouvez avoir recours au texte normal ou HTML pour les boutons, les étiquettes
et les options de menu. L’usage du format HTML n’est pas recommandé pour les boutons — il interfère avec le look
and feel. Il peut en revanche être très efficace pour les étiquettes. Encadrez simplement la chaîne d’étiquettes avec
les balises <html>. . .</html>, de la façon suivante :
label = new JLabel("<html><b>Required</b> entry:</html>");
Sachez cependant que le premier composant avec une étiquette HTML prend un certain temps avant d’être affiché,
du fait du chargement du code de rendu HTML, plutôt complexe.
Les étiquettes peuvent être positionnées à l’intérieur d’un conteneur, à l’instar de tout autre composant.
Ainsi, vous pouvez exploiter les techniques déjà étudiées pour les disposer.
javax.swing.JLabel 1.2
•
JLabel(String text)
Construit une étiquette avec le texte aligné à gauche.
Paramètres :
•
text
Le texte de l’étiquette.
JLabel(Icon icon)
Construit une étiquette avec une icône alignée à gauche.
Paramètres :
•
icon
L’icône de l’étiquette.
JLabel(String text, int align)
Paramètres :
text
Le texte de l’étiquette.
align
L’une des constantes LEFT, CENTER ou RIGHT.
Livre Java .book Page 405 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
•
Swing et les composants d’interface utilisateur
405
JLabel(String text, Icon icon, int align)
Construit une étiquette avec un texte et une icône placée à gauche du texte.
Paramètres :
•
Le texte de l’étiquette.
icon
L’icône de l’étiquette.
align
L’une des constantes SwingConstants LEFT, CENTER
ou RIGHT.
void setText(String text)
Paramètres :
•
text
text
Le texte de l’étiquette.
void setIcon(Icon icon)
Paramètres :
icon
L’icône de l’étiquette.
Suivi des modifications dans les champs de texte
Nous allons maintenant utiliser quelques champs de texte. La Figure 9.12 présente l’application de
l’Exemple 9.2, il s’agit d’une horloge et de deux champs de texte qui permettent de saisir les heures
et les minutes. Dès que le contenu des champs est modifié, l’horloge est mise à l’heure.
Figure 9.12
Exemple de champs
de texte.
Garder la trace de tout changement intervenant dans les champs de texte nécessite des efforts supplémentaires. Tout d’abord, sachez que surveiller les frappes au clavier ne suffit pas. Certaines touches,
telles que les touches fléchées, ne modifient pas le texte. De plus, selon le style de look and feel
implémenté, certaines actions de la souris peuvent provoquer des modifications dans les champs.
Vous l’avez étudié au début de ce chapitre, le champ de texte Swing est implémenté via une méthode
générique : la chaîne que vous voyez dans le champ n’est qu’une manifestation visuelle (la vue)
d’une structure de données sous-jacente (le modèle). Bien sûr, pour un simple champ de texte, il
n’existe pas de différence importante entre ces deux concepts. La vue est une chaîne affichée et le
modèle est un objet chaîne. Toutefois, c’est cette même architecture qui est utilisée dans les composants d’édition plus avancés pour présenter du texte formaté avec des polices, des paragraphes et
d’autres attributs, représentés en interne par une structure de données plus complexe. Le modèle
pour tous les composants de texte est décrit par l’interface Document qui concerne aussi bien du
Livre Java .book Page 406 Jeudi, 25. novembre 2004 3:04 15
406
Au cœur de Java 2 - Notions fondamentales
texte simple que formaté, comme HTML. En fait, vous pouvez interroger le document (et non le
composant texte) pour être informé des changements, en prévoyant un écouteur de document :
textField.getDocument().addDocumentListener(listener);
Lorsque le texte a changé, l’une des méthodes DocumentListener suivantes est appelée :
void insertUpdate(DocumentEvent event)
void removeUpdate(DocumentEvent event)
void changedUpdate(DocumentEvent event)
Les deux premières méthodes sont appelées lorsque des caractères ont été insérés ou supprimés. La
troisième méthode n’est pas appelée pour les champs de texte. Pour des documents plus complexes,
elle sera appelée pour certains types de modifications, tel qu’un changement de mise en forme.
Malheureusement, il n’y a pas de fonction de rappel unique pour vous indiquer que le texte a été
modifié — généralement vous ne vous préoccupez pas de la façon dont il a changé. Il n’existe pas
non plus de classe adapter. Ainsi, l’écouteur de document doit implémenter les trois méthodes.
Voici ce qui se passe dans notre exemple de programme :
private class ClockFieldListener implements DocumentListener
{
public void insertUpdate(DocumentEvent event) { setClock(); }
public void removeUpdate(DocumentEvent event) { setClock(); }
public void changedUpdate(DocumentEvent event) {}
}
La méthode setClock appelle la méthode getText pour obtenir les chaînes tapées par l’utilisateur.
Pour notre programme, nous devons convertir les chaînes en entiers au moyen de la fastidieuse, mais
familière, formule :
int hours = Integer.parseInt(hourField.getText().trim());
int minutes = Integer.parseInt(minuteField.getText().trim());
Ce code ne fonctionnera toutefois pas correctement si l’utilisateur tape une chaîne telle que "deux",
qui ne représente pas un chiffre entier, ou s’il laisse le champ de texte vierge. La méthode parseInt
déclenche alors l’exception NumberFormatException. Nous allons pour l’instant l’intercepter et ne
pas mettre à jour l’horloge si l’utilisateur n’entre pas un nombre. Dans la prochaine section, vous
verrez comment empêcher l’utilisateur de saisir une entrée non valide.
INFO
Au lieu d’écouter les événements de document, vous pouvez aussi ajouter un écouteur d’action pour un champ de
texte. Celui-ci est notifié lorsque l’utilisateur appuie sur la touche Entrée. Nous ne recommandons pas cette approche, car les utilisateurs oublient parfois d’appuyer sur Entrée lorsqu’ils ont fini d’inscrire des données. Si vous
employez un tel écouteur, vous devrez également prévoir un écouteur de focus pour pouvoir déterminer quand
l’utilisateur quitte le champ de texte.
Enfin, notez comment le constructeur ClockPanel définit la taille de préférence :
public ClockPanel()
{
setPreferredSize(new Dimension(2 * RADIUS + 1, 2 * RADIUS + 1));
}
Lorsque la méthode pack du cadre calcule la taille du cadre, elle utilise la taille préférée du panneau.
Livre Java .book Page 407 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
Exemple 9.2 : TextTest.java
import
import
import
import
import
java.awt.*;
java.awt.event.*;
java.awt.geom.*;
javax.swing.*;
javax.swing.event.*;
public class TextTest
{
public static void main(String[] args)
{
TextTestFrame frame = new TextTestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec deux champs de texte pour définir l’heure.
*/
class TextTestFrame extends JFrame
{
public TextTestFrame()
{
setTitle("TextTest");
DocumentListener listener = new ClockFieldListener();
// ajouter un panneau avec les champs de texte
JPanel panel = new JPanel();
panel.add(new JLabel("Hours:"));
hourField = new JTextField("12", 3);
panel.add(hourField);
hourField.getDocument().addDocumentListener(listener);
panel.add(new JLabel("Minutes:"));
minuteField = new JTextField("00", 3);
panel.add(minuteField);
minuteField.getDocument().addDocumentListener(listener);
add(panel, BorderLayout.SOUTH);
// ajouter l’horloge
clock = new ClockPanel();
add(clock, BorderLayout.CENTER);
pack();
}
/**
Affecte à l’horloge les valeurs indiquées
dans les champs de texte.
*/
public void setClock()
{
try
407
Livre Java .book Page 408 Jeudi, 25. novembre 2004 3:04 15
408
Au cœur de Java 2 - Notions fondamentales
{
int hours
= Integer.parseInt(hourField.getText().trim());
int minutes
= Integer.parseInt(minuteField.getText().trim());
clock.setTime(hours, minutes);
}
catch (NumberFormatException e) {}
/* ne pas configurer l’horloge si les entrées
saisies ne peuvent pas être analysées */
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 300;
private JTextField hourField;
private JTextField minuteField;
private ClockPanel clock;
private class ClockFieldListener implements DocumentListener
{
public void insertUpdate(DocumentEvent event) { setClock(); }
public void removeUpdate(DocumentEvent event) { setClock(); }
public void changedUpdate(DocumentEvent event) {}
}
}
/**
Un panneau avec un dessin d’horloge.
*/
class ClockPanel extends JPanel
{
public ClockPanel()
{
setPreferredSize(new Dimension(2 * RADIUS + 1, 2 * RADIUS + 1));
}
public void paintComponent(Graphics g)
{
// dessiner le cadran
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
Ellipse2D circle
= new Ellipse2D.Double(0, 0, 2 * RADIUS, 2 * RADIUS);
g2.draw(circle);
// dessiner la grande aiguille
double hourAngle
= Math.toRadians(90 - 360 * minutes / (12 * 60));
drawHand(g2, hourAngle, HOUR_HAND_LENGTH);
// dessiner la petite aiguille
double minuteAngle
= Math.toRadians(90 - 360 * minutes / 60);
drawHand(g2, minuteAngle, MINUTE_HAND_LENGTH);
}
Livre Java .book Page 409 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
public void drawHand(Graphics2D g2,
double angle, double handLength)
{
Point2D end = new Point2D.Double(
RADIUS + handLength * Math.cos(angle),
RADIUS - handLength * Math.sin(angle));
Point2D center = new Point2D.Double(RADIUS, RADIUS);
g2.draw(new Line2D.Double(center, end));
}
/**
Définit l’heure à afficher sur l’horloge
@param h Heures
@param m Minutes
*/
public void setTime(int h, int m)
{
minutes = h * 60 + m;
repaint();
}
private
private
private
private
double minutes = 0;
int RADIUS = 100;
double MINUTE_HAND_LENGTH = 0.8 * RADIUS;
double HOUR_HAND_LENGTH = 0.6 * RADIUS;
}
javax.swing.JComponent 1.2
•
void setPreferredSize(Dimension d)
Définit la taille préférée de ce composant.
javax.swing.text.Document 1.2
•
int getLength()
Renvoie le nombre de caractères actuellement dans le document.
•
String getText(int offset, int length)
Renvoie le texte contenu dans la partie de document indiquée.
Paramètres :
•
offset
Le début du texte.
length
La longueur de la chaîne désirée.
void addDocumentListener(DocumentListener listener)
Enregistre l’écouteur qui doit être notifié lorsque le document change.
javax.swing.event.DocumentEvent 1.2
•
Document getDocument()
Récupère le document qui est à la source de l’événement.
javax.swing.event.DocumentListener 1.2
•
void changedUpdate(DocumentEvent event)
Appelée chaque fois qu’un attribut ou un ensemble d’attributs est modifié.
409
Livre Java .book Page 410 Jeudi, 25. novembre 2004 3:04 15
410
•
Au cœur de Java 2 - Notions fondamentales
void insertUpdate(DocumentEvent event)
Appelée chaque fois qu’une insertion est effectuée dans le document.
•
void removeUpdate(DocumentEvent event)
Appelée chaque fois qu’une partie du document a été supprimée.
Champs de mot de passe
Le champ de mot de passe est un type spécial de champ de texte. Pour éviter que des voisins curieux
ne puissent apercevoir le mot de passe entré par un utilisateur, les caractères tapés ne sont pas affichés. Un caractère d’écho est utilisé à la place, généralement un astérisque (*). La bibliothèque
Swing fournit une classe JPasswordField qui implémente ce genre de champ.
Le champ de mot de passe est un autre exemple de la puissance de l’architecture Modèle-VueContrôleur. Il utilise le même modèle qu’un champ de texte standard pour conserver les données,
mais sa vue a été modifiée pour n’afficher que des caractères d’écho.
javax.swing.JPasswordField 1.2
• JPasswordField(String text, int columns)
Construit un nouveau champ de mot de passe.
Paramètres :
•
text
Le texte à afficher ou la valeur null s’il n’y en a pas.
columns
Le nombre de colonnes.
void setEchoChar(char echo)
Définit le caractère d’écho pour le champ de mot de passe. Un certain style d’interface peut
proposer son propre choix de caractères d’écho. La valeur 0 rétablit le caractère d’écho utilisé
par défaut.
Paramètres :
•
echo
Le caractère d’écho à afficher au lieu des caractères de texte.
char[] getPassword()
Renvoie le texte contenu dans le champ de mot de passe. Pour renforcer la sécurité, vous devez
écraser le contenu du tableau renvoyé après utilisation. Le mot de passe n’est pas retourné en tant
que String, car une chaîne resterait dans la machine virtuelle jusqu’à ce qu’elle soit éliminée
par le processus de nettoyage de mémoire (le ramasse-miettes ou garbage collector).
Champs de saisie mis en forme
Dans le dernier exemple, l’utilisateur du programme devait taper des chiffres, et non des chaînes
arbitraires. L’utilisateur n’est autorisé à taper que des chiffres de 0 à 9 et un signe moins "–". Si ce
signe est utilisé, il doit représenter le premier caractère de la chaîne d’entrée. En apparence, la validation d’entrée semble simple. Nous pouvons mettre en œuvre un écouteur de touche pour le champ
de texte et bloquer (consume) tous les événements des touches qui ne représentent pas un chiffre ou
un signe moins. Malheureusement, cette approche simple, bien que recommandée comme méthode
de validation d’entrée, ne fonctionne pas bien dans la pratique. Tout d’abord, certaines associations
de touches autorisées ne constituent pas obligatoirement une entrée valide, par exemple, --3 ou 3-3.
Plus important encore, il existe d’autres moyens de modifier le texte qui ne font pas appel à la pression d’une touche. Selon le style d’interface implémenté, certaines combinaisons clavier peuvent
servir pour couper, copier ou coller du texte. Par exemple, dans le style Metal, la combinaison de
Livre Java .book Page 411 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
411
touches Ctrl+V colle le contenu du tampon dans le champ de texte. Pour cette raison, nous devrions
aussi nous assurer que l’utilisateur ne colle pas de caractères invalides. Bref, cette tentative de filtrer
les frappes au clavier pour valider une entrée commence à devenir complexe. C’est un cas précis
d’éléments dont un programmeur ne devrait pas avoir à s’inquiéter.
Avant le JDK 1.4, il n’existait pas de composants permettant de saisir des valeurs numériques.
Depuis la première édition de "Au cœur de Java", nous avons proposé une implémentation pour un
IntTextField, un champ de texte permettant de saisir un entier correctement mis en forme. Dans
chaque nouvelle édition, nous avons modifié l’implémentation pour profiter de tout avantage, même
limité, des divers schémas de validation ajoutés à chaque version du JDK. Enfin, dans le JDK 1.4, les
concepteurs Swing se sont attaqués au problème et ont fourni une classe JFormattedTextField qui
peut être utilisée non seulement pour la saisie de chiffres, mais également pour les dates et pour des
mises en forme plus ésotériques, comme les adresses IP.
Saisie d’entiers
Commençons par un cas facile : un champ de texte pour la saisie d’un entier :
JFormattedTextField intField = new
JFormattedTextField(NumberFormat.getIntegerInstance());
NumberFormat.getIntegerInstance renvoie un objet de mise en forme qui formate les entiers à
l’aide des paramètres régionaux. Dans les paramètres français, les virgules servent de séparateurs
décimaux, ce qui permet de saisir des valeurs comme 1,72. Le chapitre du Volume 2 sur l’internationalisation explique en détail comment sélectionner d’autres paramètres locaux.
Comme pour tout champ de texte, vous pouvez définir le nombre de colonnes :
intField.setColumns(6);
La méthode setValue peut être accompagnée d’une valeur par défaut. Elle prend un paramètre
Object, vous devrez donc envelopper la valeur int par défaut dans un objet Integer :
intField.setValue(new Integer(100));
Les utilisateurs saisissent généralement des informations dans plusieurs champs de texte, puis
cliquent sur un bouton pour lire toutes les valeurs. Après ce clic, vous pouvez récupérer la valeur
fournie par l’utilisateur grâce à la méthode getValue. Elle renvoie un résultat Object et vous devez
la transtyper dans le type approprié. JFormattedTextField renvoie un objet du type Long si l’utilisateur a modifié la valeur et l’objet Integer initial si ce n’est pas le cas. Il vous faut donc transtyper
la valeur de retour sur la superclasse Number habituelle :
Number value = (Number) intField.getValue();
int v = value.intValue();
Le champ de texte mis en forme n’est pas très intéressant tant que vous ne pensez pas à ce qui
survient lorsque l’utilisateur saisit des données non autorisées. C’est le sujet de la prochaine section.
Comportement en cas de perte de focalisation
Imaginons un utilisateur entrant des données dans un champ de texte. Il tape des informations, puis
décide finalement de quitter le champ, par exemple en cliquant sur un autre composant. Le champ de
texte perd alors le focus (la focalisation). Le curseur en I n’y est plus visible et les frappes sur les
touches sont destinées à un autre composant.
Livre Java .book Page 412 Jeudi, 25. novembre 2004 3:04 15
412
Au cœur de Java 2 - Notions fondamentales
Lorsque le champ de texte mis en forme perd le focus, l’élément de mise en forme étudie la chaîne
de texte produite par l’utilisateur. S’il sait la convertir en objet, le texte est considéré comme valide,
sinon il est signalé non valide. Vous pouvez utiliser la méthode isEditValid pour vérifier la validité
du champ de texte.
Le comportement par défaut en cas de perte de focus est appelé "commit or revert" (engager ou
retourner). Si la chaîne de texte est valide, elle est engagée (committed). Le formateur la transforme
en objet, qui devient la valeur actuelle du champ (c’est-à-dire la valeur de retour de la méthode
getValue vue à la section précédente). La valeur est ensuite retransformée en chaîne, qui devient la
chaîne de texte visible dans le champ. Le formateur d’entier reconnaît par exemple que l’entrée 1729
est valide, il définit la valeur actuelle sur new Long(1729), puis la retransforme en chaîne en insérant
une espace pour les milliers : 1 729.
A l’inverse, si la chaîne de texte n’est pas valide, la valeur n’est pas modifiée et le champ de
texte retourne à la chaîne représentant l’ancienne valeur. Par exemple, si l’utilisateur entre une
valeur erronée, comme x1, l’ancienne valeur est récupérée lorsque le champ de texte perd le
focus.
INFO
Le formateur d’entier considère une chaîne de texte comme valide si elle commence par un entier. Par exemple,
1729x est une chaîne valide. Elle est transformée en 1729, puis mise en forme (1 729).
La méthode setFocusLostBehavior permet de définir d’autres comportements. Le comportement
"engager" (commit) est légèrement différent du comportement par défaut. Si la chaîne de texte n’est
pas valide, elle et la valeur du champ restent inchangées, elles sont dites hors synchronisation. Le
comportement "persister" (persist) est encore plus conservateur. Même si la chaîne de texte est
valide, ni le champ de texte ni la valeur actuelle ne sont modifiés. Il faut alors appeler commitEdit,
setValue ou setText pour les resynchroniser. Enfin, il existe un comportement dit "retourner"
(revert) qui ne semble avoir aucune utilité. Lors d’une perte de focus, la saisie utilisateur est ignorée
et la chaîne de texte retourne à l’ancienne valeur.
INFO
Le comportement par défaut "engager ou retourner" (commit or revert) convient généralement bien. Il ne pose
qu’un problème potentiel. Supposons qu’une boîte de dialogue contienne un champ de texte pour une valeur
d’entier. Un utilisateur entre la chaîne " 1729", avec une espace préalable, puis clique sur OK. La première
espace invalide le nombre, la valeur du champ retourne donc à l’ancienne valeur. L’écouteur d’action du bouton
OK récupère la valeur du champ et ferme la boîte de dialogue. L’utilisateur ne sait donc pas que la nouvelle
valeur a été refusée. Ici, le fait de choisir le comportement "engager" convient parfaitement, vous faites ensuite
vérifier par l’écouteur du bouton OK que toutes les modifications du champ sont valides avant de fermer la boîte
de dialogue.
Filtres
Cette fonction de base des champs de texte mis en forme est simple et suffit dans la plupart des
cas. Vous pouvez toutefois affiner quelque peu le processus. Il est possible, par exemple, d’empêcher l’utilisateur d’entrer d’autres caractères que les chiffres. Pour ce faire, vous utiliserez un
Livre Java .book Page 413 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
413
filtre de document. Pour mémoire, dans l’architecture Modèle-Vue-Contrôleur, le contrôleur
traduit les événements de saisie en commandes qui modifient le document sous-jacent du champ
de texte, c’est-à-dire la chaîne de texte stockée dans un objet PlainDocument. Par exemple, lorsque le contrôleur traite une commande qui entraîne l’insertion de texte dans le document, il
appelle la commande "insert string". La chaîne à insérer peut être un caractère unique ou le
contenu du tampon. Un filtre de document interceptera cette commande et modifiera la chaîne ou
annulera l’insertion. Voici le code de la méthode insertString d’un filtre qui analyse la chaîne
à insérer et n’insère que les caractères numériques ou le signe - (le code gère des caractères
Unicode complémentaires, ainsi qu’il est expliqué au Chapitre 3 ; voir Chapitre 12 pour la classe
StringBuilder) :
public void insertString(FilterBypass fb,
int offset, String string, AttributeSet attr)
throws BadLocationException
{
StringBuilder builder = new StringBuilder(string);
for (int i = builder.length() - 1; i >= 0; i--)
{
int cp = builder.codePointAt(i);
if (!Character.isDigit(cp) && cp != ’-’)
{
builder.deleteCharAt(i);
if (Character.isSupplementaryCodePoint(cp))
{
i--;
builder.deleteCharAt(i);
}
}
}
super.insertString(fb, offset, builder.toString(), attr);
}
Vous devez également écraser la méthode replace de la classe DocumentFilter : elle est appelée
lorsque le texte est sélectionné, puis remplacé. L’implémentation de la méthode replace est simple
(voir le programme à la fin de cette section).
Vous devez maintenant installer le filtre du document. Il n’existe malheureusement pas de méthode
simple pour le faire. Vous devez remplacer la méthode getDocumentFilter d’une classe formatter, puis transmettre un objet de cette classe formatter au JFormattedTextField. Le champ de
texte d’entier utilise un InternationalFormatter initialisé avec NumberFormat.getIntegerInstance(). Ce code installe un formateur qui produira le filtre désiré :
JFormattedTextField intField = new JFormattedTextField(new
InternationalFormatter(NumberFormat.getIntegerInstance())
{
protected DocumentFilter getDocumentFilter()
{
return filter;
}
private DocumentFilter filter = new IntFilter();
});
Livre Java .book Page 414 Jeudi, 25. novembre 2004 3:04 15
414
Au cœur de Java 2 - Notions fondamentales
INFO
La documentation du JDK précise que la classe DocumentFilter a été inventée pour éviter le sous-classement.
Jusqu’au JDK 1.3, le filtrage dans un champ de texte s’obtenait par l’extension de la classe PlainDocument et le
remplacement des méthodes insertString et replace. Aujourd’hui, la classe PlainDocument dispose d’un filtre
modifiable dynamiquement (pluggable). C’est une très bonne amélioration. Pourtant, il aurait encore mieux valu
que le filtre soit aussi modifiable dynamiquement dans la classe formatter. Ce n’est hélas pas le cas, et nous devons
sous-classer le formateur.
Testez le programme d’exemple FormatTest à la fin de cette section. Un filtre est installé pour le
troisième champ de texte. Vous ne pouvez insérer que des chiffres ou le caractère moins (-). Sachez
que vous pouvez toujours entrer des chaînes non valides comme "1-2-3". En général, le filtrage ne
permet pas d’éviter toutes les chaînes non valides. La chaîne "-", par exemple, n’est pas valide mais
un filtre ne peut pas la refuser car c’est le préfixe d’une chaîne autorisée comme "-1". Même si les
filtres ne constituent pas une protection parfaite, leur utilisation est logique pour refuser des entrées
qui apparaissent immédiatement non valides.
ASTUCE
Le filtrage permet aussi de transformer tous les caractères d’une chaîne en majuscules. Ce filtre est simple à écrire.
Dans les méthodes insertString et replace du filtre, transformez la chaîne à insérer en majuscules, puis appelez
la méthode superclass.
Vérificateurs
Il existe un autre mécanisme qui peut être utile pour avertir les utilisateurs de saisies invalides. Il s’agit
d’un vérificateur qui s’attache à n’importe quel JComponent. Si le composant perd le focus, le vérificateur est appelé. Lorsqu’il signale que le contenu du composant n’est pas valide, le composant
reprend immédiatement le focus. L’utilisateur est alors obligé de corriger le contenu avant de
pouvoir saisir d’autres informations.
Un vérificateur doit étendre la classe abstraite InputVerifier et définir une méthode verify. Il est
particulièrement facile de définir un vérificateur qui vérifie les champs de texte mis en forme. La
méthode isEditValid de la classe JFormattedTextField appelle le formateur et renvoie true s’il
peut transformer la chaîne de texte en objet.
Voici le vérificateur :
class FormattedTextFieldVerifier extends InputVerifier
{
public boolean verify(JComponent component)
{
JFormattedTextField field = (JFormattedTextField) component;
return field.isEditValid();
}
}
Vous pouvez l’attacher à tout JFormattedTextField :
intField.setInputVerifier(new FormattedTextFieldVerifier());
Livre Java .book Page 415 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
415
Toutefois, un vérificateur n’est pas à l’épreuve de toutes les erreurs. Si vous cliquez sur un bouton, il
avertit ses écouteurs d’action avant qu’un composant non valide ne reprenne le focus. Les écouteurs
d’action peuvent alors récupérer un résultat non valide du composant dont la vérification a échoué.
Et il existe une raison à ce comportement : les utilisateurs peuvent vouloir appuyer sur le bouton
"Annuler" sans avoir à corriger une entrée non valide.
Un vérificateur est attaché au quatrième champ de texte dans le programme d’exemple. Essayez
d’entrer un nombre non valide (comme x1729) et appuyez sur la touche de tabulation ou cliquez sur
un autre champ de texte. Sachez que le champ reprend immédiatement le focus. Toutefois, si vous
appuyez sur le bouton OK, l’écouteur d’action appelle getValue, qui signale la dernière valeur
correcte.
Autres formateurs standard
JFormattedTextField prend en charge d’autres formateurs en plus du formateur d’entier. La classe
NumberFormat possède les méthodes statiques
getNumberInstance
getCurrencyInstance
getPercentInstance
qui produisent respectivement les formateurs des nombres à virgule flottante, des valeurs de devises
et des pourcentages. Vous pouvez par exemple obtenir un champ de texte pour la saisie des valeurs
de devise en appelant :
JFormattedTextField currencyField =
➥new JFormattedTextField(NumberFormat.getCurrencyInstance());
Pour modifier les dates et les heures, appelez l’une des méthodes statiques de la classe DateFormat :
getDateInstance
getTimeInstance
getDateTimeInstance
Par exemple :
JFormattedTextField dateField = new
JFormattedTextField(DateFormat.getDateInstance());
Ce champ transforme une date au format "moyen" ou par défaut comme :
Feb 24, 2002
Vous pouvez aussi choisir un format "court" comme (format américain)
2/24/02
en appelant plutôt
DateFormat.getDateInstance(DateFormat.SHORT)
INFO
Par défaut, le format de date est assez "clément". Ainsi, une date non valide comme le 31 février 2002 est transformée pour indiquer la prochaine date valide, à savoir le 3 mars 2002. Attention, ce comportement peut surprendre les
utilisateurs ! Dans ce cas, appelez setLenient(false) sur l’objet DateFormat.
Livre Java .book Page 416 Jeudi, 25. novembre 2004 3:04 15
416
Au cœur de Java 2 - Notions fondamentales
DefaultFormatter est capable de mettre en forme les objets de toute classe qui disposent d’un
constructeur avec un paramètre de chaîne et une méthode toString correspondante. Par exemple, la
classe URL dispose d’un constructeur URL(String) pouvant être utilisé pour construire une URL
depuis une chaîne, comme
URL url = new URL("http://java.sun.com");
Vous pouvez donc utiliser DefaultFormatter pour mettre en forme les objets URL. Le formateur
appelle toString sur la valeur du champ pour initialiser le texte. Lorsque le champ perd le focus, le
formateur construit un nouvel objet de la même classe que la valeur actuelle, en utilisant le constructeur avec un paramètre String. Si ce constructeur déclenche une exception, la modification n’est pas
valide. Vous pouvez tester cette situation dans le programme d’exemple en entrant une URL qui ne
commence pas par un préfixe comme "http:".
INFO
Par défaut, DefaultFormatter est en mode overwrite (mode de remplacement). Cette situation est différente
pour les autres formateurs et finalement pas très utile. Appelez setOverwriteMode(false) pour désactiver le
mode overwrite.
Enfin, MaskFormatter convient bien aux motifs à taille fixe qui contiennent des caractères constants
et des caractères variables. Les numéros de sécurité sociale, par exemple (comme 1-56-08-75-205081-78), peuvent être mis en forme de la manière suivante :
new MaskFormatter("#-##-##-##-###-###-##")
Le symbole # remplace un chiffre. Le Tableau 9.2 montre les symboles que vous pouvez utiliser
dans un formateur de masque.
Tableau 9.2 : Symboles de MaskFormatter
#
Un chiffre
?
Une lettre
U
Une lettre, transformée en majuscule
L
Une lettre, transformée en minuscule
A
Une lettre ou un chiffre
H
Un chiffre hexadécimal [0-9A-Fa-f]
*
Tout caractère
’
Caractère d’échappement pour inclure un symbole dans le motif
Livre Java .book Page 417 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
417
Vous pouvez limiter les caractères pouvant être tapés dans le champ en appelant l’une des méthodes
de la classe MaskFormatter :
setValidCharacters
setInvalidCharacters
Par exemple, pour lire une note scolaire exprimée par une lettre (comme A+ ou F), vous pourriez
utiliser :
MaskFormatter formatter = new MaskFormatter("U*");
formatter.setValidCharacters("ABCDF+- ");
Il n’existe toutefois aucune méthode permettant d’indiquer que le deuxième caractère ne doit pas
être une lettre.
Sachez que la chaîne mise en forme par le formateur du masque a exactement la même longueur que
le masque. Si l’utilisateur efface des caractères pendant la modification, ceux-ci sont remplacés par
le caractère d’emplacement. Ce caractère est, par défaut, une espace, mais vous pouvez le modifier
grâce à la méthode setPlaceholderCharacter, par exemple :
formatter.setPlaceholderCharacter(’0’);
Par défaut, un formateur de masque agit en mode de recouvrement, un mode assez intuitif (testez le
programme d’exemple). Sachez également que la position du curseur passe au-dessus des caractères
fixes sur le masque.
Le formateur de masque est très efficace pour les motifs rigides comme les numéros de sécurité
sociale ou les numéros de téléphone. Sachez qu’aucune variation n’est admise dans le motif du
masque. Vous ne pouvez pas, par exemple, utiliser un formateur de masque pour les numéros de téléphone internationaux, dont le nombre de chiffres varie.
Formateurs personnalisés
Lorsque aucun des formateurs standard ne convient, vous pouvez assez facilement définir le vôtre.
Envisagez des adresses IP à 4 octets, comme
130.65.86.66
Vous ne pouvez pas utiliser un MaskFormatter car chaque octet pourrait être représenté par un, deux
ou trois chiffres. Il faut aussi s’assurer que la valeur de chaque octet n’excède pas 255.
Pour personnaliser votre formateur, étendez la classe DefaultFormatter et remplacez les méthodes
String valueToString(Object value)
Object stringToValue(String text)
La première méthode transforme la valeur du champ en chaîne qui s’affiche dans le champ de texte.
La seconde analyse le texte tapé par l’utilisateur et le retransforme en objet. Si l’une ou l’autre
méthode détecte une erreur, elle lance une exception ParseException.
Dans notre exemple de programme, nous stockons une adresse IP dans un tableau byte[] de
longueur 4. La méthode valueToString forme une chaîne qui sépare les octets par des points.
Livre Java .book Page 418 Jeudi, 25. novembre 2004 3:04 15
418
Au cœur de Java 2 - Notions fondamentales
Sachez que les valeurs d’octet sont des quantités signées comprises entre -?128 et 127. Pour transformer des valeurs d’octets négatives en valeurs d’entier non signées, ajoutez 256 :
public String valueToString(Object value) throws ParseException
{
if (!(value instanceof byte[]))
throw new ParseException("Not a byte[]", 0);
byte[] a = (byte[]) value;
if (a.length != 4)
throw new ParseException("Length != 4", 0);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 4; i++)
{
int b = a[i];
if (b < 0) b += 256;
builder.append(String.valueOf(b));
if (i < 3) builder.append(’.’);
}
return builder.toString();
}
A l’inverse, la méthode stringToValue analyse la chaîne et produit un objet byte[] si elle est
valide. Dans le cas contraire, elle déclenche une exception ParseException :
public Object stringToValue(String text) throws ParseException
{
StringTokenizer tokenizer = new StringTokenizer(text, ".");
byte[] a = new byte[4];
for (int i = 0; i < 4; i++)
{
int b = 0;
try
{
b = Integer.parseInt(tokenizer.nextToken());
}
catch (NumberFormatException e)
{
throw new ParseException("Not an integer", 0);
}
if (b < 0 || b >= 256)
throw new ParseException("Byte out of range", 0);
a[i] = (byte) b;
}
return a;
}
Testez le champ d’adresse IP dans le programme d’exemple. Si vous entrez une adresse non valide,
le champ retourne à la dernière adresse valide.
Le programme de l’exemple 9.3 montre les actions de divers champs de texte mis en forme (voir
Figure 9.13). Cliquez sur OK pour récupérer les valeurs actuelles des champs.
Livre Java .book Page 419 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
419
INFO
La lettre d’informations en ligne "Swing Connection" contient un petit article qui décrit un formateur correspondant
aux expressions ordinaires. Voir http://java.sun.com/products/jfc/tsc/articles/reftf/.
Figure 9.13
Le programme
FormatTest.
Exemple 9.3 : FormatTest.java
import
import
import
import
import
import
import
import
java.awt.*;
java.awt.event.*;
java.lang.reflect.*;
java.net.*;
java.text.*;
java.util.*;
javax.swing.*;
javax.swing.text.*;
/**
Un programme pour tester les champs de texte mis en forme
*/
public class FormatTest
{
public static void main(String[] args)
{
FormatTestFrame frame = new FormatTestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec une série de champs de texte
mis en forme et un bouton qui affiche les valeurs du champ.
*/
class FormatTestFrame extends JFrame
{
public FormatTestFrame()
{
setTitle("FormatTest");
setSize(WIDTH, HEIGHT);
JPanel buttonPanel = new JPanel();
okButton = new JButton("OK");
buttonPanel.add(okButton);
add(buttonPanel, BorderLayout.SOUTH);
Livre Java .book Page 420 Jeudi, 25. novembre 2004 3:04 15
420
Au cœur de Java 2 - Notions fondamentales
mainPanel = new JPanel();
mainPanel.setLayout(new GridLayout(0, 3));
add(mainPanel, BorderLayout.CENTER);
JFormattedTextField intField = new
JFormattedTextField(NumberFormat.getIntegerInstance());
intField.setValue(new Integer(100));
addRow("Number:", intField);
JFormattedTextField intField2 = new
JFormattedTextField(NumberFormat.getIntegerInstance());
intField2.setValue(new Integer(100));
intField2.setFocusLostBehavior(JFormattedTextField.COMMIT);
addRow("Number (Commit behavior):", intField2);
JFormattedTextField intField3
= new JFormattedTextField(new
InternationalFormatter(NumberFormat.getIntegerInstance())
{
protected DocumentFilter getDocumentFilter()
{
return filter;
}
private DocumentFilter filter = new IntFilter();
});
intField3.setValue(new Integer(100));
addRow("Filtered Number", intField3);
JFormattedTextField intField4 = new
JFormattedTextField(NumberFormat.getIntegerInstance());
intField4.setValue(new Integer(100));
intField4.setInputVerifier(new FormattedTextFieldVerifier());
addRow("Verified Number:", intField4);
JFormattedTextField currencyField
= new JFormattedTextField(NumberFormat.getCurrencyInstance());
currencyField.setValue(new Double(10));
addRow("Currency:", currencyField);
JFormattedTextField dateField = new
JFormattedTextField(DateFormat.getDateInstance());
dateField.setValue(new Date());
addRow("Date (default):", dateField);
DateFormat format = DateFormat.getDateInstance(DateFormat.SHORT);
format.setLenient(false);
JFormattedTextField dateField2 = new JFormattedTextField(format);
dateField2.setValue(new Date());
addRow("Date (short, not lenient):", dateField2);
try
{
DefaultFormatter formatter = new DefaultFormatter();
formatter.setOverwriteMode(false);
JFormattedTextField urlField = new
JFormattedTextField(formatter);
urlField.setValue(new URL("http://java.sun.com"));
addRow("URL:", urlField);
}
Livre Java .book Page 421 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
catch (MalformedURLException e)
{
e.printStackTrace();
}
try
{
MaskFormatter formatter = new MaskFormatter("###-##-####");
formatter.setPlaceholderCharacter(’0’);
JFormattedTextField ssnField = new
JFormattedTextField(formatter);
ssnField.setValue("078-05-1120");
addRow("SSN Mask:", ssnField);
}
catch (ParseException exception)
{
exception.printStackTrace();
}
JFormattedTextField ipField = new JFormattedTextField(new
IPAddressFormatter());
ipField.setValue(new byte[] { (byte) 130, 65, 86, 66 });
addRow("IP Address:", ipField);
}
/**
Ajoute une ligne au panneau principal.
@param labelText Le libellé du champ
@param field Le champ d’exemple
*/
public void addRow(String labelText, final JFormattedTextField field)
{
mainPanel.add(new JLabel(labelText));
mainPanel.add(field);
final JLabel valueLabel = new JLabel();
mainPanel.add(valueLabel);
okButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
Object value = field.getValue();
if (value.getClass().isArray())
{
StringBuilder builder = new StringBuilder();
builder.append(’{’);
for (int i = 0; i < Array.getLength(value); i++)
{
if (i > 0) builder.append(’,’);
builder.append(Array.get(value, i).toString());
}
builder.append(’}’);
valueLabel.setText(builder.toString());
}
else
valueLabel.setText(value.toString());
}
});
}
421
Livre Java .book Page 422 Jeudi, 25. novembre 2004 3:04 15
422
Au cœur de Java 2 - Notions fondamentales
public static final int WIDTH = 500;
public static final int HEIGHT = 250;
private JButton okButton;
private JPanel mainPanel;
}
/**
Un filtre qui limite la saisie aux chiffres et au signe ’-’.
*/
class IntFilter extends DocumentFilter
{
public void insertString(FilterBypass fb,
int offset, String string, AttributeSet attr)
throws BadLocationException
{
StringBuilder builder = new StringBuilder(string);
for (int i = builder.length() - 1; i >= 0; i--)
{
int cp = builder.codePointAt(i);
if (!Character.isDigit(cp) && cp != ’-’)
{
builder.deleteCharAt(i);
if (Character.isSupplementaryCodePoint(cp))
{
i--;
builder.deleteCharAt(i);
}
}
}
super.insertString(fb, offset, builder.toString(), attr);
}
public void replace(FilterBypass fb, int offset, int length,
String string, AttributeSet attr)
throws BadLocationException
{
if (string != null)
{
StringBuilder builder = new StringBuilder(string);
for (int i = builder.length() - 1; i >= 0; i--)
{
int cp = builder.codePointAt(i);
if (!Character.isDigit(cp) && cp != ’-’)
{
builder.deleteCharAt(i);
if (Character.isSupplementaryCodePoint(cp))
{
i--;
builder.deleteCharAt(i);
}
}
}
string = builder.toString();
}
Livre Java .book Page 423 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
super.replace(fb, offset, length, string, attr);
}
}
/**
Un vérificateur qui vérifie si le contenu d’un
champ de texte mis en forme est valide.
*/
class FormattedTextFieldVerifier extends InputVerifier
{
public boolean verify(JComponent component)
{
JFormattedTextField field = (JFormattedTextField) component;
return field.isEditValid();
}
}
/**
Un formateur pour les adresses IP à 4 octets de la forme a.b.c.d
*/
class IPAddressFormatter extends DefaultFormatter
{
public String valueToString(Object value)
throws ParseException
{
if (!(value instanceof byte[]))
throw new ParseException("Not a byte[]", 0);
byte[] a = (byte[]) value;
if (a.length != 4)
throw new ParseException("Length != 4", 0);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 4; i++)
{
int b = a[i];
if (b < 0) b += 256;
builder.append(String.valueOf(b));
if (i < 3) builder.append(’.’);
}
return builder.toString();
}
public Object stringToValue(String text) throws ParseException
{
StringTokenizer tokenizer = new StringTokenizer(text, ".");
byte[] a = new byte[4];
for (int i = 0; i < 4; i++)
{
int b = 0;
if (!tokenizer.hasMoreTokens())
throw new ParseException("Too few bytes", 0);
try
{
b = Integer.parseInt(tokenizer.nextToken());
}
catch (NumberFormatException e)
{
throw new ParseException("Not an integer", 0);
}
423
Livre Java .book Page 424 Jeudi, 25. novembre 2004 3:04 15
424
Au cœur de Java 2 - Notions fondamentales
if (b < 0 || b >= 256)
throw new ParseException("Byte out of range", 0);
a[i] = (byte) b;
}
if (tokenizer.hasMoreTokens())
throw new ParseException("Too many bytes", 0);
return a;
}
}
javax.swing.JFormattedTextField 1.4
•
JFormattedTextField(Format fmt)
Construit un champ de texte qui utilise le format spécifié.
•
JFormattedTextField(JFormattedTextField.AbstractFormatter formatter)
Construit un champ de texte utilisant le formateur spécifié. Sachez que DefaultFormatter
et InternationalFormatter sont des sous-classes de JFormattedTextField.AbstractFormatter.
•
Object getValue()
Renvoie la valeur valide courante du champ. Sachez que ceci peut ne pas correspondre à la
chaîne modifiée.
•
void setValue(Object value)
Tente de définir la valeur de l’objet donné. La tentative échoue si le formateur ne peut pas
convertir l’objet en chaîne.
•
void commitEdit()
Tente de définir la valeur valide du champ à partir de la chaîne modifiée. La tentative peut
échouer si le formateur ne parvient pas à convertir la chaîne.
•
boolean isEditValid()
Vérifie si la chaîne modifiée représente une valeur valide.
•
•
void setFocusLostBehavior(int behavior)
int getFocusLostBehavior()
Définit ou récupère le comportement "focus perdu". Les valeurs autorisées pour le comportement sont les constantes COMMIT_OR_REVERT, REVERT, COMMIT et PERSIST de la classe JFormattedTextField.
java.text.DateFormat 1.1
•
•
•
•
•
•
static DateFormat getDateInstance()
static DateFormat getDateInstance(int dateStyle)
static DateFormat getTimeInstance()
static DateFormat getTimeInstance(int timeStyle)
static DateFormat getDateTimeInstance()
static DateFormat getDateTimeInstance(int dateStyle, int timeStyle)
Ces méthodes renvoient les formateurs qui produisent la date, l’heure ou la date et l’heure des
objets Date. Les valeurs autorisées pour dateStyle et timeStyle sont les constantes SHORT,
MEDIUM, LONG, FULL et DEFAULT de la classe DateFormat.
Livre Java .book Page 425 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
425
javax.swing.JFormattedTextFieldAbstractFormatter 1.4
•
abstract String valueToString(Object value)
Transforme une valeur en chaîne modifiable. Déclenche une exception ParseException si la
valeur n’est pas adaptée à ce formateur.
•
abstract Object stringToValue(String s)
Transforme une chaîne en valeur. Déclenche une exception ParseException si s n’a pas le
format approprié.
•
DocumentFilter getDocumentFilter()
Remplace cette méthode pour fournir un filtre de document qui limite les saisies dans le champ
de texte. Une valeur de retour null indique qu’aucun filtrage n’est nécessaire.
javax.swing.text.DefaultFormatter 1.2
•
•
void setOverwriteMode(boolean mode)
boolean getOverwriteMode()
Définit ou récupère le mode de remplacement. Si mode vaut true, de nouveaux caractères
remplacent les caractères existants lorsqu’ils modifient le texte.
javax.swing.text.DocumentFilter 1.1
•
void insertString(DocumentFilter.FilterBypass bypass, int offset, String text,
AttributeSet attrib)
Cette méthode est appelée avant l’insertion d’une chaîne dans un document. Vous pouvez
remplacer la méthode et modifier la chaîne. Vous pouvez désactiver l’insertion en n’appelant pas
super.insertString ou en appelant les méthodes bypass pour modifier le document sans
filtrage.
Paramètres :
•
bypass
Un objet qui permet d’exécuter des commandes de modification qui contournent le filtre.
offset
Le décalage auquel insérer le texte.
text
Les caractères à insérer.
attrib
Les attributs de mise en forme du texte inséré.
void replace(DocumentFilter.FilterBypass bypass, int offset, int length, String
text, AttributeSet attrib)
Cette méthode est appelée avant de remplacer une partie d’un document par une nouvelle chaîne.
Vous pouvez remplacer la méthode et modifier la chaîne. Vous pouvez désactiver le remplacement en n’appelant pas super.replace ou en appelant les méthodes bypass pour modifier le
document sans filtrage.
Paramètres :
bypass
Un objet qui permet d’exécuter des commandes de modification qui contournent le filtre.
offset
Le décalage auquel insérer le texte.
length
La longueur de la partie à remplacer.
text
Les caractères à insérer.
attrib
Les attributs de mise en forme du texte inséré.
Livre Java .book Page 426 Jeudi, 25. novembre 2004 3:04 15
426
•
Au cœur de Java 2 - Notions fondamentales
void remove(DocumentFilter.FilterBypass bypass, int offset, int length)
Cette méthode est appelée avant de supprimer une partie d’un document. Récupère le document
en appelant bypass.getDocument() pour analyser l’effet de la suppression.
Paramètres :
bypass
Un objet qui permet d’exécuter des commandes de modification qui contournent le filtre.
offset
Le décalage de la partie à supprimer.
length
La longueur de la partie à supprimer.
javax.swing.text.MaskFormatter 1.4
•
MaskFormatter(String mask)
Construit un formateur de masque avec le masque donné. Voyez le Tableau 9.2 pour connaître les
symboles d’un masque.
•
•
void setValidCharacters(String characters)
String getValidCharacters()
Définissent ou récupèrent les caractères de modification valides. Seuls les caractères de la chaîne
donnée sont acceptés pour les parties variables du masque.
•
•
void setInvalidCharacters(String characters)
String getInvalidCharacters()
Définissent ou récupèrent les caractères de modification non valides. Aucun des caractères de la
chaîne donnée n’est accepté comme entrée.
•
•
void setPlaceholderCharacter(char ch)
char getPlaceholderCharacter()
Définissent ou récupèrent le caractère d’emplacement utilisé pour les caractères variables dans le
masque que l’utilisateur n’a pas encore fourni. Le caractère d’emplacement par défaut est une
espace.
•
•
void setPlaceholder(String s)
String getPlaceholder()
Définissent ou récupèrent la chaîne d’emplacement. Son extrémité est utilisée si l’utilisateur n’a
pas fourni tous les caractères variables dans le masque. S’il est null ou plus court que le
masque, le caractère d’emplacement permet de remplir les entrées restantes.
•
•
void setValueContainsLiteralCharacters(boolean b)
boolean getValueContainsLiteralCharacters()
Définissent ou récupèrent la balise "la valeur contient les caractères littéraux". Si cette
balise vaut true, la valeur du champ contient les parties littérales (non variables) du
masque. Si elle vaut false, les caractères littéraux sont supprimés. Le paramètre par défaut
est true.
Zones de texte
Parfois, vous avez besoin de recueillir une entrée d’utilisateur d’une longueur supérieure à une
ligne. Pour mémoire, vous pouvez employer le composant JTextArea pour le faire. Lorsque
vous placez un composant de ce type dans votre programme, un utilisateur peut taper n’importe
quel nombre de lignes de texte en utilisant la touche Entrée pour les séparer. Chaque ligne se
Livre Java .book Page 427 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
427
termine par un caractère retour de ligne ’\n’. Si vous devez répartir l’entrée de l’utilisateur sur
plusieurs lignes, vous pouvez faire appel à la classe StringTokenizer (voir Chapitre 12). La
Figure 9.12 montre comment se présente une zone de texte.
Dans le constructeur du composant JTextArea, vous spécifiez le nombre de lignes et de colonnes
pour la zone. Par exemple :
textArea = new JTextArea(8, 40); // 8 lignes de 40 colonnes chacune
où le paramètre qui indique le nombre de colonnes fonctionne comme auparavant ; vous devez
toujours ajouter quelques colonnes (caractères) supplémentaires par précaution. L’utilisateur
n’est pas limité au nombre de lignes et de colonnes ; le texte défilera si l’entrée est supérieure
aux valeurs spécifiées. Vous pouvez également modifier le nombre de colonnes et de lignes en
utilisant, respectivement, les méthodes setColumns et setRows. Les valeurs données n’indiquent
qu’une préférence, le gestionnaire de mise en forme peut toujours agrandir ou réduire la zone de
texte.
Figure 9.14
Une zone de texte.
Si le texte entré dépasse la capacité d’affichage de la zone de texte, le reste du texte est coupé. Pour
éviter que des longues lignes ne soient tronquées, vous pouvez activer le retour automatique à la
ligne avec la méthode :
textArea.setLineWrap(true); // sauts de ligne automatiques
Ce renvoi à la ligne n’est qu’un effet visuel. Le texte dans le document n’est pas modifié, aucun
caractère ’\n’ n’est inséré.
Dans Swing, une zone de texte ne dispose pas de barres de défilement. Si vous souhaitez en ajouter,
prévoyez la zone de texte dans un panneau avec barres de défilement JScrollPane.
textArea = new JTextArea(8, 40);
JScrollPane scrollPane = new JScrollPane(textArea);
Le panneau de défilement gère ensuite la vue de la zone de texte. Des barres de défilement apparaissent automatiquement si le texte entré dépasse la zone d’affichage ; elles disparaissent si, lors d’une
suppression, le texte restant tient dans la zone de texte. Le défilement est géré en interne par le
panneau de défilement — votre programme n’a pas besoin de traiter les événements de défilement.
Livre Java .book Page 428 Jeudi, 25. novembre 2004 3:04 15
428
Au cœur de Java 2 - Notions fondamentales
ASTUCE
C’est un mécanisme général que vous rencontrerez fréquemment en travaillant avec Swing. Pour ajouter des barres
de défilement à un composant, placez-le à l’intérieur d’un panneau de défilement.
L’Exemple 9.4 donne le code complet pour le programme de démonstration de la zone de texte.
Il vous permet simplement de taper et de modifier un texte. Cliquez sur le bouton "Insert" pour
insérer une phrase à la fin du texte. Cliquez sur le second bouton pour activer ou désactiver le
renvoi à la ligne. Son nom bascule entre "Wrap" et "No wrap". Bien sûr, vous pouvez simplement utiliser le clavier pour changer le texte dans la zone. Notez que vous pouvez sélectionner
une portion du texte et réaliser des opérations de couper, copier et coller à l’aide des combinaisons Ctrl+X, Ctrl+C et Ctrl+V habituelles. Les raccourcis clavier sont spécifiques au style
d’interface. Ces combinaisons de touches particulières fonctionnent pour les look and feel
Metal et Windows.
INFO
Le composant JTextArea n’affiche que du texte brut, sans polices spéciales ni mise en forme. Pour afficher du
texte formaté tel que HTML ou RTF, servez-vous des classes JEditorPane et JTextPane, qui sont vues dans le
Volume 2.
Exemple 9.4 : TextAreaTest.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class TextAreaTest
{
public static void main(String[] args)
{
TextAreaFrame frame = new TextAreaFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec une zone de texte et des boutons
pour la mise à jour d’un texte
*/
class TextAreaFrame extends JFrame
{
public TextAreaFrame()
{
setTitle("TextAreaTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
buttonPanel = new JPanel();
Livre Java .book Page 429 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
// un bouton pour ajouter du texte à la fin de la zone
JButton insertButton = new JButton("Insert");
buttonPanel.add(insertButton);
insertButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
textArea.append("The quick brown fox
jumps over the lazy dog. ");
}
});
/* bouton pour activer/désactiver le retour
automatique à la ligne */
wrapButton = new JButton("Wrap");
buttonPanel.add(wrapButton);
wrapButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
boolean wrap = !textArea.getLineWrap();
textArea.setLineWrap(wrap);
scrollPane.revalidate();
wrapButton.setText(wrap ? "No wrap" : "Wrap");
}
});
add(buttonPanel, BorderLayout.SOUTH);
// ajouter une zone de texte avec défilement
textArea = new JTextArea(8, 40);
scrollPane = new JScrollPane(textArea);
add(scrollPane, BorderLayout.CENTER);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 300;
private
private
private
private
JTextArea textArea;
JScrollPane scrollPane;
JPanel buttonPanel;
JButton wrapButton;
}
javax.swing.JTextArea 1.2
•
JTextArea(int rows, int cols)
Construit une nouvelle zone de texte.
Paramètres :
rows
Le nombre de lignes.
cols
Le nombre de colonnes.
429
Livre Java .book Page 430 Jeudi, 25. novembre 2004 3:04 15
430
•
Au cœur de Java 2 - Notions fondamentales
JTextArea(String text, int rows, int cols)
Construit une nouvelle zone de texte avec un texte initial.
Paramètres :
•
text
Le texte initial.
rows
Le nombre de lignes.
cols
Le nombre de colonnes.
void setColumns(int cols)
Indique à la zone de texte le nombre de colonnes préféré à utiliser.
Paramètres :
•
cols
Le nombre de colonnes.
void setRows(int rows)
Indique à la zone de texte le nombre de lignes préféré à utiliser.
Paramètres :
•
rows
Le nombre de lignes.
void append(String newText)
Ajoute le texte spécifié à la fin du texte déjà présent dans la zone de texte.
Paramètres :
•
newText
Le texte à ajouter.
void setLineWrap(boolean wrap)
Active ou désactive le retour automatique à la ligne.
Paramètres :
•
wrap
true (vrai) si retour automatique des lignes longues.
void setWrapStyleWord(boolean word)
Si le paramètre word vaut true, les lignes longues sont scindées au niveau des fins de mot. Si la
valeur est false, elles sont divisées sans tenir compte des fins de mot.
•
void setTabSize(int c)
Définit les marques de tabulation toutes les c colonnes. Notez que les tabulations ne sont pas
converties en espaces, mais provoquent l’alignement avec la marque suivante.
Paramètres :
c
Le nombre de colonnes pour l’insertion d’une marque de
tabulation.
javax.swing.JScrollPane 1.2
•
JScrollPane(Component c)
Crée un panneau avec défilement qui affiche le contenu du composant spécifié. Les barres de
défilement apparaissent automatiquement lorsque le contenu du composant est plus grand que la
vue.
Paramètres :
c
Le composant à faire défiler.
Livre Java .book Page 431 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
431
Composants du choix
Vous savez maintenant comment recueillir du texte tapé par les utilisateurs. Toutefois, il est souvent
préférable de leur offrir un ensemble fini de choix plutôt que de leur demander d’introduire des
données dans un composant de texte. Au moyen d’un ensemble de boutons ou d’une liste d’options,
vous pouvez indiquer aux utilisateurs les choix mis à leur disposition. De plus, vous n’aurez pas
besoin de créer de procédures de contrôle d’erreurs pour ces types de sélections. Dans cette section,
vous apprendrez à programmer des cases à cocher, des boutons radio, des listes d’options et des
curseurs.
Cases à cocher
Si vous souhaitez recueillir une réponse qui se limite à une entrée "oui" ou "non", utilisez une case à
cocher. Ce type de composant s’accompagne d’un intitulé qui permet de les identifier. L’utilisateur
clique à l’intérieur de la case pour la cocher, et fait de même pour la désactiver. Pour ajouter/supprimer la coche, l’utilisateur peut également appuyer sur la barre d’espacement si le focus d’entrée se
trouve dans la case à cocher.
La Figure 9.15 illustre un programme simple avec deux cases à cocher : l’une pour activer ou désactiver l’attribut de mise en italique d’une police et l’autre pour l’attribut de mise en gras. Notez que le
focus d’entrée se trouve sur la deuxième case d’option, comme l’indique le rectangle autour de son
intitulé. Chaque fois que l’utilisateur clique sur l’une des options, l’écran est mis à jour avec les
nouveaux attributs de la police.
Figure 9.15
Exemples de cases
à cocher.
Les cases à cocher s’accompagnent d’un libellé qui indique leur rôle. Le texte prévu pour le libellé
est passé au constructeur.
bold = new JCheckBox("Bold");
La méthode setSelected permet d’activer ou de désactiver une case à cocher, comme dans la ligne
suivante :
bold.setSelected(true);
La méthode isSelected extrait le statut actuel de chaque case à cocher. Il est false si la case n’est
pas cochée, true le reste du temps.
Livre Java .book Page 432 Jeudi, 25. novembre 2004 3:04 15
432
Au cœur de Java 2 - Notions fondamentales
Lorsque l’utilisateur clique sur une case à cocher, un événement d’action est déclenché. Comme
toujours, vous associez un écouteur d’action à la case à cocher. Dans notre programme, les deux
cases partagent le même écouteur d’action :
ActionListener listener = . . .
bold.addActionListener(listener);
italic.addActionListener(listener);
La méthode actionPerformed interroge le statut des cases à cocher bold et italic et définit la
police du panneau à Normal, Gras ou Italique, ou les deux :
public void actionPerformed(ActionEvent event)
{
int mode = 0;
if (bold.isSelected()) mode += Font.BOLD;
if (italic.isSelected()) mode += Font.ITALIC;
label.setFont(new Font("Serif", mode, FONTSIZE));
}
L’Exemple 9.5 présente le programme complet d’implémentation des cases à cocher.
Exemple 9.5 : CheckBoxtTest.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class CheckBoxTest
{
public static void main(String[] args)
{
CheckBoxFrame frame = new CheckBoxFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec un label pour l’exemple de texte et des
cases à cocher pour sélectionner des attributs de police.
*/
class CheckBoxFrame extends JFrame
{
public CheckBoxFrame()
{
setTitle("CheckBoxTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter le label d’exemple de texte
label = new JLabel("The quick brown fox jumps over the lazy dog.");
label.setFont(new Font("Serif", Font.PLAIN, FONTSIZE));
add(label, BorderLayout.CENTER);
Livre Java .book Page 433 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
// Cet écouteur affecte à l’attribut de police du libellé
// le statut de la case à cocher
ActionListener listener = new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
int mode = 0;
if (bold.isSelected()) mode += Font.BOLD;
if (italic.isSelected()) mode += Font.ITALIC;
label.setFont(new Font("Serif", mode, FONTSIZE));
}
};
// ajouter les cases à cocher
JPanel buttonPanel = new JPanel();
bold = new JCheckBox("Bold");
bold.addActionListener(listener);
buttonPanel.add(bold);
italic = new JCheckBox("Italic");
italic.addActionListener(listener);
buttonPanel.add(italic);
add(buttonPanel, BorderLayout.SOUTH);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
private JLabel label;
private JCheckBox bold;
private JCheckBox italic;
private static final int FONTSIZE = 12;
}
javax.swing.JCheckBox 1.2
•
JCheckBox(String label)
Construit une case à cocher avec l’étiquette donnée qui est, à l’origine, non sélectionnée.
•
JCheckBox(String label, boolean state)
Construit une case à cocher avec l’étiquette donnée et l’état initial.
•
JCheckBox(String label, Icon icon)
Construit une case à cocher avec l’étiquette donnée et une icône initialement désactivée.
•
boolean isSelected ()
Renvoie l’état de la case à cocher.
•
void setSelected(boolean state)
Définit un nouvel état pour la case à cocher.
433
Livre Java .book Page 434 Jeudi, 25. novembre 2004 3:04 15
434
Au cœur de Java 2 - Notions fondamentales
Boutons radio
Dans l’exemple précédent, l’utilisateur pouvait activer ou désactiver une ou plusieurs options, ou
aucune. De nombreuses situations exigent que l’utilisateur ne puisse choisir qu’une seule option
parmi un ensemble de choix. Lorsqu’une seconde option est sélectionnée, la première est désactivée.
Cet ensemble d’options est souvent mis en œuvre au moyen d’un groupe de boutons radio. Ils sont
ainsi appelés, car ils fonctionnent de la même manière que les boutons d’une radio : lorsque vous
appuyez sur un bouton, celui qui était enfoncé ressort. La Figure 9.16 illustre un exemple typique.
L’utilisateur est autorisé à sélectionner une taille de police parmi plusieurs possibilités : Petit,
Moyen, Grand, et Très grand — bien sûr, il ne peut en choisir qu’une à la fois.
Figure 9.16
Un groupe de boutons
radio.
L’implémentation des groupes de boutons radio est facile avec Swing. Construisez un objet de type
ButtonGroup pour chaque groupe de boutons. Ajoutez ensuite des objets du type JRadioButton au
groupe. L’objet groupe est responsable de la désactivation de la première option choisie lors d’une
seconde sélection.
ButtonGroup group = new ButtonGroup();
JRadioButton smallButton
= new JRadioButton("Small", false);
group.add(smallButton);
JRadioButton mediumButton
= new JRadioButton("Medium", true);
group.add(mediumButton);
. . .
Passez la valeur true comme second argument au constructeur pour l’option qui doit être initialement sélectionnée et false pour les autres. Notez qu’un groupe de boutons ne contrôle que le
comportement des boutons. Si vous voulez grouper les options pour des raisons de présentation,
vous devez les placer dans un conteneur tel que JPanel.
Si vous observez de nouveau les Figures 9.15 et 9.16, vous noterez que les boutons radio n’ont pas
la même apparence que les cases à cocher. Ces dernières se présentent sous la forme d’un carré qui
contient une coche une fois qu’elles sont sélectionnées. Les boutons radio sont ronds et contiennent
un point lorsqu’ils sont activés.
Le mécanisme de notification d’événement est le même pour les boutons radio que pour les autres
boutons. Lorsque l’utilisateur active l’un des boutons, il génère un événement d’action. Dans notre
Livre Java .book Page 435 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
435
exemple de programme, nous définissons un écouteur d’action qui définit la taille de police à une
valeur donnée :
ActionListener listener = new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
// size fait référence au paramètre final de
// la méthode addRadioButton
label.setFont(new Font("Serif", Font.PLAIN,
size));
}
};
Comparez cette configuration d’écouteur à celle de l’exemple de case à cocher. Chaque bouton radio
obtient un objet écouteur différent. Chaque objet écouteur sait exactement ce qu’il doit faire — définir la taille de police à une valeur particulière. Dans le cas des cases à cocher, nous avons utilisé
une approche différente. Les deux cases à cocher avaient le même écouteur d’action ; il appelait une
méthode qui examinait l’état des deux cases à cocher.
Pourrions-nous adopter une approche similaire ici ? Nous pourrions avoir un seul écouteur qui
calculerait la taille de la façon suivante :
if (smallButton.isSelected()) size = 8;
else if (mediumButton.isSelected()) size = 12;
. . .
Nous préférons toutefois avoir recours à des objets écouteur d’action différents, car ils associent les
valeurs de taille aux boutons avec une plus grande précision.
INFO
Si vous utilisez un groupe de boutons radio, vous savez que seul l’un d’entre eux peut être choisi. Il serait intéressant
de pouvoir le localiser rapidement sans avoir à interroger tous les boutons du groupe. Puisque l’objet ButtonGroup
contrôle tous les boutons, il devrait pouvoir nous donner une référence à celui qui a été sélectionné. La classe
ButtonGroup possède bien une méthode getSelection, mais elle ne renvoie pas le bouton choisi. Elle retourne
une référence ButtonModel au modèle associé au bouton. Malheureusement, aucune des méthodes de ButtonModel n’est vraiment d’un grand secours. L’interface ButtonModel hérite d’une méthode getSelectedObjects
de l’interface ItemSelectable qui, sans grande utilité, renvoie la valeur null. La méthode getActionCommand
semble prometteuse, car la "commande d’action" d’un bouton radio est son texte de libellé. Mais la commande
d’action de son modèle vaut null. Ce n’est que si vous définissez explicitement les commandes d’action de tous les
boutons avec la méthode setActionCommand que les valeurs de commande d’action des modèles sont aussi définies.
Vous pouvez alors extraire la commande d’action du bouton actuellement sélectionné à l’aide de :
buttonGroup.getSelection().getActionCommand()
L’Exemple 9.6 illustre le programme complet de sélection de taille de police qui met en œuvre un
ensemble de boutons radio.
Exemple 9.6 : RadioButtonTest.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
Livre Java .book Page 436 Jeudi, 25. novembre 2004 3:04 15
436
Au cœur de Java 2 - Notions fondamentales
public class RadioButtonTest
{
public static void main(String[] args)
{
RadioButtonFrame frame = new RadioButtonFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.show();
}
}
/**
Un cadre avec un label pour l’exemple de texte et des
boutons radio pour la sélection de taille de police.
*/
class RadioButtonFrame extends JFrame
{
public RadioButtonFrame()
{
setTitle("RadioButtonTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter le libellé d’exemple de texte
label = new JLabel("The quick brown fox jumps over the lazy dog.");
label.setFont(new Font("Serif", Font.PLAIN,
DEFAULT_SIZE));
add(label, BorderLayout.CENTER);
// ajouter les boutons radio
buttonPanel = new JPanel();
group = new ButtonGroup();
addRadioButton("Small", 8);
addRadioButton("Medium", 12);
addRadioButton("Large", 18);
addRadioButton("Extra large", 36);
add(buttonPanel, BorderLayout.SOUTH);
}
/**
Ajoute un bouton radio qui définit la taille de police
de l’exemple de texte.
@param name La chaîne de libellé du bouton
@param size La taille de police que définit ce bouton
*/
public void addRadioButton(String name, final int size)
{
boolean selected = size == DEFAULT_SIZE;
JRadioButton button = new JRadioButton(name, selected);
group.add(button);
buttonPanel.add(button);
// Cet écouteur définit la taille de police de l’exemple
ActionListener listener = new
ActionListener()
Livre Java .book Page 437 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
437
{
public void actionPerformed(ActionEvent event)
{
// size fait référence au paramètre final de
// la méthode addRadioButton
label.setFont(new Font("Serif", Font.PLAIN,
size));
}
};
button.addActionListener(listener);
}
public static final int DEFAULT_WIDTH = 400;
public static final int DEFAULT_HEIGHT = 200;
private JPanel buttonPanel;
private ButtonGroup group;
private JLabel label;
private static final int DEFAULT_SIZE = 12;
}
javax.swing.JRadioButton 1.2
•
JRadioButton(String label, boolean state)
Construit un bouton radio avec le libellé donné et l’état initial.
•
JRadioButton(String label, Icon icon)
Construit un bouton radio avec le libellé donné et une icône qui est initialement désactivée..
javax.swing.ButtonGroup 1.2
•
void add(AbstractButton b)
Ajoute le bouton au groupe.
•
ButtonModel getSelection()
Renvoie le modèle du bouton sélectionné.
javax.swing.ButtonModel 1.2
•
String getActionCommand()
Renvoie la commande d’action du modèle du bouton.
javax.swing.AbstractButton 1.2
•
void setActionCommand(String s)
Définit la commande d’action pour le bouton et son modèle.
Bordures
Si vous présentez plusieurs groupes de boutons radio dans une fenêtre, vous devrez indiquer visuellement quels sont ceux qui appartiennent au même groupe. Swing fournit un ensemble de bordures
(ou cadres) qui sont utiles pour réaliser cet effet. Vous pouvez appliquer une bordure à n’importe
quel composant qui étend la classe JComponent. L’usage le plus courant est de placer une bordure
autour d’un panneau et de le remplir avec d’autres éléments d’interface tels que des boutons radio.
Livre Java .book Page 438 Jeudi, 25. novembre 2004 3:04 15
438
Au cœur de Java 2 - Notions fondamentales
Il y a plusieurs choix de bordures, mais leur emploi respecte la même procédure :
1. Appelez une méthode static de BorderFactory pour créer une bordure. Vous pouvez choisir
parmi les styles suivants (voir Figure 9.17) :
– LoweredBevel (relief incrusté) ;
– RaisedBevel (relief en saillie) ;
– Etched (gravée) ;
– Line (trait uniforme) ;
– Matte (avec trait variable ou texture) ;
– Empty (vide ; pour créer un espace autour du composant).
2. Vous pouvez ajouter un titre à votre bordure en la passant avec le titre à la méthode BorderFactory.createTitledBorder.
3. Il est aussi possible de combiner plusieurs bordures avec la méthode BorderFactory.createCompoundBorder.
4. Ajoutez la bordure résultante à votre composant au moyen de la méthode setBorder de la classe
JComponent.
Voici, par exemple, de quelle façon ajouter une bordure gravée avec un titre dans un panneau :
Border etched = BorderFactory.createEtchedBorder()
Border titled = BorderFactory.createTitledBorder(etched,
"A Title");
panel.setBorder(titled);
Exécutez le programme de l’Exemple 9.7 pour avoir un aperçu de l’apparence des diverses bordures
proposées.
Les bordures s’accompagnent d’options qui permettent d’en définir l’épaisseur et la couleur. Reportez-vous à la documentation API pour plus de renseignements à ce sujet. Les fans de bordures apprécieront la classe SoftBevelBorder qui permet de créer une bordure avec des angles arrondis ; une
bordure LineBorder peut aussi avoir des coins arrondis. Toutefois, leur création n’est possible qu’au
moyen d’un des constructeurs de la classe ; il n’existe pas de méthode BorderFactory pour ces
types.
Figure 9.17
Test des types
de bordures.
Exemple 9.7 : BorderTest.java
import java.awt.*;
import java.awt.event.*;
Livre Java .book Page 439 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
import javax.swing.*;
import javax.swing.border.*;
public class BorderTest
{
public static void main(String[] args)
{
BorderFrame frame = new BorderFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec des boutons radio pour choisir un style de bordure.
*/
class BorderFrame extends JFrame
{
public BorderFrame()
{
setTitle("BorderTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
demoPanel = new JPanel();
buttonPanel = new JPanel();
group = new ButtonGroup();
addRadioButton("Lowered bevel",
BorderFactory.createLoweredBevelBorder());
addRadioButton("Raised bevel",
BorderFactory.createRaisedBevelBorder());
addRadioButton("Etched",
BorderFactory.createEtchedBorder());
addRadioButton("Line",
BorderFactory.createLineBorder(Color.BLUE));
addRadioButton("Matte",
BorderFactory.createMatteBorder(
10, 10, 10, 10, Color.BLUE));
addRadioButton("Empty",
BorderFactory.createEmptyBorder());
Border etched = BorderFactory.createEtchedBorder();
Border titled = BorderFactory.createTitledBorder
(etched, "Border types");
buttonPanel.setBorder(titled);
setLayout(new GridLayout(2, 1));
add(buttonPanel);
add(demoPanel);
}
public void addRadioButton(String buttonName, final Border b)
{
JRadioButton button = new JRadioButton(buttonName);
button.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
439
Livre Java .book Page 440 Jeudi, 25. novembre 2004 3:04 15
440
Au cœur de Java 2 - Notions fondamentales
demoPanel.setBorder(b);
validate();
}
});
group.add(button);
buttonPanel.add(button);
}
public static final int DEFAULT_WIDTH = 600;
public static final int DEFAULT_HEIGHT = 200;
private JPanel demoPanel;
private JPanel buttonPanel;
private ButtonGroup group;
}
javax.swing.BorderFactory 1.2
•
•
static Border createLineBorder(Color color)
static Border createLineBorder(Color color, int thickness)
Créent une bordure avec un trait simple régulier.
•
static MatteBorder createMatteBorder(int top, int left, int bottom, int right,
Color color)
•
static MatteBorder createMatteBorder(int top, int left, int bottom, int right,
Icon tileIcon)
Créent une bordure épaisse remplie avec une couleur ou une répétition d’icônes (mosaïque).
•
•
static Border createEmptyBorder()
static Border createEmptyBorder(int top, int left, int bottom, int right)
Créent une bordure vide.
•
•
•
•
static Border createEtchedBorder()
static Border createEtchedBorder(Color highlight, Color shadow)
static Border createEtchedBorder(int type)
static Border createEtchedBorder(int type, Color highlight, Color shadow)
Créent une bordure avec un effet 3D.
Paramètres :
•
•
•
•
lumière, ombre
Couleurs pour un effet 3D.
type
L’un des types EtchedBorder.RAISED ou
EtchedBorder.LOWERED.
static Border createBevelBorder(int type)
static Border createBevelBorder(int type, Color light, Color shadow)
static Border createLoweredBevelBorder()
static Border createRaisedBevelBorder()
Créent une bordure qui donne l’effet d’une surface avec un relief incrusté ou en saillie.
Paramètres :
type
highlight,
shadow
L’un des types BevelBorder.LOWERED ou
BevelBorder.RAISED
Couleurs pour un effet 3D.
Livre Java .book Page 441 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
441
•
•
•
•
static TitledBorder createTitledBorder(String title)
•
static TitledBorder createTitledBorder(Border border, String title, int justification, int position, Font font)
•
static TitledBorder createTitledBorder(Border border, String title, int justification, int position, Font font, Color color)
static TitledBorder createTitledBorder(Border border)
static TitledBorder createTitledBorder(Border border, String title)
static TitledBorder createTitledBorder(Border border, String title, int justification, int position)
Créent une bordure de titre avec les propriétés spécifiées.
Paramètres :
•
title
La chaîne pour le titre.
border
La bordure à laquelle associer le titre.
justification
L’une des constantes TitleBorder.LEFT,
TitleBorder.CENTER, et TitleBorder.RIGHT.
position
L’une des constantes ABOVE_TOP, TOP, B
ELOW_TOP, ABOVE_BOTTOM, BOTTOM et BELOW_BOTTOM.
font
La police du titre.
color
La couleur du titre.
static CompoundBorder createCompoundBorder(Border outsideBorder, Border insideBorder)
Combine deux bordures pour en créer une nouvelle.
javax.swing.border.SoftBevelBorder 1.2
•
•
SoftBevelBorder(int type)
SoftBevelBorder(int type, Color highlight, Color shadow)
Créent une bordure en relief avec des angles arrondis.
Paramètres :
type
highlight,
shadow
L’un des types BevelBorder.LOWERED ou
BevelBorder.RAISED.
Couleurs pour un effet 3D.
javax.swing.border.LineBorder 1.2
•
public LineBorder(Color color, int thickness, boolean roundedCorners)
Crée une ligne de bordure avec l’épaisseur et la couleur indiquées. Si roundedCorners vaut
true, les coins sont arrondis.
javax.swing.JComponent 1.2
•
void setBorder(Border border)
Définit la bordure pour le composant.
Livre Java .book Page 442 Jeudi, 25. novembre 2004 3:04 15
442
Au cœur de Java 2 - Notions fondamentales
Listes déroulantes
Dès que le nombre d’options augmente, les boutons radio ne conviennent plus, car ils occupent trop
d’espace à l’écran. Vous pouvez, dans ce cas, utiliser une liste déroulante. Lorsque l’utilisateur
clique sur le champ, une liste de choix se déroule ; il peut ainsi faire une sélection (voir Figure 9.18).
Figure 9.18
Une liste déroulante.
Si la liste déroulante est configurée pour accepter une saisie (editable), il est alors possible de changer le choix actuel en tapant dans le champ, comme pour un champ de texte. La liste déroulante est
aussi appelée liste combinée ; elle allie la souplesse d’un champ de texte et une liste de choix prédéfinis. La classe JComboBox implémente ce composant.
Vous appelez la méthode setEditable pour que la liste puisse être modifiable. Notez que la saisie
ne modifie que l’élément courant et ne change pas le contenu de la liste.
Vous pouvez obtenir la sélection courante ou le texte tapé en appelant la méthode getSelectedItem.
Dans notre exemple de programme, l’utilisateur peut choisir un style de police à partir d’une liste
(Serif, SansSerif, Monospaced, etc.). Il peut aussi taper une autre police dans le champ de sélection.
Vous ajoutez les options avec la méthode addItem. Dans notre programme, elle n’est appelée que
dans le constructeur, mais vous pouvez l’appeler à tout moment :
faceCombo = new JComboBox();
faceCombo.setEditable(true);
faceCombo.addItem("Serif");
faceCombo.addItem("SansSerif");
. . .
Cette méthode ajoute la chaîne à la fin de la liste. Vous pouvez ajouter de nouvelles options à
n’importe quel endroit dans la liste avec la méthode insertItemAt :
faceCombo.insertItemAt("Monospaced", 0); // ajoute l’option au début
Vous pouvez ajouter des éléments de tout type — la liste déroulante invoque la méthode toString
de chaque élément pour l’afficher.
Livre Java .book Page 443 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
443
Si vous devez supprimer des options au cours de l’exécution, utilisez la méthode removeItem ou
removeItemAt, selon que vous spécifiez l’élément ou sa position :
faceCombo.removeItem("Monospaced");
faceCombo.removeItemAt(0); // supprimer le premier élément
Il existe aussi une méthode removeAllItems qui supprime toutes les options de la liste.
ASTUCE
Si vous devez ajouter un grand nombre d’éléments à une liste déroulante, la méthode addItem fonctionnera mal.
Construisez plutôt un DefaultComboBoxModel, remplissez-le en appelant addElement, puis appelez la méthode
setModel de la classe JComboBox.
Lorsque l’utilisateur sélectionne une option de la liste déroulante, un événement d’action est généré.
Pour trouver l’élément sélectionné, appelez getSource sur le paramètre event (événement) pour
obtenir une référence à la liste qui a envoyé l’événement. Appelez ensuite la méthode getSelectedItem pour récupérer l’option sélectionnée. Vous devez convertir la valeur renvoyée vers le type
approprié, généralement une chaîne (String).
public void actionPerformed(ActionEvent event)
{
label.setFont(new Font(
(String) faceCombo.getSelectedItem() ,
Font.PLAIN,
DEFAULT_SIZE));
}
L’Exemple 9.8 décrit le programme complet.
INFO
Pour afficher une liste en permanence au lieu d’utiliser une liste déroulante, faites appel au composant JList. Nous
traiterons de JList dans le Volume 2.
Exemple 9.8 : ComboBoxTest.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ComboBoxTest
{
public static void main(String[] args)
{
ComboBoxFrame frame = new ComboBoxFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec un label pour l’exemple de texte et une zone de liste
déroulante pour sélectionner les types de police.
Livre Java .book Page 444 Jeudi, 25. novembre 2004 3:04 15
444
Au cœur de Java 2 - Notions fondamentales
*/
class ComboBoxFrame extends JFrame
{
public ComboBoxFrame()
{
setTitle("ComboBoxTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// ajouter le libellé d’exemple de texte
label = new JLabel("The quick brown fox jumps over the lazy dog.");
label.setFont(new Font("Serif", Font.PLAIN,
DEFAULT_SIZE));
add(label, BorderLayout.CENTER);
// créer une liste déroulante et ajouter les types de polices
faceCombo = new JComboBox();
faceCombo.setEditable(true);
faceCombo.addItem("Serif");
faceCombo.addItem("SansSerif");
faceCombo.addItem("Monospaced");
faceCombo.addItem("Dialog");
faceCombo.addItem("DialogInput");
// L’écouteur de la liste déroulante remplace la police du
// libellé contenant l’exemple par celle sélectionnée
faceCombo.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
label.setFont(new Font(
(String)faceCombo.getSelectedItem(),
Font.PLAIN,
DEFAULT_SIZE));
}
});
// ajouter une liste déroulante à un panneau dans la
// partie sud du cadre
JPanel comboPanel = new JPanel();
comboPanel.add(faceCombo);
add(comboPanel, BorderLayout.SOUTH);
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
private JComboBox faceCombo;
private JLabel label;
private static final int DEFAULT_SIZE = 12;
}
Livre Java .book Page 445 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
445
javax.swing.JComboBox 1.2
•
void setEditable(boolean b)
Paramètres :
•
b
true si le champ de la liste déroulante est modifiable par
l’utilisateur, false sinon.
void addItem(Object item)
Ajoute un élément dans la liste.
•
void insertItemAt(Object item, int index)
Insère un élément dans la liste à la position donnée par l’indice.
•
void removeItem(Object item)
Supprime un élément de la liste.
•
void removeItemAt(int index)
Supprime l’élément dans la liste à la position donnée par l’indice.
•
void removeAllItems()
Supprime tous les éléments de la liste.
•
Object getSelectedItem()
Renvoie l’élément actuellement sélectionné.
Curseurs
Les zones de liste déroulantes permettent à l’utilisateur de choisir parmi un ensemble de valeurs. Les
curseurs proposent un choix exhaustif de valeurs, par exemple, n’importe quel nombre compris entre
1 et 100.
La méthode la plus courante pour construire un curseur est la suivante :
JSlider curseur = new JSlider(min, max, valeurInitiale);
Si vous omettez les valeurs minimum, maximum et initiale, elles sont respectivement initialisées à 0,
100 et 50.
Vous pouvez positionner le curseur verticalement, à l’aide de l’appel suivant au constructeur :
JSlider curseur = new JSlider(SwingConstants.VERTICAL,
min, max, valeurInitiale);
Ces constructeurs créent un curseur normal, comme celui se trouvant en haut de la Figure 9.19. Nous
allons vous indiquer comment y ajouter des fioritures.
Au fur et à mesure que l’utilisateur fait glisser le curseur, la valeur évolue entre les valeurs minimum
et maximum. Un événement ChangeEvent est alors envoyé à tous les écouteurs de changement.
Pour obtenir une notification du changement, vous devez appeler la méthode addChangeListener et
installer un objet qui implémente l’interface ChangeListener. Cette interface possède une seule
méthode, stateChanged, de laquelle vous devez extraire la valeur du curseur :
public void stateChanged(ChangeEvent event)
{
JSlider slider = (JSlider)event.getSource();
Livre Java .book Page 446 Jeudi, 25. novembre 2004 3:04 15
446
Au cœur de Java 2 - Notions fondamentales
int value = slider.getValue();
. . .
}
Figure 9.19
Différents types
de curseurs.
Vous pouvez améliorer le curseur en affichant des repères. Par exemple, dans notre programme
exemple, le deuxième curseur utilise la configuration suivante :
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
Le curseur affiche des grands repères toutes les 20 unités et des petits repères toutes les 5 unités. Les
repères font référence aux valeurs du curseur, et non à des pixels.
Les instructions ci-dessus ne font que définir les unités pour les repères. Pour les afficher, vous devez
appeler :
slider.setPaintTicks(true);
Les petites et grandes marques de repères sont indépendantes. Vous pouvez parfaitement définir les
grandes marques toutes les 20 unités et les petites toutes les 7 unités, mais vous risquez d’obtenir
une échelle assez déroutante.
Vous pouvez forcer le curseur à s’aligner sur les repères. Chaque fois que l’utilisateur relâche le
curseur dans ce mode de déplacement, il est positionné automatiquement sur le repère la plus
proche. Vous activez ce mode à l’aide de l’appel :
slider.setSnapToTicks(true);
INFO
Le mode d’alignement sur les repères ne fonctionne pas aussi bien que prévu. Tant que le curseur n’est pas réellement aligné, l’écouteur de changement fait toujours état de valeurs qui ne correspondent pas à des repères. Et si
vous cliquez ensuite sur le curseur — une action qui est censée faire bouger le curseur de la valeur d’un repère — le
curseur ne se déplace pas vers le repère suivant (ou précédent).
Livre Java .book Page 447 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
447
Vous pouvez demander l’affichage de valeurs de repères pour les grandes marques, en appelant :
slider.setPaintLabels(true);
Par exemple, pour un curseur allant de 0 à 100 ayant des grandes marques espacées de 20, les repères
seront libellés 0, 20, 40, 60, 80 et 100.
Vous pouvez aussi fournir d’autres libellés, comme des chaînes ou des icônes (voir Figure 9.19). Le
processus est un peu compliqué. Vous devez remplir une table de hachage avec des clés de type
Integer et des valeurs de type Component (l’autoboxing simplifie ceci à partir de la version 5.0
du JDK). Vous appelez ensuite la méthode setLabelTable. Les composants sont placés sous les
marques de repères. Vous avez généralement recours à des objets JLabel. Voici comment libeller
les marques avec A, B, C, D, E et F :
Hashtable<Integer, Component> labelTable =
new Hashtable<Integer, Component>();
labelTable.put(0, new JLabel("A"));
labelTable.put(20, new JLabel("B"));
. . .
labelTable.put(100, new JLabel("F"));
slider.setLabelTable(labelTable);
Reportez-vous au Chapitre 2 du Volume 2 pour plus d’informations au sujet des tables de hachage.
L’Exemple 9.9 montre aussi comment programmer un curseur avec des icônes comme libellés de
repères.
ASTUCE
Si vos marques ou libellés de repères ne s’affichent pas, vérifiez que vous avez bien appelé setPaintTicks(true)
et setPaintLabels(true).
Pour supprimer le "couloir" dans lequel se déplace le curseur, appelez
slider.setPaintTrack(false);
Le quatrième curseur de la Figure 9.19 ne présente pas de couloir.
Le remplissage du cinquième curseur est inversé par l’appel suivant :
slider.setInverted(true);
Le programme d’exemple montre comment obtenir tous ces effets visuels sur une série de curseurs.
Un écouteur d’événement de changement est installé pour chaque curseur ; il place la valeur actuelle
du curseur dans le champ de texte en bas du cadre.
Exemple 9.9 : SliderTest.java
import
import
import
import
import
java.awt.*;
java.awt.event.*;
java.util.*;
javax.swing.*;
javax.swing.event.*;
Livre Java .book Page 448 Jeudi, 25. novembre 2004 3:04 15
448
Au cœur de Java 2 - Notions fondamentales
public class SliderTest
{
public static void main(String[] args)
{
SliderTestFrame frame = new SliderTestFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec une série de curseurs et une zone de texte pour
afficher les valeurs du curseur.
*/
class SliderTestFrame extends JFrame
{
public SliderTestFrame()
{
setTitle("SliderTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
sliderPanel = new JPanel();
sliderPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
// écouteur commun à tous les curseurs
listener = new
ChangeListener()
{
public void stateChanged(ChangeEvent event)
{
// mettre à jour le champ de texte lorsque la
// valeur du curseur change
JSlider source = (JSlider) event.getSource();
textField.setText("" + source.getValue());
}
};
// ajouter un curseur normal
JSlider slider = new JSlider();
addSlider(slider, "Normal");
// ajouter un curseur avec des repères grands et petits
slider = new JSlider();
slider.setPaintTicks(true);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
addSlider(slider, "Ticks");
// ajouter un curseur qui s’aligne sur les repères
slider = new JSlider();
slider.setPaintTicks(true);
slider.setSnapToTicks(true);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
addSlider(slider, "Snap to ticks");
Livre Java .book Page 449 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
// ajouter un curseur sans couloir
slider = new JSlider();
slider.setPaintTicks(true);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
slider.setPaintTrack(false);
addSlider(slider, "No track");
// ajouter un curseur inversé
slider = new JSlider();
slider.setPaintTicks(true);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
slider.setInverted(true);
addSlider(slider, "Inverted");
// ajouter un curseur avec des libellés numériques
slider = new JSlider();
slider.setPaintTicks(true);
slider.setPaintLabels(true);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
addSlider(slider, "Labels");
// ajouter un curseur avec libellés alphabétiques
slider = new JSlider();
slider.setPaintLabels(true);
slider.setPaintTicks(true);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
Dictionary<Integer, Component> labelTable =
new Hashtable<Integer, Component>();
labelTable.put(0, new JLabel("A"));
labelTable.put(20, new JLabel("B"));
labelTable.put(40, new JLabel("C"));
labelTable.put(60, new JLabel("D"));
labelTable.put(80, new JLabel("E"));
labelTable.put(100, new JLabel("F"));
slider.setLabelTable(labelTable);
addSlider(slider, "Custom labels");
// ajouter un curseur avec des icônes comme libellés
slider = new JSlider();
slider.setPaintTicks(true);
slider.setPaintLabels(true);
slider.setSnapToTicks(true);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(20);
labelTable = new Hashtable<Integer, Component>();
449
Livre Java .book Page 450 Jeudi, 25. novembre 2004 3:04 15
450
Au cœur de Java 2 - Notions fondamentales
// ajoutés les images de cartes
labelTable.put(0,
new JLabel(new ImageIcon("nine.gif")));
labelTable.put(20,
new JLabel(new ImageIcon("ten.gif")));
labelTable.put(40,
new JLabel(new ImageIcon("jack.gif")));
labelTable.put(60,
new JLabel(new ImageIcon("queen.gif")));
labelTable.put(80,
new JLabel(new ImageIcon("king.gif")));
labelTable.put(100,
new JLabel(new ImageIcon("ace.gif")));
slider.setLabelTable(labelTable);
addSlider(slider, "Icon labels");
// ajouter le champ de texte affichant la valeur du curseur
textField = new JTextField();
add(sliderPanel, BorderLayout.CENTER);
add(textField, BorderLayout.SOUTH);
}
/**
Ajoute un curseur au panneau curseur et attache l’écouteur
@param s Le curseur
@param description La description du curseur
*/
public void addSlider(JSlider s, String description)
{
s.addChangeListener(listener);
JPanel panel = new JPanel();
panel.add(s);
panel.add(new JLabel(description));
sliderPanel.add(panel);
}
public static final int DEFAULT_WIDTH = 350;
public static final int DEFAULT_HEIGHT = 450;
private JPanel sliderPanel;
private JTextField textField;
private ChangeListener listener;
}
javax.swing.JSlider 1.2
•
•
JSlider()
JSlider(int direction)
Livre Java .book Page 451 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
•
•
•
Swing et les composants d’interface utilisateur
451
JSlider(int min, int max)
JSlider(int min, int max, int initialValue)
JSlider(int direction, int min, int max, int initialValue)
Construisent un curseur horizontal avec la direction et les valeurs minimum, maximum et initiale
données.
Paramètres :
•
direction
L’une des valeurs SwingConstants.VERTICAL ou
SwingConstants.HORIZONTAL. Par défaut horizontal.
min, max
Mini et maxi pour les valeurs de curseurs. Par défaut 0
et 100.
InitialValue
Valeur initiale du curseur. Par défaut 50.
void setPaintTicks(boolean b)
Affiche des repères si b vaut true.
•
•
void setMajorTickSpacing(int units)
void setMinorTickSpacing(int units)
Définissent les repères grands et petits comme multiples des unités de curseur données.
•
void setPaintLabels(boolean b)
Si b vaut true, les libellés des repères sont affichés.
•
void.setTable(Dictionary table)
Définit les composants à utiliser comme libellés des repères. Chaque paire clé/valeur dans la
table a la forme new Integer(valeur)/composant.
•
void setSnapToTicks(boolean b)
Si b vaut true, le curseur s’aligne sur le repère le plus proche après chaque déplacement.
•
void setPaintTrack(boolean b)
Si b vaut true, le curseur se déplace dans un couloir.
Le composant JSpinner
Un JSpinner est un champ de texte disposant de deux petits boutons. Lorsque l’utilisateur clique
dessus, la valeur du champ de texte est augmentée ou diminuée (voir Figure 9.20).
Figure 9.20
Variations du composant
Jspinner.
Livre Java .book Page 452 Jeudi, 25. novembre 2004 3:04 15
452
Au cœur de Java 2 - Notions fondamentales
Les valeurs du champ fléché peuvent être des nombres, des dates, des valeurs d’une liste ou, plus
généralement, une suite de valeurs dans laquelle il est possible de naviguer. La classe JSpinner définit des modèles de données standard pour les trois premiers cas. Vous pouvez définir votre propre
modèle de données et ainsi obtenir des suites arbitraires.
Par défaut, un champ fléché gère un entier que les boutons augmentent ou diminuent de 1. Vous
pouvez récupérer la valeur actuelle en appelant la méthode getValue. Cette méthode renvoie un
Object. Transtypez-le en un Integer et récupérez la valeur enveloppée :
JSpinner defaultSpinner = new JSpinner();
. . .
int value = (Integer) defaultSpinner.getValue();
Vous pouvez modifier l’incrémentation pour qu’elle soit différente de 1 et fournir des limites inférieures et supérieures. Voici un champ fléché commençant à 5 dont les valeurs sont comprises entre
0 et 10 et augmentent par incrémentations de 0,5 :
JSpinner boundedSpinner = new JSpinner(
new SpinnerNumberModel(5, 0, 10, 0.5));
Il existe deux constructeurs SpinnerNumberModel, l’un avec le seul paramètre int et l’autre avec
des paramètres double. Si l’un des paramètres est un nombre à virgule flottante, on utilise le
deuxième constructeur : il définit la valeur du bouton fléché sur un objet Double.
Les boutons fléchés ne sont pas limités à des valeurs numériques. Vous pouvez amener un bouton
fléché à faire défiler toute suite de valeurs. Transférez simplement un SpinnerListModel au
constructeur JSpinner. Pour construire un SpinnerListModel, utilisez un tableau ou une classe
implémentant l’interface List (comme un ArrayList). Dans notre exemple, nous affichons un
bouton fléché faisant défiler tous les noms de police disponibles :
String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().
getAvailableFontFamilyNames();
JSpinner listSpinner = new JSpinner(new SpinnerListModel(fonts));
Nous avons toutefois considéré que la direction de l’itération était assez peu claire ; c’est en effet
l’opposé de ce que l’on a l’habitude de voir dans une liste déroulante. Les valeurs supérieures viennent généralement après les valeurs inférieures, la flèche descendante permettant alors de naviguer
vers les valeurs supérieures. Mais le bouton fléché augmente l’indice du tableau de sorte que la
flèche ascendante produise des valeurs supérieures. Il n’existe pas de manière d’inverser l’ordre du
SpinnerListModel ; toutefois, une sous-classe anonyme produit le résultat souhaité :
JSpinner reverseListSpinner = new JSpinner(
new SpinnerListModel(fonts)
{
public Object getNextValue()
{
return super.getPreviousValue();
}
public Object getPreviousValue()
{
return super.getNextValue();
}
});
Testez les deux versions et voyez celle qui vous paraît la plus intuitive.
Livre Java .book Page 453 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
453
Les boutons fléchés peuvent aussi être utiles pour augmenter ou diminuer une date. Vous obtenez un
bouton fléché initialisé avec la date du jour grâce à l’appel
JSpinner dateSpinner = new JSpinner(new SpinnerDateModel());
Cependant, si vous étudiez soigneusement la Figure 9.20, vous verrez que le texte du bouton fléché
montre à la fois la date et l’heure, par exemple
3/12/02 7:23 PM
L’heure est inutile dans un choix de date. Il se révèle pourtant assez difficile de créer ce bouton
n’affichant que la date. Voici l’incantation magique :
JSpinner betterDateSpinner = new JSpinner(new SpinnerDateModel());
String pattern = ((SimpleDateFormat)
DateFormat.getDateInstance()).toPattern();
betterDateSpinner.setEditor(new JSpinner.DateEditor(betterDateSpinner,
pattern));
Ce procédé permet aussi de créer un système pour connaître l’heure. Utilisez le constructeur SpinnerDateModel pour spécifier une Date, les limites inférieures et supérieures (ou null s’il n’existe
pas de limite) et le champ Calendar (comme Calendar.HOUR) à modifier :
JSpinner timeSpinner = new JSpinner(
new SpinnerDateModel(
new GregorianCalendar(2000, Calendar.JANUARY, 1, 12, 0,
0).getTime(),
null, null, Calendar.HOUR));
Toutefois, si vous souhaitez mettre à jour les minutes par incréments de 15, vous allez au-delà des
capacités de la classe SpinnerDateModel standard.
Vous pouvez afficher des suites arbitraires dans un champ fléché en définissant votre propre modèle.
Dans notre exemple de programme, nous disposons d’un champ fléché qui procède à une itération
sur toutes les permutations de la chaîne "meat" (viande, en anglais). Vous pouvez accéder à "mate",
"meta", "team" et à vingt autres permutations en cliquant sur les boutons.
Lorsque vous définissez votre propre modèle, étendez la classe AbstractSpinnerModel et définissez
les quatre méthodes suivantes :
Object getValue()
void setValue(Object value)
Object getNextValue()
Object getPreviousValue()
La méthode getValue renvoie la valeur stockée par le modèle. La méthode setValue définit une
nouvelle valeur. Elle lancera une exception IllegalArgumentException si la nouvelle valeur ne
convient pas.
ATTENTION
La méthode setValue doit appeler la méthode fireStateChanged après avoir défini la nouvelle valeur, faute de
quoi le champ fléché ne sera pas mis à jour.
Livre Java .book Page 454 Jeudi, 25. novembre 2004 3:04 15
454
Au cœur de Java 2 - Notions fondamentales
Les méthodes getNextValue et getPreviousValue renvoient les valeurs qui doivent venir après ou
avant la valeur courante ou null si la fin de l’itération est atteinte.
ATTENTION
Les valeurs getNextValue et getPreviousValue ne doivent pas modifier la valeur courante. Cliquer sur la flèche
ascendante du bouton appelle la méthode getNextValue. Si la valeur de retour n’est pas null, elle est définie par
un appel à setValue.
Dans le programme d’exemple, nous utilisons un algorithme standard pour déterminer les permutations
suivantes et précédentes. Ne vous préoccupez pas des détails de l’algorithme.
L’Exemple 9.10 montre comment générer les divers types de boutons fléchés. Cliquez sur le bouton
OK pour voir les valeurs du bouton.
Exemple 9.10 : SpinnerTest.java
import
import
import
import
import
java.awt.*;
java.awt.event.*;
java.text.*;
java.util.*;
javax.swing.*;
/**
Un programme qui teste les champs fléchés.
*/
public class SpinnerTest
{
public static void main(String[] args)
{
SpinnerFrame frame = new SpinnerFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
/**
Un cadre avec un panneau contenant plusieurs champs fléchés
et un bouton qui affiche les valeurs du champ.
*/
class SpinnerFrame extends JFrame
{
public SpinnerFrame()
{
setTitle("SpinnerTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
JPanel buttonPanel = new JPanel();
okButton = new JButton("Ok");
buttonPanel.add(okButton);
add(buttonPanel, BorderLayout.SOUTH);
mainPanel = new JPanel();
mainPanel.setLayout(new GridLayout(0, 3));
add(mainPanel, BorderLayout.CENTER);
Livre Java .book Page 455 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
455
JSpinner defaultSpinner = new JSpinner();
addRow("Default", defaultSpinner);
JSpinner boundedSpinner = new JSpinner(
new SpinnerNumberModel(5, 0, 10, 0.5));
addRow("Bounded", boundedSpinner);
String[] fonts = GraphicsEnvironment
.getLocalGraphicsEnvironment()
.getAvailableFontFamilyNames();
JSpinner listSpinner = new JSpinner(new SpinnerListModel(fonts));
addRow("List", listSpinner);
JSpinner reverseListSpinner = new JSpinner(
new
SpinnerListModel(fonts)
{
public Object getNextValue()
{
return super.getPreviousValue();
}
public Object getPreviousValue()
{
return super.getNextValue();
}
});
addRow("Reverse List", reverseListSpinner);
JSpinner dateSpinner = new JSpinner(new SpinnerDateModel());
addRow("Date", dateSpinner);
JSpinner betterDateSpinner = new JSpinner(new SpinnerDateModel());
String pattern = ((SimpleDateFormat) DateFormat.getDateInstance()).
➥toPattern();
betterDateSpinner.setEditor(new JSpinner.DateEditor(betterDateSpinner,
➥pattern));
addRow("Better Date", betterDateSpinner);
JSpinner timeSpinner = new JSpinner(
new SpinnerDateModel(
new GregorianCalendar(2000, Calendar.JANUARY, 1, 12, 0,
➥0).getTime(),
null, null, Calendar.HOUR));
addRow("Time", timeSpinner);
JSpinner permSpinner = new JSpinner(new PermutationSpinnerModel
➥("meat"));
addRow("Word permutations", permSpinner);
}
/**
Ajoute une ligne au panneau principal.
@param labelText Le libellé du champ fléché
@param spinner L’exemple de champ fléché
Livre Java .book Page 456 Jeudi, 25. novembre 2004 3:04 15
456
Au cœur de Java 2 - Notions fondamentales
*/
public void addRow(String labelText, final JSpinner spinner)
{
mainPanel.add(new JLabel(labelText));
mainPanel.add(spinner);
final JLabel valueLabel = new JLabel();
mainPanel.add(valueLabel);
okButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
Object value = spinner.getValue();
valueLabel.setText(value.toString());
}
});
}
public static final int DEFAULT_WIDTH = 400;
public static final int DEFAULT_HEIGHT = 250;
private JPanel mainPanel;
private JButton okButton;
}
/**
Un modèle qui génère dynamiquement des permutations de mots
*/
class PermutationSpinnerModel extends AbstractSpinnerModel
{
/**
Construit le modèle.
@param w Le mot à permuter
*/
public PermutationSpinnerModel(String w)
{
word = w;
}
public Object getValue()
{
return word;
}
public void setValue(Object value)
{
if (!(value instanceof String))
throw new IllegalArgumentException();
word = (String) value;
fireStateChanged();
}
public Object getNextValue()
{
int[] codePoints = toCodePointArray(word);
for (int i = codePoints.length - 1; i > 0; i--)
{
if (codePoints[i - 1] < codePoints[i])
Livre Java .book Page 457 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
{
int j = codePoints.length - 1;
while (codePoints[i - 1] > codePoints[j]) j--;
swap(codePoints, i - 1, j);
reverse(codePoints, i, codePoints.length - 1);
return new String(codePoints, 0, codePoints.length);
}
}
reverse(codePoints, 0, codePoints.length - 1);
return new String(codePoints, 0, codePoints.length);
}
public Object getPreviousValue()
{
int[] codePoints = toCodePointArray(word);
for (int i = codePoints.length - 1; i > 0; i--)
{
if (codePoints[i - 1] > codePoints[i])
{
int j = codePoints.length - 1;
while (codePoints[i - 1] < codePoints[j]) j--;
swap(codePoints, i - 1, j);
reverse(codePoints, i, codePoints.length - 1);
return new String(codePoints, 0, codePoints.length);
}
}
reverse(codePoints, 0, codePoints.length - 1);
return new String(codePoints, 0, codePoints.length);
}
private static int[] toCodePointArray(String str)
{
int[] codePoints = new int[str.codePointCount(0, str.length())];
for (int i = 0, j = 0; i < str.length(); i++, j++)
{
int cp = str.codePointAt(i);
if (Character.isSupplementaryCodePoint(cp)) i++;
codePoints[j] = cp;
}
return codePoints;
}
private static void swap(int[] a, int i, int j)
{
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
private static void reverse(int[] a, int i, int j)
{
while (i < j) { swap(a, i, j); i++; j--; }
}
private String word;
}
457
Livre Java .book Page 458 Jeudi, 25. novembre 2004 3:04 15
458
Au cœur de Java 2 - Notions fondamentales
javax.swing.JSpinner 1.4
•
JSpinner()
Construit un champ fléché qui modifie un entier avec une valeur de départ de 0, une incrémentation
de 1 et pas de limite.
•
JSpinner(SpinnerModel model)
Construit un champ fléché qui utilise le modèle de données indiqué.
•
Object getValue()
Récupère la valeur actuelle du champ fléché.
•
void setValue(Object value)
Tente de définir la valeur du champ fléché. Déclenche une exception IllegalArgumentException
si le modèle n’accepte pas la valeur.
•
void setEditor(JComponent editor)
Définit le composant utilisé pour modifier la valeur du bouton fléché.
javax.swing.SpinnerNumberModel 1.4
•
•
SpinnerNumberModel(int initval, int minimum, int maximum, int stepSize)
SpinnerNumberModel(double initval, double minimum, double maximum, double stepSize)
Ces constructeurs produisent des modèles de nombre qui gèrent une valeur Integer ou Double.
Utilisez les constantes MIN_VALUE et MAX_VALUE des classes Integer et Double pour les valeurs
sans limites.
Paramètres :
initval
La valeur initiale.
minimum
La valeur valide minimale.
maximum
La valeur valide maximale.
stepSize
L’incrémentation ou la décrémentation de chaque intervalle.
javax.swing.SpinnerListModel 1.4
•
•
SpinnerListModel(Object[] values)
SpinnerListModel(List values)
Ces constructeurs produisent des modèles qui sélectionnent une valeur parmi les valeurs
données.
javax.swing.SpinnerDateModel 1.4
•
SpinnerDateModel()
Construit un modèle de date avec la date du jour comme valeur initiale, pas de limite inférieure
ou supérieure et une incrémentation de Calendar.DAY_OF_MONTH.
•
SpinnerDateModel(Date initval, Comparable minimum, Comparable
step)
Paramètres :
maximum, int
initval
La valeur initiale.
minimum
La valeur valide minimale ou null si aucune limite
inférieure n’est souhaitée.
Livre Java .book Page 459 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
459
maximum
La valeur valide maximale ou null si aucune limite
supérieure n’est souhaitée.
step
Le champ de date à incrémenter ou à décrémenter de chaque
intervalle. L’une des constantes ERA, YEAR, MONTH,
WEEK_OF_YEAR, WEEK_OF_MONTH, DAY_OF_MONTH,
DAY_OF_YEAR, DAY_OF_WEEK, DAY_OF_WEEK_IN_MONTH,
AM_PM, HOUR, HOUR_OF_DAY, MINUTE, SECOND ou
MILLISECOND de la classe Calendar.
java.text.SimpleDateFormat 1.1
•
String toPattern() 1.2
Récupère le motif de modification pour ce formateur de date. On utilise souvent "jj-MMAAAA". Voyez la documentation JDK pour en savoir plus sur le motif.
javax.swing.JSpinner.DateEditor 1.4
•
DateEditor(JSpinner spinner, String pattern)
Construit un éditeur de date pour un champ fléché.
Paramètres :
spinner
Le champ fléché auquel appartient cet éditeur.
pattern
Le motif du format pour le SimpleDateFormat associé.
javax.swing.AbstractSpinnerModel 1.4
•
Object getValue()
Récupère la valeur actuelle du modèle.
•
void setValue(Object value)
Tente de définir une nouvelle valeur pour le modèle. Lance une exception IllegalArgumentException si la valeur n’est pas acceptable. Lors du remplacement de cette méthode, vous devez
appeler fireStateChanged après avoir défini la nouvelle valeur.
•
•
Object getNextValue()
Object getPreviousValue()
Ces méthodes calculent (mais ne définissent pas) la valeur suivante ou précédente dans la suite
définie par ce modèle.
Menus
Ce chapitre a débuté par une introduction aux composants les plus connus pouvant être placés dans
une fenêtre : les différents types de boutons, les champs de texte et les listes déroulantes. Swing gère
aussi un autre type d’élément d’interface, les menus déroulants, utilisés largement dans les interfaces
GUI.
Livre Java .book Page 460 Jeudi, 25. novembre 2004 3:04 15
460
Au cœur de Java 2 - Notions fondamentales
Une barre de menus située au sommet de la fenêtre contient les noms des menus déroulants. Il suffit
de cliquer dessus pour les ouvrir, ce qui fait apparaître des options de menu et de sous-menus. Lorsque l’utilisateur clique sur une option de menu, tous les menus sont fermés et un message est envoyé
au programme. La Figure 9.21 présente un menu classique comprenant un sous-menu.
Figure 9.21
Un menu contenant
un sous-menu.
Création d’un menu
La création de menus est une opération assez simple. On commence par créer une barre de menus :
JMenuBar menuBar = new JMenuBar();
Une barre de menus est un simple composant que vous pouvez ajouter n’importe où. Le plus
souvent, elle est placée au sommet d’un cadre. Pour cela, la méthode setJMenuBar est appelée :
frame.setJMenuBar(menuBar);
Pour chaque menu, vous devez créer un objet menu :
JMenu editMenu = new JMenu("Edit");
Vous ajoutez les menus principaux à la barre de menus :
menuBar.add(editMenu);
Vous ajoutez ensuite à l’objet menu les options du menu, les séparateurs et les sous-menus :
JMenuItem pasteItem = new JMenuItem("Paste");
editMenu.add(pasteItem);
editMenu.addSeparator();
JMenu optionsMenu = . . .; // un sous-menu
editMenu.add(optionsMenu);
Vous pouvez voir les séparateurs à la Figure 9.21 sous les éléments "Paste" et "Read-only".
Lorsque l’utilisateur sélectionne un menu, un événement d’action est déclenché. Vous devez installer
un écouteur d’action pour chaque option de menu :
ActionListener listener = . . .;
pasteItem.addActionListener(listener);
Il existe une méthode, JMenu.add(String s), qui ajoute une option à la fin d’un menu, par exemple :
editMenu.add("Paste");
Livre Java .book Page 461 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
461
La méthode add renvoie l’option de menu créée, afin que vous puissiez la capturer et lui associer un
écouteur, de la façon suivante :
JMenuItem pasteItem = editMenu.add("Paste");
pasteItem.addActionListener(listener);
Il arrive fréquemment que des options de menu lancent des commandes qui peuvent aussi être activées par le biais d’autres éléments de l’interface utilisateur, comme les boutons de la barre d’outils.
Au Chapitre 8, vous avez vu comment spécifier des commandes par l’intermédiaire des objets
Action. Vous définissez une classe qui implémente l’interface Action, généralement par dérivation
de la classe AbstractAction. Vous spécifiez le libellé de l’option de menu dans le constructeur de
l’objet AbstractAction, puis vous surchargez la méthode actionPerformed pour qu’elle
contienne le gestionnaire d’action du menu. Par exemple :
Action exitAction = new
AbstractAction("Exit") // texte de l’option de menu ici
{
public void actionPerformed(ActionEvent event)
{
// code de l’action ici
System.exit(0);
}
};
Vous pouvez alors ajouter l’action au menu :
JMenuItem exitItem = fileMenu.add(exitAction);
Cette commande ajoute une option au menu, à l’aide du nom de l’action. L’objet action devient son
écouteur. C’est un raccourci pratique pour :
JMenuItem exitItem = new JMenuItem(exitAction);
fileMenu.add(exitItem);
INFO
Sous Windows et Macintosh, les menus sont généralement définis dans un fichier de ressources externes, qui est lié
aux applications par l’intermédiaire des identifiants de ressources. Il est possible de créer des menus par programmation, mais cela arrive rarement. Dans Java, les menus sont habituellement conçus au sein des programmes, car le
mécanisme de gestion des ressources externes est beaucoup plus limité que ceux de Windows ou de Mac OS.
javax.swing.JMenu 1.2
•
JMenu(String label)
Construit un menu.
Paramètres :
•
label
L’intitulé du menu dans la barre de menus ou le menu parent.
JMenuItem add(JMenuItem option)
Ajoute une option de menu (ou un menu).
Paramètres :
•
option
L’option ou le menu à ajouter.
JMenuItem add(String label)
Ajoute une option au menu.
Paramètres :
label
L’intitulé de l’option de menu.
Livre Java .book Page 462 Jeudi, 25. novembre 2004 3:04 15
462
•
Au cœur de Java 2 - Notions fondamentales
JMenuItem add(Action a)
Ajoute une option de menu et lui associe une action.
Paramètres :
•
a
Une action encapsulant un nom, une icône optionnelle et un
écouteur (voir Chapitre 8).
void addSeparator()
Ajoute une ligne de séparation dans le menu.
•
JMenuItem insert(JMenuItem menu, int index)
Ajoute une nouvelle option (ou sous-menu) dans le menu à la position donnée par l’indice.
Paramètres :
•
menu
Le menu à ajouter.
index
Emplacement où ajouter l’option.
JMenuItem insert(Action a, int index)
Ajoute une nouvelle option au menu à la position donnée par l’indice et y associe une action.
Paramètres :
•
a
Une action encapsulant un nom, une icône facultative et un
écouteur.
index
Emplacement où ajouter l’option.
void insertSeparator(int index)
Ajoute un séparateur à la position donnée par l’indice.
Paramètres :
•
index
Emplacement où ajouter le séparateur.
void remove(int index)
Supprime une option spécifique du menu à la position donnée par l’indice.
Paramètres :
•
index
La position de l’option à supprimer.
void remove(JMenuItem option)
Supprime une option spécifique du menu.
Paramètres :
option
L’option à supprimer.
javax.swing.JMenuItem 1.2
•
JMenuItem(String label)
Construit un élément de menu avec un intitulé donné.
•
JMenuItem(Action a) 1.3
Construit un élément de menu pour l’action donnée.
Paramètres :
a
Une action encapsulant un nom, une icône facultative et un
écouteur.
javax.swing.AbstractButton 1.2
•
void setAction(Action a) 1.3
Définit l’action pour ce bouton ou élément de menu.
Paramètres :
a
Une action encapsulant un nom, une icône facultative
et un écouteur.
Livre Java .book Page 463 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
463
javax.swing.JFrame 1.2
•
void setJMenuBar(JMenuBar menubar)
Définit la barre de menus de la fenêtre.
Icônes et options de menu
Les options de menu sont très semblables aux boutons. En réalité, la classe JMenuItem étend la
classe AbstractButton. Comme les boutons, les menus peuvent contenir un libellé, une icône, ou
les deux. Vous pouvez spécifier une icône à l’aide des constructeurs JMenuItem (String, Icon)
ou JMenuItem (Icon), ou la définir à l’aide de la méthode setIcon dont la classe JMenuItem hérite
de la classe AbstractButton. Par exemple :
JMenuItem cutItem = new JMenuItem("Cut", new ImageIcon("cut.gif"));
La Figure 9.22 présente un menu dont certaines options comprennent des icônes. Le texte des
options est placé par défaut à droite de l’icône. Si vous préférez que le texte soit placé à gauche,
appelez la méthode setHorizontalTextPosition dont la classe JMenuItem hérite de la classe
AbstractButton. Par exemple, l’appel
cutItem.setHorizontalTextPosition(SwingConstants.LEFT);
déplace le texte de l’option de menu à gauche de l’icône.
Vous pouvez aussi ajouter une icône à une action :
cutAction.putValue(Action.SMALL_ICON, new ImageIcon("cut.gif"));
Lorsque vous construisez une option de menu à partir d’une action, la valeur Action.NAME devient
le texte de l’option de menu, et la valeur Action.SMALL_ICON devient l’icône.
Figure 9.22
Icônes associées à
des options de menu.
Vous pouvez aussi définir l’icône dans le constructeur de AbstractAction :
cutAction = new
AbstractAction("Cut", new ImageIcon("cut.gif"))
{
public void actionPerformed(ActionEvent event)
{
// le code de l’action ici
}
};
Livre Java .book Page 464 Jeudi, 25. novembre 2004 3:04 15
464
Au cœur de Java 2 - Notions fondamentales
javax.swing.JMenuItem 1.2
•
JMenuItem(String label, Icon icon)
Construit un élément de menu avec l’intitulé donné et une icône.
javax.swing.AbstractButton 1.2
•
void setHorizontalTextPosition(int position)
Définit la position horizontale du texte par rapport à l’icône.
Paramètres :
ou
pos
SwingConstants.RIGHT (texte placé à droite de l’icône),
SwingConstants.LEFT (texte placé à gauche).
javax.swing.AbstractAction 1.2
•
AbstractAction(String name, Icon smallIcon)
Construit une action abstraite avec le nom donné et une icône.
Options de menu avec cases à cocher et boutons radio
Il est possible d’ajouter des cases à cocher ou des boutons radio aux options de menu (voir
Figure 9.23). Lorsque l’utilisateur sélectionne l’option, elle passe à l’état activé ou désactivé, selon
son état initial.
Figure 9.23
Une option de menu
cochée et des options
avec boutons radio.
<
Excepté leur aspect, vous traitez ces options de la même manière que toutes les autres. Voici, par
exemple, comment créer une option avec une case à cocher :
JCheckBoxMenuItem readonlyItem
= new JCheckBoxMenuItem("Read-only");
optionsMenu.add(readonlyItem);
Les boutons radio des options de menu fonctionnent de façon standard. Vous devez les inclure dans
un groupe de boutons. Quand l’un de ces boutons est choisi, tous les autres sont automatiquement
désactivés :
ButtonGroup group = new ButtonGroup();
JRadioButtonMenuItem insertItem
= new JRadioButtonMenuItem("Insert");
Livre Java .book Page 465 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
465
insertItem.setSelected(true);
JRadioButtonMenuItem overtypeItem = new JRadioButtonMenuItem("Overtype");
group.add(insertItem);
group.add(overtypeItem);
optionsMenu.add(insertItem);
optionsMenu.add(overtypeItem);
Lorsque vous utilisez ce type d’option, vous n’avez généralement pas besoin d’être informé du
moment exact où l’utilisateur effectue sa sélection. Vous pouvez simplement employer la méthode
isSelected pour tester l’état actuel d’une option (cela implique bien sûr que vous conserviez une
référence sur l’option de menu stockée dans un champ d’instance). Utilisez la méthode setSelected
pour définir l’état d’une option.
javax.swing.JCheckBoxMenuItem 1.2
•
JCheckBoxMenuItem(String label)
Crée une option de menu avec case à cocher et l’intitulé spécifié.
•
JCheckBoxMenuItem(String label, boolean state)
Crée une option de menu avec case à cocher avec l’intitulé et l’état initial spécifiés (true signifie
cochée).
javax.swing.JRadioButtonMenuItem 1.2
•
JRadioButtonMenuItem(String label)
Crée une option de menu avec bouton radio et l’intitulé spécifié.
•
JRadioButtonMenuItem(String label, boolean state)
Crée une option de menu avec bouton radio avec l’intitulé et l’état initial spécifiés (true signifie
activée).
javax.swing.AbstractButton 1.2
•
boolean isSelected()
Renvoie l’état d’une option (true signifie activée).
•
void setSelected(boolean state)
Définit l’état d’une option.
Menus contextuels
Un menu contextuel est un menu qui n’est pas attaché à une barre de menus, mais qui flotte à l’intérieur
d’une fenêtre (voir Figure 9.24).
Figure 9.24
Un menu contextuel.
Livre Java .book Page 466 Jeudi, 25. novembre 2004 3:04 15
466
Au cœur de Java 2 - Notions fondamentales
Vous créez un menu contextuel comme n’importe quel autre menu, sachant qu’il ne possède pas de
titre :
JPopupMenu popup = new JPopupMenu();
Vous ajoutez ensuite les options comme à l’accoutumée :
JMenuItem item = new JMenuItem("Cut");
item.addActionListener(listener);
popup.add(item);
Contrairement à la barre de menus standard qui apparaît toujours au sommet d’un cadre, un menu
contextuel doit être explicitement affiché à l’aide de la méthode show. Vous devez spécifier le
composant parent et la position du menu contextuel en fonction du système de coordonnées du
parent. Par exemple :
popup.show(panel, x, y);
Le code est habituellement écrit pour que le menu surgisse au moment où l’utilisateur clique sur un
bouton particulier de la souris. Sous Windows et Linux, il s’agit souvent du bouton droit. Pour faire
apparaître un menu lorsque l’utilisateur clique sur un composant, appelez simplement la méthode :
component.setComponentPopupMenu(popup);
Très occasionnellement, vous pouvez placer un composant dans un autre qui dispose d’un menu
contextuel. Le composant enfant peut hériter du menu contextuel du composant parent en appelant
child.setInheritsPopupMenu(true);
Ces méthodes ont été ajoutées au JDK 5.0 pour isoler les programmeurs des dépendances du
système avec les menus déroulants. Avant le JDK 5.0, vous deviez installer un écouteur de souris et
ajouter le code suivant sur les méthodes d’écouteur mousePressed et mouseReleased :
if (popup.isPopupTrigger(event))
popup.show(event.getComponent(), event.getX(), event.getY());
Certains systèmes déclenchent des menus déroulants lorsque la souris descend, d’autres, lorsque la
souris remonte.
javax.swing.JPopupMenu 1.2
•
void show(Component c, int x, int y)
Affiche le menu contextuel.
Paramètres :
•
c
Le composant sur lequel le menu contextuel doit apparaître.
x, y
Les coordonnées (dans l’espace de coordonnées de c)
du coin supérieur gauche du menu contextuel.
boolean isPopupTrigger(MouseEvent event) 1.3
Renvoie true si l’événement de la souris est le déclencheur du menu contextuel.
java.awt.event.MouseEvent 1.1
•
boolean isPopupTrigger()
Renvoie true si l’événement de la souris est le déclencheur du menu contextuel.
javax.swing.JComponent 1.2
•
void setComponentPopupMenu(JPopupMenu popup) 5.0
Livre Java .book Page 467 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
•
Swing et les composants d’interface utilisateur
467
JPopup getComponentPopupMenu() 5.0
Définissent ou récupèrent le menu contextuel pour ce composant.
•
•
void setInheritsPopupMenu(boolean b) 5.0
boolean getInheritsPopupMenu() 5.0
Définissent ou récupèrent la propriété inheritsPopupMenu. Si la propriété est définie et que le
menu contextuel de ce composant soit null, elle utilise le menu contextuel de son parent.
Caractères mnémoniques et raccourcis clavier
Les utilisateurs expérimentés sélectionnent souvent une option de menu par le biais d’un caractère
mnémonique. Vous pouvez définir un caractère mnémonique en le spécifiant dans le constructeur de
l’option de menu :
JMenuItem indexItem = new JMenuItem("Index", ’I’);
Il apparaît automatiquement sous forme d’un soulignement de la lettre concernée dans le nom de
l’option (voir Figure 9.25). Dans l’exemple ci-dessus, il s’agit de la lettre I de l’option Index.
Quand le menu est déroulé, il suffit à l’utilisateur d’appuyer sur la touche I pour sélectionner
l’option (lorsque la lettre utilisée ne fait pas partie du libellé, elle n’apparaît pas dans le menu, mais
son activation permet néanmoins de sélectionner l’option. On peut bien entendu douter de l’utilité de
caractères mnémoniques invisibles).
Figure 9.25
Caractères
mnémoniques.
Parfois, vous ne souhaiterez pas souligner la première lettre de l’élément de menu correspondant au
caractère mnémonique. Si vous avez, par exemple, un "E" mnémonique pour le menu "Enregistrer
sous", vous pourriez souligner le deuxième "E". Depuis le JDK 1.4, vous pouvez spécifier le caractère qui sera souligné en appelant la méthode setDisplayedMnemonicIndex.
Si vous avez un objet Action, vous pouvez ajouter le caractère mnémonique comme valeur de la
touche Action.MNEMONIC_KEY, de la façon suivante :
indexAction.putValue(Index.MNEMONIC_KEY, new Integer(’I’));
Vous ne pouvez spécifier de caractère mnémonique que dans le constructeur d’une option de menu,
et non dans le constructeur d’un menu. Pour associer un caractère mnémonique à un menu, vous
devez appeler la méthode setMnemonic :
JMenu helpMenu = new JMenu("Help");
helpMenu.setMnemonic(’H’);
Livre Java .book Page 468 Jeudi, 25. novembre 2004 3:04 15
468
Au cœur de Java 2 - Notions fondamentales
Pour sélectionner le nom d’un menu principal dans la barre de menus, il faut maintenir enfoncée la
touche Alt puis appuyer sur la touche mnémonique. Par exemple, si vous appuyez sur Alt-H, le menu
Help (Aide) sera sélectionné.
Les touches mnémoniques permettent de sélectionner une option ou un sous-menu à partir d’un
menu déjà ouvert. Les combinaisons de touches ou accélérateurs sont des raccourcis clavier qui
permettent de sélectionner des options sans avoir à ouvrir le menu. Par exemple, de nombreux
programmes associent les combinaisons Ctrl+O et Ctrl+S aux options Ouvrir et Enregistrer du menu
Fichier. Pour lier une combinaison de touches à une option, utilisez la méthode setAccelerator,
qui reçoit un objet de type Keystroke. L’appel suivant associe la combinaison Ctrl+O à l’option
openItem (Ouvrir) :
openItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O,
InputEvent.CTRL_MASK));
Si l’utilisateur exécute cette combinaison de touches, l’option de menu correspondante est sélectionnée et un événement d’action est déclenché, comme dans le cas d’une sélection manuelle.
Vous ne pouvez associer de raccourcis clavier qu’aux options de menu, et non aux menus. Les
raccourcis clavier n’ouvrent pas le menu. Ils déclenchent directement l’événement d’action associé
à l’option du menu.
D’un point de vue conceptuel, l’ajout d’un raccourci clavier à une option de menu est semblable à la
technique d’ajout d’un raccourci à un composant Swing (nous avons étudié cette technique au
Chapitre 8). Cependant, lorsque le raccourci clavier est ajouté à une option de menu, la combinaison
de touches est automatiquement affichée en regard de l’option (voir Figure 9.26).
Figure 9.26
Raccourcis clavier.
INFO
Sous Windows, la combinaison Alt+F4 ferme une fenêtre. Mais cet accélérateur n’a pas été programmé sous Java. Il
s’agit d’un raccourci défini par le système d’exploitation et qui déclenche toujours l’événement WindowClosing de
la fenêtre active, indépendamment du fait qu’il existe ou non une option de menu Fermer.
javax.swing.JMenuItem 1.2
•
JMenuItem(String label, int mnemonic)
Construit un élément de menu avec un intitulé donné et un caractère mnémonique.
Paramètres :
label
L’intitulé de l’option de menu.
mnemonic
Le caractère mnémonique de l’option ; il apparaît souligné
dans l’intitulé.
Livre Java .book Page 469 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
•
Swing et les composants d’interface utilisateur
469
void setAccelerator(KeyStroke k)
Définit la combinaison de touche k comme raccourci de l’option de menu. La combinaison
s’affiche en face de l’intitulé.
javax.swing.AbstractButton 1.2
•
void setMnemonic(int mnemonic)
Définit le caractère mnémonique du bouton. Il apparaît souligné dans l’intitulé.
Paramètres :
•
mnemonic
Le caractère mnémonique du bouton.
void setDisplayedMnemonicIndex(int index) 1.4
Définit l’indice du caractère à souligner dans le texte du bouton. Utilisez cette méthode si la
première occurrence du caractère mnémonique ne doit pas être soulignée.
Paramètres :
index
L’indice du caractère de texte du bouton à souligner.
Activation et désactivation des options de menu
Il arrive qu’une option de menu spécifique ne doive pouvoir être sélectionnée que dans certains contextes. Par exemple, lorsqu’un document est ouvert en lecture seule, les options Enregistrer et Enregistrer
sous n’ont aucune utilité. Bien sûr, on pourrait les supprimer du menu à l’aide de la méthode
JMenu.remove, mais les utilisateurs seraient désorientés de voir son contenu changer ainsi. Par conséquent, il vaut mieux désactiver les options pouvant donner lieu à des commandes inappropriées. Une
option de menu désactivée apparaît grisée et ne peut pas être sélectionnée (voir Figure 9.27).
Figure 9.27
Options
de menu désactivées.
Pour activer ou désactiver une option de menu, utilisez la méthode setEnabled de la façon
suivante :
saveItem.setEnabled(false);
Il existe en fait deux façons de procéder. Chaque fois qu’une situation change, vous pouvez appeler
la méthode setEnabled sur les options de menu ou les actions concernées. Par exemple, dès qu’un
document a été défini en lecture seule, vous pouvez localiser les options Enregistrer et Enregistrer
sous et les désactiver. Vous pouvez aussi désactiver les éléments juste avant que les options ne soient
affichées. Pour cela, vous devez enregistrer un écouteur pour l’événement "menu sélectionné".
Le package javax.swing.event définit une interface MenuListener comprenant trois méthodes :
void menuSelected(MenuEvent event)
void menuDeselected(MenuEvent event)
void menuCanceled(MenuEvent event)
Livre Java .book Page 470 Jeudi, 25. novembre 2004 3:04 15
470
Au cœur de Java 2 - Notions fondamentales
La méthode menuSelected est appelée avant que le menu ne soit affiché. Par conséquent, elle peut
être utilisée pour activer ou désactiver une option. Le code suivant montre comment désactiver les
actions Enregistrer et Enregistrer sous lorsque le mode Lecture seule est sélectionné :
public void menuSelected(MenuEvent event)
{
saveAction.setEnabled(!readonlyItem.isSelected());
saveAsAction.setEnabled(!readonlyItem.isSelected());
}
ATTENTION
Désactiver des éléments de menu juste avant l’affichage du menu est une idée brillante, mais cela ne fonctionne pas
pour ceux qui disposent aussi de touches accélérateur. Le menu n’étant jamais ouvert à la pression de la touche accélérateur, l’action n’est donc jamais désactivée ; elle est toujours déclenchée par la touche accélérateur.
javax.swing.JMenuItem 1.2
•
void setEnabled(boolean b)
Active ou désactive une option de menu.
javax.swing.event.MenuListener 1.2
•
void menuSelected(MenuEvent e)
Appelée lorsqu’un menu a été sélectionné, avant qu’il ne soit ouvert.
•
void menuDeselected(MenuEvent e)
Appelée lorsqu’un menu a été désélectionné, après qu’il a été fermé.
•
void menuCanceled(MenuEvent e)
Appelée lorsque le processus de sélection du menu est abandonné, par exemple en cliquant en
dehors du menu.
L’Exemple 9.11 est un programme qui génère un ensemble de menus. Il présente toutes les fonctionnalités étudiées dans cette section : menus imbriqués, options de menu désactivées, options avec
cases à cocher et boutons radio, menus contextuels, caractères mnémoniques et raccourcis clavier.
Exemple 9.11 : MenuTest.java
import
import
import
import
java.awt.*;
java.awt.event.*;
javax.swing.*;
javax.swing.event.*;
public class MenuTest
{
public static void main(String[] args)
{
MenuFrame frame = new MenuFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
Livre Java .book Page 471 Jeudi, 25. novembre 2004 3:04 15
Chapitre 9
Swing et les composants d’interface utilisateur
/**
Un cadre avec une barre de menus.
*/
class MenuFrame extends JFrame
{
public MenuFrame()
{
setTitle("MenuTest");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
JMenu fileMenu = new JMenu("File");
JMenuItem newItem = fileMenu.add(new TestAction("New"));
// démonstration des raccourcis clavier (accélérateurs)
JMenuItem openItem = fileMenu.add(new TestAction("Open"));
openItem.setAccelerator(KeyStroke.getKeyStroke(
KeyEvent.VK_O, InputEvent.CTRL_MASK));
fileMenu.addSeparator();
saveAction = new TestAction("Save");
JMenuItem saveItem = fileMenu.add(saveAction);
saveItem.setAccelerator(KeyStroke.getKeyStroke(
KeyEvent.VK_S, InputEvent.CTRL_MASK));
saveAsAction = new TestAction("Save As");
JMenuItem saveAsItem = fileMenu.add(saveAsAction);
fileMenu.addSeparator();
fileMenu.add(new
AbstractAction("Exit")
{
public void actionPerformed(ActionEvent event)
{
System.exit(0);
}
});
// démonstration de menus avec cases à cocher
// et boutons radio
readonlyItem = new JCheckBoxMenuItem("Read-only");
readonlyItem.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
boolean saveOk = !readonlyItem.isSelected();
saveAction.setEnabled(saveOk);
saveAsAction.setEnabled(saveOk);
}
});
ButtonGroup group = new ButtonGroup();
JRadioButtonMenuItem insertItem
= new JRadioButtonMenuItem("Insert");
insertItem.setSelected(true);
471
Livre Java .book Page 472 Jeudi, 25. novembre 2004 3:04 15
472
Au cœur de Java 2 - Notions fondamentales
JRadioButtonMenuItem overtypeItem
= new JRadioButtonMenuItem("Overtype");
group.add(insertItem);
group.add(overtypeItem);
// démonstration des icônes
Action cutAction = new TestAction("Cut");
cutAction.putValue(Action.SMALL_ICON,
new ImageIcon("cut.gif"));
Action copyAction = new TestAction("Copy");
copyAction.putValue(Action.SMALL_ICON,
new ImageIcon("copy.gif"));
Action pasteAction = new TestAction("Paste");
pasteAction.putValue(Action.SMALL_ICON,
new ImageIcon("paste.gif"));
JMenu editMenu = new JMenu("Edit");
editMenu.add(cutAction);
editMenu.add(copyAction);
editMenu.add(pasteAction);
// démonstration de menus imbriqués
JMenu optio
Téléchargement