Notions de base 1. La logistique 2. La qualité du service 3. Notion des flux 4. Les flux logistiques 5. Les flux physiques 6. Les flux d’information 7. Les flux financiers 8. Les flux pouss...
Support de cours Matière : Notions fondamentales en droit public, Filière : Droit Français, Faculté des sciences Juridiques et économiques de Mohammadia, 1er Semestre, Année Universitaire 2009/2010...
for reading and vocabulary development
for reading and vocabulary development
for reading and vocabulary developmentFull description
Partitition de Coeur de Pirate tiré de son premier album pour piano et guitare.
sheet music coeur de pirateFull description
The 4 Notions
NOTIONS DE COMMUNICATION PROFESSIONNELLE
Description complète
Henry Thonier Tome 2Description complète
Henry Thonier Tome 2Description complète
Full description
Description complète
origami
Description complète
Magazine Ta Jeunesse n°260Description complète
Full description
Connaissance des avions Tome 1 Fuselage Ailes Gouvernes Atterrisseurs Commandes de vols Instruments Moteurs
Livre Java .book Page I Jeudi, 25. novembre 2004 3:04 15
http://www.free-livres.com/
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
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
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 ...................................................................................
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 ............................................................................................................
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 .....................................................................................................
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 ....................................................................................
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 ...................................................................................
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 ...............................................................................................
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 ..................................................................................
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 .............................................................................................................................
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 .........................................................................................................
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 ..................................................
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 .............................................................................................................
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 ....................................................................................................................
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 .............................................................................................................
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 pour la concordance de type .................................. Informations de type générique dans la machine virtuelle ................................................
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
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
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.
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.
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).
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.
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
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.
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.
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
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.
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
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
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
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
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
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
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.
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).
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
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.
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.
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.
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.
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
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 !
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.
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.
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.
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
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).
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).
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.
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).
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.
/** 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)
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.
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 WelcomeApplet
This applet is from the book Core Java by Cay Horstmann and Gary Cornell, published by Sun Microsystems Press.
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.
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.
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.
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.
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 } }
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".
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.
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)
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é.
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"
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
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).
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;
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
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.
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
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.
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
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 <<<.
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.
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.
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
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 };
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.
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
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.
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,
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. •
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. •
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.
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.
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();
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?");
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). • •
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
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.
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 %
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
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 %
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.
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.
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).
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)
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
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;
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();
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(); }
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
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.
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;
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:
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++; }
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
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);
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);
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
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.
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.
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 };
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]);
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,");
// 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;
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];
// 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.
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 = {
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++)
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".
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
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
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];
✔ 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++).
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
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
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.
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.
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
(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 :
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.
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();
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();
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.
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.
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.
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);
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.
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(); } }
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.
– 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.
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 . . .
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) {
// 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; }
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;
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 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;
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; }
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 {
. . . 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.
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
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++.
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.
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é
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;
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)
{ 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()); }
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).
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).
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
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
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:");
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.
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()
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) { }
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) {
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++; } }
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
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
{ // 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.
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
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.
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.*;
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.
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 {
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; }
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.
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
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.*;
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
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.
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.
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 ... pour insister, ... pour une police à espacement fixe, ... pour des caractères gras, et même pour inclure une image. Evitez cependant les balises
ou 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 Card 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).
INFO De nombreux programmeurs commencent chaque ligne de documentation par un astérisque, comme ci-après : /** * Un objet Card 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; }
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 setVisible(true) à 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 label "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.
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 The Core Java home page
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 ... 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 ... 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
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.
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).
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).
✔ 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.
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.
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 }
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.
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);
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.
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; }
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é.
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
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.
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.
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.
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
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).
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.
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(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(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.
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.
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; }
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()); } }
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 {
/** @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.
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*.
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; } }
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.
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) && . . .;
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()
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).
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
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.
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));
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;
// 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; }
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
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. 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 staff = new ArrayList();
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);
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 staff = new ArrayList(100);
ATTENTION L’allocation d’une liste de tableaux de la façon suivante : new ArrayList(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.
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++) {
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, 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 list = new ArrayList(); 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 list = new ArrayList(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);
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. 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 staff = new ArrayList(); 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()); } }
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, 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 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 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.
Utiliser un transtypage ne permet pas de se débarrasser de l’avertissement : ArrayList result = (ArrayList) 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) 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. C’est ici que la classe enveloppe Integer fait son apparition. Vous pouvez déclarer une liste de tableaux d’objets Integer : ArrayList list = new ArrayList();
ATTENTION Un ArrayList 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));
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.
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.
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[].
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++.
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.
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. 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 :
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.
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();
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.
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; }
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) {
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();
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. • •
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. • •
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.
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.
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); }
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 squares = new ArrayList(); 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 squares = new ArrayList(); for (int i = 1; i <= 5; i++) squares.add(i * i); System.out.println(new ObjectAnalyzer().toString(squares)); } }
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(); }