Programmer avec des objets
Transcription
Programmer avec des objets
Programmer avec des objets (notes provisoires, janvier 2002) Etienne Vandeput © CeFIS - FUNDP Ces notes sont celles de la formation organisée en 2002 sur le thème précité. Elles sont donc destinées à être remaniées dans un futur proche en fonction des observations, corrections, remarques, commentaires que vous êtes invités à y apporter. Adresse de contact: [email protected] Table des matières Chapitre 1 Pourquoi programmer avec des objets? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Le choix d’un langage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 L’installation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 La machine Java virtuelle (JVM) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Chapitre 2 Des objets qui dialoguent... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Une approche naturelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 L’encapsulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Un exemple simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Les classes d’objets et les mécanismes de la POO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Exercice unique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Chapitre 3 Java: Notions de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Avertissement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Définition d’une classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 La classe Point . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Les types primitifs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 Les méthodes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 Les membres de classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Les constructeurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 La déclaration des variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 La méthode main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 L’exécution du programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Une application qui crée des objets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Objet vs variable objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 La référence “this” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 Le traitement des erreurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 Chapitre 4 Quelques compléments utiles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 Le passage des paramètres en Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 Premier point . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 Second point . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 La classe String . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 La méthode toString() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 Les méthodes statiques et les variables de classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 Les champs et les méthodes déclarés finals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 Les opérateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 Les opérateurs arithmétiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 Les opérateurs logiques et de comparaison . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 Les opérateurs d’incrémentation, de décrémentation et d’affectation élargie . . . . . 48 L’opérateur conditionnel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 Les commentaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 Les tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 Entrées et sorties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 Les conversions automatiques et le transtypage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 Exercices de compréhension . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 Chapitre 5 Java: héritage et polymorphisme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 Héritage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 Spécialisation et généralisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 Intérêt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 Héritage, surcharge et remplacement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 Syntaxe en Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 Le modificateur d’accès “protected” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 Constructeurs et héritage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 Polymorphisme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 Intérêt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 Mécanisme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 Chapitre 6 Les structures de contrôle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 Utilité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 Les structures alternatives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 Alternative simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 Alternative multiple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 Les structures répétitives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 La boucle “while” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 La boucle “do - while” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 La boucle “for” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 “break”, “label”, “continue” et autre “return” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 Chapitre 7 D’autres mécanismes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 Aperçu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 Les classes abstraites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 Les interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 Commentaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 Héritage multiple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 Héritage multiple et interfaces prédéfinies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 Les interfaces graphiques et la gestion des événements . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 Quelques outils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 Le garbage collector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 Les packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 Déclaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 Les noms de packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 Accès . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Hiérarchie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Stockage des classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 La recherche des classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 Chapitre 8 GUI et gestion des événements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 Le modèle des événements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 AWT et Swing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 Que de hiérarchies! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 Interfaces et programmation événementielle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 Le détecteur de multiples de 7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 Première version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 Deuxième version: contrôle de fermeture de la fenêtre . . . . . . . . . . . . . . . . . . . . . . 113 Troisième version: classe anaonyme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 Quatrième version: autre système d’écoute et traitement des exceptions . . . . . . . . 114 Cinquième version: une mise à jour extrêmement dynamique . . . . . . . . . . . . . . . . 117 Le compteur interactif . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 Diagramme de classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 Première version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 Deuxième version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 Troisième version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124 Quatrième version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125 Les classes internes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128 Exercice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 Chapitre 9 JavaScript, Java: quels rapports? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 Un dilemme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 Un air de famille? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 Les langages de script . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 Java et le HTML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 Un brin de comparaison . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 Un autre exemple en JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138 Saisie de nombres et calcul d’une moyenne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141 Programmer avec des objets Chapitre 1 Etienne Vandeput ©CeFIS 2002 Pourquoi programmer avec des objets? Introduction Il n’entre pas dans mes intentions de débattre ici des différents paradigmes de programmation et de leurs intérêts respectifs. De nombreux ouvrages et articles traitent de cette problématique. L’existence de ces paradigmes et les succès relatifs des langages qui s’en inspirent prouvent que les domaines d’application de l’informatique sont suffisamment étendus pour laisser de nombreux champs d’application possibles à chacun d’eux. Je signalerai simplement que la naissance d’un nouveau paradigme vient évidemment des limites atteintes par les langages qui sont associés aux paradigmes courants. La programmation impérative a bien répondu aux attentes, dès lors que les applications développées ne comptaient guère plus de quelques milliers de lignes et étaient le produit d’un ou deux programmeurs. Plusieurs pistes ont été et sont toujours explorées parmi lesquelles, celle de la programmation logique chère à tous ceux qui s’intéressent à l’intelligence artificielle et aux domaines dans lesquels la quantité de données à traiter est gigantesque. Mais il y a d’autres aspects. Ainsi, l’augmentation phénoménale de la complexité des logiciels fait qu’aujourd’hui, le nombre de lignes de code s’élève très souvent à des dizaines de milliers. Ce sont des équipes de programmeurs qui travaillent à la réalisation de ces logiciels et il faut éviter des pertes de temps dues à d’éventuelles synchronisation du travail. Chacun doit pouvoir travailler dans son coin en faisant confiance aux autres et en se montrant digne de la confiance des autres. En conséquence, des impératifs nouveaux apparaissent. Il devient nécessaire: • de penser les traitements d’une manière plus naturelle, plus proche de la réalité1 afin de dégager le programmeur de tout une série de contraintes opérationnelles; • d’encapsuler les traitements dans des modules qui soient des boîtes noires équipées d’une interface, permettant au programmeur de les utiliser sans savoir comment ces traitements sont implantés, mais connaissant les éventuelles données à fournir et les éventuels résultats retournés; • d’évoluer ainsi vers un développement qui favorise la réutilisation logicielle et l’adaptabilité. C’est essentiellement de ces nécessités qu’est née la programmation orientée objet. Cela explique notamment que ce n’est pas un programme d’addition de nombres saisis au clavier jusqu’à ce qu’un signal soit donné (valeur spéciale ou bidon) qui vous convaincra du bien-fondé de ce type de programmation et cela, même si un tel programme peut être écrit au moyen d’objets. 1 Dans certains cas, un raisonnement déterministe n’est pas toujours celui qui est le plus approprié. Chapitre 1 Pourquoi programmer avec des objets? -1- Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Quant à la programmation impérative, elle a encore de beaux jours devant elle, ne fusse que par la nécessité de maintenir une quantité de programmes utiles et qui fonctionnent2. Le but de ce cours n’est pas de vous convaincre que la programmation orientée objet3 est la solution à tous les problèmes, mais seulement de vous faire percevoir quels en sont les mécanismes fondateurs afin de vous permettre de juger, dans quelles situations celle-ci se révèle intéressante. Toutes ces choses étant dites, il faudra sans doute perdre un peu de vos habitudes de programmation pour bien cerner tout le profit que vous pourrez tirer de cette autre manière de programmer. L’introduction qui sera faite et les exemples qui seront choisis tenteront de vous faire oublier vos anciennes amours et de vous attirer hors de leur champ d’influence. Ce cours a aussi pour objectif d’analyser dans quelle mesure les concepts qui y sont développés sont accessibles à des élèves du degré supérieur de l’enseignement secondaire. Vos avis seront donc très autorisés en la matière. Les notions seront présentées de la manière la plus pédagogique possible 4 de manière à pouvoir en juger efficacement. Le choix d’un langage Une dernière chose enfin, il n’est pas possible de parler langage de programmation sans en choisir un en particulier. Ceux qui ont suivi des cours sur la programmation impérative ont souvent utilisé le langage Pascal pour développer des applications. Beaucoup d’experts du domaine de l’enseignement estimaient, sans doute à raison, que ce langage était pédagogique, même s’il n’offrait pas les facilités de développement de certains autres langages. Nous utiliserons principalement le langage Java, même si je caresse l’espoir de pouvoir faire une petite incursion du côté d’un langage de script (en l’occurrence, Javascript). En voici, de mon point de vue et par ordre décroissant d’importance à mes yeux, les principales raisons: • Java est un langage de programmation qui met en oeuvre assez clairement les mécanismes fondamentaux de la programmation orientée objet que sont l’héritage, la surcharge et le polymorphisme; 2 Une entreprise se moque pas mal de faire réécrire des programmes qui fonctionnent bien dans de nouveaux langages, dans la mesure où la réécriture est une source de problèmes potentiels. 3 La Programmation Orientée Objet (POO) se caractérise par l’utilisation d’un certain nombre de mécanismes liés à la création des objets. La plupart des langages de programmation se revendiquant de cette catégorie ne peuvent cependant gommer totalement tous les aspects de la programmation impérative. C’est en ce sens qu’on parle de programmation orientée objet. Certains langages permettent de les réduire à la portion congrue en exploitant au maximum les mécanismes de la POO, d’autres continuent à nécessiter une part importante de programmation impérative tout en proposant certains des mécanismes en question. On parlera donc de langages complètement orientés objet pour désigner les premiers et de langages orientés objet pour désigner les autres et cela, quel que soit leur degré de respect des mécanismes de la POO. 4 C’est en tous cas ma volonté. Chapitre 1 Pourquoi programmer avec des objets? -2- Programmer avec des objets Etienne Vandeput ©CeFIS 2002 • Java demande de la rigueur dans la structure et l’écriture des programmes et les implicites y sont moins nombreux que chez la plupart des langages de script (Javascript, Perl, Php, Python,...); • Java a grandi avec le Web, ce qui le rend bien adapté à son exploitation; • un programme écrit en Java et compilé en octets de code peut tourner sur n’importe quel type de matériel et n’importe quel type de plateforme. Finalement, les huit cours qui sont prévus s’avèreront insuffisants pour couvrir un aperçu assez large de ce que propose Java. Sans connaître ni les capacités du groupe, ni le temps qu’il me faudra pour mettre en place les fondements du langage, j’opterai pour l’installation solide des concepts, même si ce choix m’oblige à envisager avec vous une suite au cours de l’année académique prochaine. C’est une évidence mais laissez-moi vous redire que la bonne compréhension d’un tel cours passe par une pratique minimum. Entre les différentes séances, je ne puis que vous encourager à programmer les petites applications que je vous proposerai. L’installation Cette section est un peu plus technique mais il faut bien décrire quelque part, comment acquérir une version du langage Java et comment l’installer. Je vous suggère d’installer Java2 SDK (Software Development Kit) version 1.3. Cette version est actuellement appelée Java2 Platform Standard Edition v. 1.3. Vous le trouverez à la source c’est-àdire à l’adresse http://java.sun.com/j2se/1.3/ et cela, que votre OS préféré soit Windows, Linux ou même Solaris. Un exécutable d’environ 40 Mo est à télécharger. Son exécution installe le langage et toute la hiérarchie des librairies dont il a besoin. Sous Windows, le dossier par défaut est c:\jdk1.3.1_02 mais il est possible de faire un autre choix de dossier5. 5 Il existe une autre solution: installer Jbuilder 5 Personal un produit de Borland qui offre d’excellentes possibilités d’édition de programmes en Java. Ce logiciel contient une version du JDK 1.3 (Java Development Kit). Cette version est gratuite pour autant que vous respectiez le contrat de licence qui vous est proposé. Elle est téléchargeable à partir de l’adresse http://www.borland.com/jbuilder/. Le fichier compressé à télécharger est de 40 Mo pour la version Windows et de 53 Mo pour la version Linux. Bien entendu, il est tout à fait possible de rédiger du code Java en utilisant le simple blocnotes de Windows ou un éditeur de texte élémentaire. Un produit comme Jbuilder fournit aux développeurs des tas de services intéressants allant de la coloration syntaxique à la génération de documentation de programme en passant par l’indentation automatique, une gestion souple des fichiers etc. La relative convivialité du produit parle en la faveur de son auto-découverte car soyons clairs, la connaissance d’un tel outil dédié aux développeurs professionnels n’est pas l’objectif du cours. Son étude nous éloignerait d’ailleurs de l’essentiel. A vous de voir si l’installation de Jbuilder, à terme, vous sera utile. Tenez compte également des performances de votre matériel. Chapitre 1 Pourquoi programmer avec des objets? -3- Programmer avec des objets Etienne Vandeput ©CeFIS 2002 La machine Java virtuelle (JVM) La description qui suit n’est pas nécessaire à la compréhension du prochain chapitre. Sa lecture peut être différée. Cependant, on évoque tellement souvent la machine virtuelle qu’il me semble utile d’en donner une explication à ce stade à l’intention de ceux qui connaissent déjà un peu les tenants et aboutissants de la programmation orientée objet. Une des préoccupations qui se fait jour au moment du développement du projet Java, c’est la portabilité des applications sur différentes plate-formes. C’est une exigence qui naît du développement d’Internet mais aussi de l’évolution de l’architecture informatique de nombreuses grosses entreprises, de systèmes centralisés vers des systèmes distribués. La variété du matériel et des systèmes d’exploitation en interconnexion impose que les applications développées dans des langages modernes puissent être hébergées sur des systèmes très hétéroclites. Comment cette caractéristique de portabilité est-elle obtenue? Que représente cette machine virtuelle qu’on évoque pour expliquer cette qualité du langage. En voici une très brève explication. L’installation de Java dont il vient d’être question consiste notamment en la fourniture : • d’un grand nombre de classes prédéfinies • de plusieurs outils indispensables (compilateur, interpréteur, générateur de documentation, débogueur,...) Pourquoi un compilateur et un interpréteur? Le principe est le suivant: sur un système donné, le compilateur transforme le code source (texte des programmes) en “octets de code” (bytecodes en anglais). Il s’agit d’un langage universel que chacun des systèmes s’engage à déchiffrer au moyen de son interpréteur spécifique capable de digérer ces octets de code en tenant compte du contexte local. Les bytecodes générés par la machine A gouvernée par l’OS A’ peuvent maintenant être interprétés par l’interpréteur de la machine B gouvernée par l’OS B’. En d’autres termes, un même programme, compilé par différents systèmes, fournit les mêmes bytecodes. A l’exécution du programme sur un système donné, ces bytecodes sont interprétés grâce à un interpréteur propre au système. Le couple constitué de ce système donné et d’une instance de l’interpréteur est appelé machine virtuelle. Un même système peut donc être à la base de l’existence de plusieurs machines virtuelles6. Nous verrons par la suite que pour qu’un programme puisse être exécuté, il faut qu’existe une classe exécutable. Elle doit être enregistrée dans un fichier qui porte le même nom qu’elle suivi de l’extension java. Si la classe exécutable s’appelle Application, elle doit être enregistrée dans un fichier qui s’appelle Application.java. 6 Cette opportunité nous sera de peu d’utilité tant que nous ne développons pas des applications qui interagissent entre elles. Chapitre 1 Pourquoi programmer avec des objets? -4- Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Le code contenu dans ce fichier (et dans les fichiers qui lui sont liés7) est compilé grâce à la commande javac suivie du nom du fichier. La commande javac correspond donc à l’activation de l’outil de compilation. Le fichier résultant portera le nom de la classe suivi cette fois de l’extension class. La compilation du fichier Application.java se fait par la commande javac Application.java et produit un fichier Application.class. Le code de cette classe est exécuté en utilisant la commande java suivi du nom de ce dernier fichier, sans l’extension. Elle correspond à l’activation de l’interpréteur de bytecodes. La commande java Application produit l’exécution du programme 8. Toutes les commandes qui viennent d’être évoquées sont données au niveau d’un shell de commandes du système d’exploitation utilisé et fonctionnent sous réserve d’un choix correct des paramètres concernant les variables d’environnement (notamment les chemins que l’OS doit suivre pour trouver les programmes et ceux que les programmes doivent suivre pour trouver le code des applications). 7 Il est clair que la définition d’une classe, y compris celle d’une classe exécutable, fait généralement appel à d’autres définitions de classes contenues dans d’autres fichiers. La compilation consiste à générer des bytecodes qui intègrent les informations contenues dans ces différents fichiers. 8 Cette commande peut comporter d’autres paramètres. En particulier, nous verrons qu’il est possible, et même souhaitable, de définir dans les différentes commandes les chemins d’accès aux fichiers. Pour ce qui est du lancement d’une JVM, on peut aussi fournir des paramètres qui sont récupérables par le programme. Chapitre 1 Pourquoi programmer avec des objets? -5- Programmer avec des objets Chapitre 2 Etienne Vandeput ©CeFIS 2002 Des objets qui dialoguent... Une approche naturelle Un des reproches que l’on peut faire à la programmation impérative est d’éloigner le programmeur du monde réel en l’obligeant à décrire, d’une manière très élémentaire, des actions et des traitements qui sont parfois très complexes. Les limites de ce type de démarche sont atteintes dès l’instant où l’ensemble de ces instructions devient pléthorique. Il devient alors difficile de contrôler l’ensemble des modules d’une application, même lorsque l’analyse de celle-ci a fait l’objet d’une approche descendante. Une autre critique importante revient régulièrement lorsqu’on parle de programmation impérative: l’application programmée répond généralement à une vision, espérons-le correcte, que l’on a des traitements au moment du développement de celle-ci. La conséquence, c’est que les programmes rédigés manquent souvent d’adaptabilité et que le monde est (trop) souvent à refaire. La programmation orientée objet trouve une partie de son intérêt dans la réponse qu’elle apporte à la difficulté qui vient d’être évoquée. Pour prendre un exemple simple, un patron ne peut gérer une très grosse entreprise en voulant s’occuper des moindres détails de son fonctionnement, des grandes décisions stratégiques, jusqu’à l’achat des produits d’entretien des locaux, en passant par la paie des ouvriers, par exemple. Il doit pouvoir faire confiance à ses subordonnés et se limiter à une interaction avec eux. C’est un peu l’idée de la programmation orientée objet d’arriver à constituer des logiciels basés sur l’interaction entre les objets, chacun pouvant demander à d’autres d’effectuer certaines tâches pour la réalisation desquelles eux-mêmes s’adresseront à d’autres objets et ainsi de suite. Un des principes sur lesquels nous reviendrons est basé sur le fait qu’une relation de confiance existe entre les objets. Chacun d’entre eux propose et s’engage à rendre au monde extérieur (les autres objets potentiels) un ensemble de services. Il le fait via une interface d’interaction qui ne permet pas de savoir comment ces services sont rendus. Le déroulement d’une application ressemblera à une conversation entre objets9. Ceux-ci vont donc s’envoyer mutuellement des messages qui seront autant de demandes de sous-traitance. Si un objet ne peut réaliser une partie du travail qui lui est demandé, il devra pouvoir confier cette partie du travail à un autre objet. La conversation entre objets sera donc aussi constituée de réponses que les objets s’enverront les uns les autres. Pour établir une comparaison avec des choses connues, une demande de service d’un objet à un autre est une sorte d’appel de fonction. Dans le vocabulaire de la POO, on parlera d’appel de méthodes. L’encapsulation 9 Il faudra bien qu’un objet engage la conversation! Chapitre 2 Des objets qui dialoguent... -6- Programmer avec des objets Etienne Vandeput ©CeFIS 2002 C’est un des principes essentiels de la POO. Un objet est caractérisé par son comportement et par son état. Son comportement est décrit par les services qu’il peut rendre et donc, d’une certaine manière par les fonctions qu’il peut remplir. Nous avons déjà signalé qu’on parlait ici de méthodes plutôt que de fonctions. L’état d’un objet est ce qui le caractérise à un moment donné. Cet état est variable, dépend de ses comportements et des comportements des autres objets. La structure d’un objet ne lui est généralement pas propre. Il fait partie d’une classe d’objets qui la partagent. Il en est ainsi dans le monde réel. Une personne, une voiture, un chien, une carte d’identité,... possèdent généralement une série de propriétés communes, même si les valeurs qui y sont attachées sont différentes. Les personnes n’ont pas le même nom, les chiens ne sont pas de la même race, les cartes d’identité ne contiennent pas la même photo,... Ils ont également une série de comportements identiques. Les personnes parlent, les chiens aboient, les voitures peuvent freiner et accélérer,... Il n’est pas du tout souhaitable que chaque objet puisse modifier anarchiquement et directement l’état des autres objets. Il est préférable de réserver la modification de l’état à l’objet lui-même. Ceci n’implique pas qu’un objet puisse demander cette modification mais il devra le demander à l’objet lui-même qui pourra, notamment, s’assurer que cette modification est valide. Il vaut mieux, en effet, qu’un seul objet prenne cette responsabilité en charge. Ces remarques vont nous conduire à définir, plus tard, la portée des variables et des méthodes. Si le comportement d’un objet est caractérisé par les méthodes qu’il supporte, son état est principalement décrit par des variables propres appelées variables d’instances. Pour désigner cette association de l’état d’un objet à son comportement, on parle d’encapsulation. Les données et les traitements ne semblent plus séparés mais donnent l’impression d’être regroupés à l’intérieur de l’objet lui-même. L’encapsulation présente un avantage incontestable. Lorsqu’un programmeur utilise des classes qu’il n’a pas développées lui-même et en crée des instances, il ne connaît pas la manière dont les traitements correspondant à l’exécution des méthodes sont implantés. La seule chose que celui-ci connaisse, c’est l’ensemble des services rendus par la classe, le contrat qu’elle s’engage à remplir en quelque sorte 10 . Cet avantage n’est pas à négliger lorsqu’il s’agit d’évoluer vers un type de programmation dont une partie importante de la démarche consiste à réutiliser des modules (logiciels) existants. Un exemple simple Considérons un carré. On peut imaginer que son état soit décrit par deux données variables que sont la longueur de son côté et la coordonnée dans un repère défini de son coin supérieur gauche . Le fait d’évoquer un carré contient implicitement la référence à un objet de la classe des carrés. On peut demander beaucoup de services à un objet de type carré. Les plus évidents sont sans doute: • calculer et fournir son périmètre, 10 Dans le contexte d’une bonne programmation, les classes sont généralement bien documentées, qu’il s’agisse de décrire le contrat rempli par chacune des méthodes ou de détailler la nature des autres membres de la classe. Chapitre 2 Des objets qui dialoguent... -7- Programmer avec des objets Etienne Vandeput ©CeFIS 2002 • calculer et fournir son aire, • se dessiner, • changer ses propres dimensions ou sa position, • fournir certains de ces renseignements à la demande, • ...11 Lorsqu’on demandera à un objet carré de changer la valeur de la longueur de son côté, celui-ci pourra vérifier que la valeur fournie est cohérente et faire en sorte de corriger les éventuelles erreurs pour que les données soient consistantes. Par exemple, si le paramètre fourni pour la longueur du côté est négatif, l’objet pourra décider de lui donner la valeur 0. Le fait de confier cette tâche à l’objet lui-même combiné au fait que l’accès aux données peut lui être réservé, empêche tout objet (logiciel12) extérieur de rendre le système de données inconsistant. Les classes d’objets et les mécanismes de la POO En programmation orientée objet, chaque objet fait partie d’une classe dont la description mentionne les données et le comportement qui le caractérisent ainsi que ses semblables. En suivant l’exemple, la description de la classe des carrés mentionnera l’existence de deux champs de données: la longueur du côté et la position du sommet supérieur gauche. Elle précisera les méthodes disponibles, celle qui calcule et affiche le périmètre, celle qui modifie la position du sommet,... en indiquant chaque fois le nombre et le type des paramètres à fournir. Un programmeur qui utilisera la classe carré saura qu’il peut en obtenir le périmètre sans savoir comment le calcul est effectué13. Le mot “type” est bien connu des programmeurs. La programmation orientée objet fera généralement apparaître deux sortes de types: les types primitifs (entiers, caractères, booléens,...) et les types construits (ceux qui correspondent aux classes d’objets). Ainsi, un objet peut être déclaré de type carré, cela signifiera qu’il est caractérisé par tel et tel types de données et par telle et telle méthodes. Notre exemple peut constituer une application directe de ce qui précède. Nous pouvons décider que le type de donnée pour la longueur du côté est “nombre entier” qui est un type primitif et que le type de donnée pour le sommet est point ou point constitue une autre classe d’objets. 11 Dans une perspective graphique plus poussée, on pourrait considérer que le carré possède un fond coloré, un bord ayant lui-même une épaisseur et une couleur, etc. 12 De par le fait qu’un objet encapsule ses données et ses méthodes, on assimile parfois le mot “objet” au mot “logiciel”. Certains objets peuvent en effet être d’une grande complexité. 13 Dans ce cas-ci, ça peut paraître évident. Notez toutefois que le calcul pourrait consister à faire multiplier l’aire du carré par 16 et prendre la racine carrée du résultat. La seule chose qui intéresse l’utilisateur (programmeur) c’est la garantie d’obtenir la valeur du périmètre. Chapitre 2 Des objets qui dialoguent... -8- Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Toutes ces possibilités prendront forme dans le chapitre suivant consacré à Java. En attendant, et pour montrer que la programmation orientée objet fait la part belle à l’abstraction, nous pourrions encore avoir une autre vue des choses et décider de créer d’abord une classe point qui contiendrait, par exemple, deux données de type nombre entier (une abscisse et une ordonnée). Nous choisirions alors de dériver la classe carré de la classe point en précisant qu’un carré, c’est d’abord un point (son sommet supérieur gauche) avec en plus, la longueur d’un côté (ce qui permettrait de le définir complètement)14. Ceci permet d’amorcer la présentation d’un des mécanismes classiques de la POO à savoir l’héritage. L’héritage est ce que l’on a inventé de mieux pour justement ne pas devoir tout réinventer. En se basant sur le principe qui veut que ce qui a déjà été défini peut être réutilisé ou légèrement modifié et que le reste peut être ajouté, une classe d’objets peut hériter d’une autre classe en la spécialisant. Cette spécialisation consiste: • à rajouter des choses qui n’existaient pas dans la définition de la classe précédente, à savoir des champs de données ou des méthodes; • à redéfinir certaines méthodes en leur faisant faire les choses de manière différente, tout en se ménageant la possibilité de considérer l’objet comme faisant aussi partie de la classe d’héritage. On peut aussi envisager les choses d’une autre manière en considérant que des objets appartenant à des classes différentes ont des points communs qui pourraient permettre de les considérer comme faisant tous partie d’une classe d’objets plus générale. L’héritage s’examinera donc souvent dans le sens de la spécialisation mais parfois aussi dans celui de la généralisation. Cette petite réflexion nous permet aussi d’amorcer la description d’un autre mécanisme fondamental: le polymorphisme. Si la classe d’un objet hérite d’une autre classe, tout objet de la première est aussi un objet de la seconde15. De ce fait, l’objet a plusieurs formes possibles et on peut donc l’invoquer en le considérant de différentes manières. C’est pourquoi on parle de polymorphisme. Il existe en POO d’autres mécanismes tels, par exemple, la surcharge. Un résultat peut être renvoyé par une méthode sur base de données différentes. Ainsi, le calcul de l’aire du carré peut être réalisé sur base de la connaissance de la longueur du côté de celui-ci, mais aussi, pourquoi pas, sur base de la connaissance des coordonnées de deux sommets opposés. Dans le premier cas, l’unique paramètre est un simple entier alors que dans le second, les deux paramètres sont des objets “points”. Pour qu’un compilateur puisse identifier une méthode sans équivoque, il se réfère à sa signature qui comprend nécessairement le nom de la méthode mais aussi la liste (éventuellement vide) des paramètres et de leurs 14 Ceci a (peut-être) de quoi faire dresser les cheveux sur la tête des mathématiciens. Dire qu’un carré est une sorte de point est en effet assez osé. Le mathématicien dirait plutôt qu’un point correspond à un carré dont la longueur du côté est nulle... et encore. Pourtant, cette manière de voir les choses peut être commode en POO. 15 Attention, l’inverse n’est pas vrai car la classe qui hérite “spécialise” la classe d’héritage. Il est fort probable, par exemple, qu’elle supporte des méthodes que la précédente ignore. Chapitre 2 Des objets qui dialoguent... -9- Programmer avec des objets Etienne Vandeput ©CeFIS 2002 types. C’est en ce sens qu’on parle de surcharge de la méthode. On peut donc dire qu’un même nom de méthode dissimule des sémantiques différentes16. Nous avons évoqué le mécanisme d’héritage. Beaucoup de langages de POO implantent un mécanisme d’héritage multiple. Une classe peut hériter de plusieurs autres classes au sens où nous l’avons défini. La sous-classe hérite alors de caractéristiques de toutes les classes dont elle hérite. Cela ne va pas sans poser un certain nombre de problèmes17. Cette première approche devrait suffire à comprendre dans quel esprit fonctionne la POO. L’algorithmique reste bien présente, notamment au niveau de la description des méthodes. Néanmoins, tout code préalablement rédigé et qui fonctionne, c’est-à-dire qui remplit son contrat, ne doit plus être pris en compte par celui qui utilise l’objet. Cette approche autorise, vous l’aurez compris, un meilleur débogage et une meilleure localisation des problèmes. Dans le chapitre suivant, nous vous proposons de découvrir comment Java, un langage qualifié de totalement orienté objet, implante les concepts qui viennent d’être décrits. Ce sera aussi l’occasion de découvrir la syntaxe de ce langage et de souligner quelques bonnes habitudes rédactionnelles. A la fin du syllabus, nous tentons une comparaison, si tant est qu’elle ait du sens, entre Java et JavaScript que l’on pourrait qualifier de faiblement orienté objet. Exercice unique En vous basant sur l’exemple du carré dont les sommets sont des points qui ont chacun une coordonnée, identifiez, dans les domaines que vous connaissez, des objets dont la définition fait éventuellement appel à d’autres objets. Imaginez, pour ces objets et ceux qui les composent, des méthodes que l’on pourrait invoquer auprès d’eux en précisant ce que ces méthodes sont sensées effectuer comme traitement ou renvoyer comme résultat. Pour que les choses soient claires, voici d’autres orientations possibles pour étoffer l’exemple du carré et des points18. Elles seront traitées dans les chapitres qui suivent. Vous devriez pouvoir tenir un discours comme celui qui suit, discours qui vous permettrait de dégager les classes, les champs de 16 Dans notre exemple, le résultat renvoyé est un entier qui représente l’aire du carré mais rien n’empêche que la sémantique même des résultats soient tout à fait différentes selon le nombre et le type des paramètres de la méthode. 17 Le langage Java, que nous étudierons au prochain chapitre, n’implante pas directement la notion d’héritage multiple. Une classe ne peut hériter que d’une seule autre classe. Néanmoins, le concept d’interface autorise sa simulation en en supprimant les désavantages. 18 Pardonnez-moi d’avoir puisé cet exemple dans le domaine des mathématiques, ma première vocation ;-) Chapitre 2 Des objets qui dialoguent... - 10 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 données et leurs types (donc éventuellement d’autres objets) ainsi que quelques méthodes supportées par ces classes d’objets19. Une droite est constituée de points. Deux points permettent de définir une droite, mais il y peut y avoir d’autres moyens. On peut demander à une droite de changer de direction, de glisser parallèlement à elle-même. Pour autant que l’on ait défini ce qu’est un cercle, on peut demander à une droite si elle est tangente à un cercle donné, si elle comprend un point donné etc. De la description qui précède, on peut retenir qu’à tout le moins, il existe des points, des droites et des cercles. Pour ce qui est des droites, on peut imaginer une méthode qui répondra par vrai ou faux à la question: comprends-tu tel point? Il s’agira d’un résultat. Une autre méthode ne fournira pas de résultat mais modifiera les paramètres de la droite lorsqu’on lui demandera de glisser parallèlement à elle-même pour passer par un point donné etc. 19 Il serait utile que chacun s’impose une telle description de manière à pouvoir la faire partager lors de la prochaine séance. Cette réflexion pourrait conduire à la constitution d’une mini-base de données d’exemples exploitables en situation d’enseignement. Chapitre 2 Des objets qui dialoguent... - 11 - Programmer avec des objets Chapitre 3 Etienne Vandeput ©CeFIS 2002 Java: Notions de base Avertissement Dans ce chapitre, j’éviterai, autant que possible, de vous noyer dans une multitude de détails, même si ceux-ci sont importants. Je me contenterai de préciser les éléments qui vous permettront assez rapidement de réaliser une petite application Java en mettant d’emblée l’accent sur les concepts qui me paraissent fondamentaux. A ce stade, il n’est pas encore question de mettre en oeuvre les mécanismes principaux de la POO. Nous sommes évidemment encore bien loin de pouvoir nous intéresser à la manière de conduire le développement d’une application. . Il y a tellement de choses à illustrer que nous laisserons un peu de côté (pour l’instant) la méthodologie. Définition d’une classe Pour une introduction en douceur de la syntaxe Java, intéressons-nous aux classes que nous avons évoquées dans le chapitre précédent 20. Définissons-les en apportant à leur définition les commentaires utiles. La classe Point Pour rappel, cette classe caractérise des objets Point qui ont une coordonnée composée d’une abscisse et d’une ordonnée. Nous décidons que ces deux informations sont des nombres entiers. Voici à quoi pourrait ressembler la définition de cette classe. class Point{ public int x; public int y; } 20 Je me permets d’en rajouter encore un peu à l’avertissement, en vous faisant remarquer que les objets dont il est ici question ne s’inscrivent pas dans une problématique bien précise. Il faut d’ailleurs reconnaître qu’un des intérêts de la POO, c’est de pouvoir revisiter la définition des objets en fonction de nécessités nouvelles. On définira donc souvent des classes d’objets de manière très schématique, voire très courte, simplement parce qu’on les a identifiées en réservant à plus tard une description plus détaillée. Le fait que certaines classes d’objets ne soient pas utilisées “in fine” n’est pas vraiment un problème. Chapitre 3 Java: notions de base - 12 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Dans la définition d’une classe, le mot-clé class est suivi du nom de la classe. Le nom d’une classe doit impérativement commencer par une lettre et peut se poursuivre par des chiffres et des lettres. En Java, on utilise conventionnellement des noms de classes commençant par une lettre majuscule et on utilise des lettres majuscules au début de chaque autre mot dans le nom: Formes, FormesGeometriques, CompteurDeMots,... Attention, Java est sensible à la casse. Tout ce que la classe englobe en termes de champs de données et de méthodes est encadré par des accolades. Une bonne habitude de rédaction consiste à faire suivre le nom de la classe de l’accolade ouvrante et de placer l’accolade fermante au même niveau que le premier caractère de la ligne de définition de la classe, ce qui permet une détection rapide des erreurs de syntaxe dues à leur absence. Comme dans de nombreux langages, le séparateur d’instruction en Java est le point-virgule. Le mot public utilisé pour les champs de données, mais qui peut l’être aussi pour la classe et les méthodes, est un modificateur d’accès. Nous discuterons plus tard de l’opportunité d’élargir ou de restreindre les accès aux champs de données et aux méthodes. Les objets de cette classe comptent deux champs de données qui sont du type primitif “entier” (int) et ne supportent, pour l’instant, aucune méthode. Les types primitifs Java est un langage fortement typé. Le type de chacune des variables doit être impérativement précisé. Un rapide premier contact avec ces types primitifs nous apprend qu’ils sont au nombre de huit: • quatre types entiers; • deux types réels à virgule flottante; • un type caractère; • un type booléen. Le type int est codé sur 4 octets, le type short sur 2 octets, le type long sur 8 octets et le type byte sur un seul. Le type float est codé sur 4 octets et le type double sur 8 octets21. Le type char correspond à l’Unicode (2 octets)22. Le type boolean prend les valeurs true et false. Les méthodes Nous allons pallier l’absence de méthodes de la classe point en créant une méthode qui fournit la valeur de l’abscisse du point, une qui fournit son ordonnée et deux autres méthodes qui permettent de les modifier. Ces méthodes sont donc autant de services que l’objet peut rendre au monde extérieur. 21 Java utilise la norme IEEE 754-1985. 22 Attention, syntaxiquement ‘A’ désigne le caractère A alors que “A” désigne la chaîne de caractères A. Il n’y a pas de type primitif pour les chaînes de caractères mais il existe une classe String prédéfinie. Chapitre 3 Java: notions de base - 13 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 class Point{ private int x; private int y; public int getAbscisse(){ return x; } public int getOrdonnee(){ return y; } public void setAbscisse(int a){ x=a; } public void setOrdonnee(int b){ y=b; } } La définition d’une méthode commence généralement par un modificateur d’accès. Elle comprend également le type de résultat renvoyé par la méthode s’il y en a un. Les méthodes getAbscisse et getOrdonnee renvoient un résultat dont le type est le type primitif “entier”. En cas d’absence de résultat retourné, le mot clé est void comme dans les méthodes setAbscisse et setOrdonnee. Le choix du nom de la méthode est soumis à la recommandation suivante: lettres minuscules (et éventuellement chiffres) et majuscule à la première lettre de chaque mot à partir du deuxième. Le nom de la méthode est suivi des paramètres éventuels précédés de leur type et séparés par des virgules23. L’opérateur d’affectation est symbolisé par le signe =. Lorsqu’il s’agit d’affectation entre objets, ce sont des références qui sont unifiées. Voyez à ce propos ce qui est dit de la construction des objets. Le choix d’un accès public pour les méthodes est assez classique. Les objets extérieurs doivent pouvoir invoquer la méthode de l’objet sauf si celle-ci est destinée à l’usage exclusif de la classe. Dans ce cas, il vaut mieux la déclarer private. L’accès aux champs de données est maintenant private 24. C’est une bonne idée que d’empêcher la modification de l’état de l’objet par d’autres objets que lui-même. En d’autres termes, plutôt que de 23 Lorsque le paramètre est un objet, le type n’est pas un type primitif, mais la classe à laquelle l’objet est censé appartenir. 24 Il est bon de donner aux champs d’une classe l’accès le plus restreint possible et à l’élargir en fonction des nécessités. Dans le tout premier exemple, la classe Point ne comprenait pas de méthode. Il était donc impératif que les champs soient déclarés public. Chapitre 3 Java: notions de base - 14 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 modifier l’état d’un autre objet, un objet invoquera la méthode de ce dernier pour accomplir le travail. L’intégrité des données est ainsi plus facile à assurer de même que le débogage et la maintenance. On sent que l’idée sous-jacente est de créer du logiciel qui soit facile à adapter, grâce à une localisation précise des endroits où les traitements sont effectués. Les membres de classes Les méthodes et les champs de données sont appelés membres de classe. Une classe peut aussi posséder, comme membres, d’autres classes (ou des interfaces25) mais cela ne nous est pas très utile pour l’instant. Le type d’accès à chaque membre de classe doit être spécifié. Jusqu’à présent, nous avons évoqués les accès public et private mais l’accès protected pourra aussi nous intéresser par la suite. Les constructeurs Un programme OO a pour objectif essentiel de faire naître des objets dans le but de les faire communiquer et réagir. Pour prendre un exemple très élémentaire, lorsqu’un objet carré est créé, un autre objet peut en invoquer les méthodes afin d’obtenir des informations à son sujet (son aire, son périmètre,...). Cela signifie qu’il est nécessaire de disposer d’une technique de construction des objets. A cette fin, chaque classe doit contenir au moins un constructeur d’objets 26. Le constructeur d’objets est une sorte de méthode de fabrication de l’objet bien qu’il ne s’agisse pas d’une méthode au sens défini précédemment. Il existe un constructeur par défaut qui ne comporte évidemment aucun paramètre. Le programmeur a le loisir de définir autant de constructeurs qu’il le souhaite. Ceux-ci vont différer par les paramètres qu’ils vont accepter et par les initialisations des objets qu’ils vont effectuer. Nous savions déjà que la définition d’une classe se composait de la définition des champs de données et de celle des méthodes de la classe. Nous pouvons maintenant y ajouter les constructeurs des objets (autres que le constructeur par défaut qui ne doit pas être défini). Un constructeur d’objets d’une classe porte toujours le nom de la classe. Sa signature27 peut comporter de 0 à n paramètres. Comment, concrètement, un objet est-il créé? Le jeu de la communication des objets entre eux fait qu’à un moment donné du déroulement du programme, une méthode d’une des classes d’objets est en train de s’exécuter. Il est possible que cette exécution résulte elle-même d’un appel provoqué par l’exécution d’une autre méthode associée à un autre objet ou à une autre classe. La création d’un objet est une des instructions possibles faisant partie du code de la méthode exécutée. La manière dont l’objet est créé (l’instruction) détermine celui des constructeurs qui doit être choisi. 25 La notion d’interface est la réponse de Java à l’absence de mécanisme d’héritage multiple. Cette notion est développée plus loin. 26 A titre d’exception, nous verrons qu’il existe des classes qui ne sont jamais instanciées. 27 La signature d’une méthode ou d’un constructeur comprend son nom, la liste des paramètres et leur type. Chapitre 3 Java: notions de base - 15 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Améliorons la classe Point en y ajoutant deux constructeurs. public class Point{ private int x; private int y; public Point(){ setPoint(0,0); } public Point(int a,int b){ setPoint(a,b); } public int getAbscisse(){ return x; } public int getOrdonnee(){ return y; } public void setAbscisse(int a){ x=a; } public void setOrdonnee(int b){ y=b; } public void setPoint(int a,int b){ x=a; y=b; } } Le premier constructeur est sans paramètre. Son utilisation provoque la création d’un point origine. Le second constructeur demande deux paramètres de type entier. Syntaxiquement, la création d’un point prendra une de ces formes: Point p1=new Point(); Point p2=new Point(3,5); ou une forme équivalente comme Point p1; int a=4; ... Chapitre 3 Java: notions de base - 16 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 p1=new Point(4,a) Une bonne habitude consiste à choisir comme identificateurs de variables des chaînes de caractères contenant des lettres et éventuellement des chiffres en utilisant la même règle que pour les méthodes (lettres minuscules et première lettre en majuscule à partir du deuxième mot; exemple: centreDuCercle). Notez aussi que les types primitifs commencent par une minuscule ce qui permet de les distinguer des types correspondant aux classes d’objets. Dans la description de la classe, nous avons rajouté une méthode setPoint qui permet de modifier à la fois l’abscisse et l’ordonnée d’un point. Cette méthode apparaît dans la définition des deux constructeurs. Les constructeurs sont déclarés d’accès public ce qui permet à des méthodes d’autres classes d’invoquer la création de nouveaux objets. Chaque fois qu’un objet est créé, on dit que la classe est instanciée. Les variables que constituent les champs de données d’un objet sont propres à chacun d’eux, malgré qu’elles portent les mêmes noms. C’est la raison pour laquelle on parle de variables d’instances pour les désigner. S’il n’y a pas d’ambiguïté (objet courant), la variable d’instance est désignée par son nom. Dans le cas contraire, le nom de l’objet doit être mentionné suivi d’un point et du nom de la variable. L’instruction abscisse = p1.ordonnee; signifie que la valeur du champ ordonnee de l’objet point p1 est affectée au champ abscisse de l’objet point courant. Il s’agit bien ici d’une affectation classique puisqu’elle concerne des variables dont les types sont primitifs. Nous verrons plus tard qu’il est possible de définir des variables de classes pour accueillir des valeurs qui sont identiques pour toutes les instances de la classe. La déclaration des variables Une règle simple et importante concerne la déclaration des variables, qu’il s’agisse de variables de types primitifs ou de variables objets: elle peut se situer à n’importe quel endroit mais doit toujours précéder l’utilisation de la variable. Cette règle autorise donc des écritures telles Carre monCarre = new Carre(5); double monSalaire, tonSalaire = 4000; monSalaire = tonSalaire/2; Chapitre 3 Java: notions de base - 17 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Une réflexion sur la portée des variables sera nécessaire dans la suite de notre réflexion. Signalons déjà qu’une variable cesse d’exister en dehors du bloc dans lequel elle est définie. Un bloc est délimité par des accolades et peut évidemment contenir d’autres blocs. Ainsi en est-il du bloc de définition d’une classe. La définition d’une méthode ou d’un constructeur est également réalisée dans un bloc. Nous verrons que les blocs apparaissent également au niveau des structures de contrôle des actions mais peuvent aussi être définis n’importe où (pour regrouper plusieurs instructions, par exemple). La méthode main Si les objets s’envoient les uns les autres des messages de méthodes à exécuter, il faut bien un point de départ. En réalité, une application peut être vue comme une conversation entre objets. L’initiatrice de cette conversation est habituellement une classe qui possède dans sa définition une méthode appelée main. L’application démarre en demandant à la machine Java virtuelle (JVM) d’exécuter la dite classe. La méthode main est alors exécutée. Dans les instructions de celle-ci, on peut trouver des invocations des méthodes des autres objets ou des appels à des constructeurs d’objets d’autres classes. En voici un exemple très court. L’invocation d’objet y est réduite à sa plus simple expression mais ça nous permet de faire quelques commentaires d’ordre syntaxique et de comprendre comment fonctionne une “application”. Un seul objet est invoqué par la méthode main et c’est un objet prédéfini. public class Application1{ public static void main(String args[]){ System.out.println("Bienvenue au cours Java!"); } } Cet exemple peut paraître bizarre car il ne ressemble guère à ce que nous avons vu jusqu’à présent. La classe ne possède pas de variables d’instance (en fait, elle ne sera jamais instanciée). Son unique méthode est la méthode main. Elle est déclarée static et ses paramètres peuvent sembler curieux. Une méthode est déclarée static lorsqu’il s’agit d’une méthode de classe. Elle ne concerne pas un objet quelconque de la classe, mais la classe elle-même. Nous verrons que lorsqu’une classe possède des variables de classe, elles sont également déclarées static. Le paramètre args[] est en réalité un tableau de chaînes de caractères (String) auquel nous avons donné le nomt args. Ce paramètre désigne un tableau des chaînes de caractères qui peuvent éventuellement suivre la commande d’exécution au niveau du shell de commande. Dans la plupart des exemples que nous utiliserons, ces paramètres seront absents. Chapitre 3 Java: notions de base - 18 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Vous pourriez également écrire public void main(String[] tableau) ou encore public void main(String table[]). Les syntaxes String[] param et String param[] signifient la même chose à savoir: param est un tableau de String. Notez encore que String est une classe et non un type prédéfini (d’où la majuscule). Comme toutes les méthodes dynamiques, la méthode println est invoquée sur un objet. Comprendre lequel est un peu plus délicat mais rappelez-vous que nous sommes dans un contexte où tout est objet. out est un champ statique28 (non modifiable) de type PrintStream de la classe non instanciable System. La classe PrintStream est également une classe prédéfinie29. La variable objet out correspond à la sortie standard (souvent l’écran) et prend comme argument une chaîne de caractère. Le travail de cette méthode est d’envoyer la chaîne de caractères vers cette sortie. Ceci nous fait prendre conscience du fait qu’il existe évidemment un nombre considérable de classes prêtes à l’emploi. Sinon, à quoi cela servirait-il de parler de réutilisabilité! Ces classes sont regroupées dans des packages. Une section est également consacrée à leur utilisation. Le package java.lang qui contient la classe System est automatiquement chargé en mémoire à l’exécution d’un programme. L’exécution du programme Pour pouvoir faire exécuter un programme aussi simple que celui-là, rappelez-vous certaines choses30. Lorsque du code Java est écrit, celui-ci doit être compilé. La compilation produit des octets de code (en anglais, bytecodes), cette espèce de langage universel qui sera interprété au niveau de chaque système par un interpréteur Java qui est propre au système utilisé. La production des octets de codes est réalisée par la commande (le programme) javac. Ce programme est localisé quelque part dans le système de fichiers. Sous Windows, par exemple, il pourrait se trouver dans c:\jdk1.3.1_02\bin. Sous Unix ou Linux, on pourrait le trouver dans /usr/local/bin. La classe qui contient la méthode main doit être enregistrée dans un fichier qui porte le même nom qu’elle et l’extension java. Dans notre exemple, le fichier doit s’appeler Application1.java. Sa compilation javac Application1.java 28 Les membres de classe peuvent être déclarés statiques. Cela signifie qu’ils ne peuvent plus être modifiés par la suite. Dans ce cas, le mot-clé static est utilisé lors de leur déclaration. Il peut paraître bizarre qu’une méthode (qui est un membre de classe) soit modifiable. Nous verrons que c’est pourtant souvent le cas lorsque nous aurons parlé d’héritage. La méthode main d’une classe application est déclarée static. 29 Pour une documentation complète des classes prédéfinies et de leurs méthodes, je vous recommande l’adresse suivante http://java.sun.com/j2se/1.4/docs/api/index.html. 30 Si vous avez sauté la lecture du paragraphe concernant la machine Java virtuelle en fin du chapitre 1, c’est sans doute le moment d’y revenir. Chapitre 3 Java: notions de base - 19 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 produira un fichier qui s’appellera Application1.class. Pour faire exécuter l’application, il faut faire appel à l’interpréteur qui se trouve, en principe, dans le même répertoire ou dossier que le compilateur. La commande sera, dans notre cas, java Application1 Le message s’affiche au niveau de la ligne de commande. Attention, pour que l’interpréteur trouve la classe à exécuter, il faut lui en fournir le chemin. Sous Windows, par exemple, vous modifierez la variable d’environnement classpath en utilisant la commande set classpath=c:\java\myclasses si la classe ci-dessus se trouve dans ce dossier ou, ce qui est souhaitable, vous utiliserez l’option -classpath ... au niveau de la commande. java -classpath=c:\java\myclasses Application1 Dans un premier temps, nous nous arrangerons pour que les classes se trouvent dans le dossier par défaut. Plus tard, lorsque nous parlerons des packages, nous verrons comment avoir une démarche organisée à ce propos. Une application qui crée des objets L’exemple précédent ne nous montre toutefois pas comment des objets peuvent être créés et s’envoyer des messages. Maintenant que nous connaissons la manière de faire afficher des résultats, nous allons faire en sorte que l’application crée deux points, calcule la distance entre les deux et affiche le résultat. Pour cela, nous ajoutons une méthode distanceJusque qui prendra comme paramètre un autre objet point. Lorsque cette méthode sera invoquée sur un objet point, celui-ci calculera la distance qui le sépare du point donné en paramètre. public class Point{ private int x; private int y; public Point(){ setPoint(0,0); } public Point(int a,int b){ setPoint(a,b); } public int getAbscisse(){ return x; } public int getOrdonnee(){ return y; } Chapitre 3 Java: notions de base - 20 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 public void setAbscisse(int a){ x=a; } public void setOrdonnee(int b){ y=b; } public void setPoint(int a,int b){ x=a; y=b; } public double distanceJusque(Point p){ int diffX=x-p.x; int diffY=y-p.y; return Math.sqrt(diffX*diffX+diffY*diffY); } } La méthode distanceJusque calcule la différence d’abscisse et d’ordonnée. Remarquez que la distance calculée est celle qui sépare le point courant du point p qui est le paramètre. Pour le point courant, on peut utiliser directement x et y qui sont disponibles au niveau de l’objet qui exécute la méthode. Pour se référer aux coordonnées de p, il faut en mentionner le nom suivi d’un point et du champ concerné. Les opérateurs sont assez classiques pour qui a déjà un peu programmé. Le chapitre suivant en parle davantage. Le calcul de la distance fait appel à la méthode sqrt de la classe Math. C’est une méthode qui assez logiquement est déclarée static. Voyons maintenant comment l’application peut créer deux points et renvoyer en résultat, la distance qui les sépare. public class Application2{ public static void main(String args[]){ Point p1=new Point(2,3); Point p2=new Point(); System.out.println("La distance entre p1 et p2 est " +p1.distanceJusque(p2)+"."); } } Chapitre 3 Java: notions de base - 21 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Deux objets de type point sont créés. Le premier point est créé via le constructeur dont la signature comprend deux paramètres, le second via le constructeur sans paramètre qui génère automatiquement le point de coordonnée (0,0). L’argument de la méthode println est une chaîne de caractères. Vous pourriez vous étonner de rencontrer l’opérateur de concaténation (+) entre des chaînes de caractères “La distance entre p1 et p2 est ” et “.” et une expression entière qui est le résultat renvoyé par la méthode distanceJusque. C’est un peu tôt pour donner une explication du bon fonctionnement de cette instruction, mais en voici tout de même une rapide qui pourrait convenir à ce stade. L’instruction ne pose pas de problème dans la mesure où tous les objets en Java héritent de la classe Object. Cette classe possède plusieurs méthodes dont une méthode toString qui convertit l’objet en chaîne de caractères. L’utilisation de l’opérateur + provoque implicitement l’exécution de cette méthode. Dans le cas où l’argument est du type primitif entier, la méthode convertit automatiquement le nombre en chaîne de caractères. C’est précisément ce qui se passe ici et qui permet la concaténation. Nous reviendrons plus en détail sur la méthode toString. Objet vs variable objet Nous venons de constater que la création d’un objet se fait exclusivement par l’intermédiaire du mot-clé new suivi du type d’objet et des paramètres éventuels pour un constructeur. Dans l’exemple précédent, deux constructeurs différents ont été utilisés. Vous devez aussi vous douter qu’il n’est pas toujours nécessaire de définir explicitement un constructeur. Si le constructeur ne doit rien initialiser, un constructeur par défaut peut convenir. Dans ce cas, sa définition est absente de la définition de la classe. Mais penchons-nous un peu sur ce qui se passe réellement lors de la création d’un objet. Lorsqu’une variable objet est déclarée, l’objet n’existe pas encore. Notez qu’il est possible de fusionner la déclaration du type de la variable objet avec la création de l’objet comme dans notre exemple mais il est aussi possible de les séparer: Point p1; // Déclaration de la variable objet p1=new Point(2,3); // Création de l’objet Observez que Java autorise évidemment les commentaires en fin de ligne (grâce à la séquence de caractères //) mais aussi d’autres types de commentaires que nous décrirons plus loin. Il convient de ne pas confondre p1 et l’objet que p1 désigne. En réalité, p1 est une référence vers un objet de type point. C’est un pointeur, une adresse en quelque sorte. A ce propos, mesurez bien la portée qu’aurait une instruction comme p1=p2. Elle signifie ni plus ni moins que “l’objet référencé par p1 est le même que celui référencé par p2". Il n’y a donc pas deux objets mais deux références au même objet31. Si une méthode effectue une modification sur l’objet en utilisant la référence p1, une autre méthode consultant ensuite l’objet en utilisant la référence p2 peut constater ces modifications. 31 Il existe en Java une méthode qui permet de cloner les objets quand c’est nécessaire. Chapitre 3 Java: notions de base - 22 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Point p1=new Point(3,6); Point p2=new Point(1,1); p2=p1; p1.setPoint(4,7); int x=p2.getOrdonnee(); System.out.println(x); La séquence d’instructions qui précède aura comme effet visible d’afficher la valeur 7. p1 et p2 sont des variables objets qui sont en réalité des références vers des objets de type Point, leurs adresses en mémoire. Lorsqu’un objet est référencé parce qu’on invoque une de ses méthodes, par exemple, celui-ci se débrouille pour trouver l’endroit où se situe le code à exécuter. Ces considérations sont extrêmement importantes pour la suite afin de garantir une programmation correcte. La référence “this” Nous avons déjà signalé que chaque objet, instance d’une classe, possède ses propres variables d’instance. Les variables x et y portent les mêmes noms quels que soient les objets pris en considération. C’est assez commode de se référer à l’abscisse d’un point en l’appelant x et à son ordonnée en l’appelant y. Si vous observez le code de la classe Point, vous constaterez que les méthodes setAbscisse, setOrdonnee et setPoint utilisent des paramètres qui portent un autre nom pour éviter la confusion entre les variables. Il est commode de les appeler également x et y, comme dans le code qui suit. La distinction est alors à faire entre les variables d’instance de l’objet courant et les paramètres en utilisant la référence this. this.x désigne la variable d’instance x de l’objet courant (this) public class Point{ private int x; private int y; public Point(){ setPoint(0,0); } public Point(int a,int b){ setPoint(a,b); } public int getAbscisse(){ return x; } public int getOrdonnee(){ Chapitre 3 Java: notions de base - 23 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 return y; } public void setAbscisse(int x){ this.x=x; } public void setOrdonnee(int y){ this.y=y; } public void setPoint(int x,int y){ setAbscisse(x); setOrdonnee(y); } public double distanceJusque(Point p){ int diffX=x-p.x; int diffY=y-p.y; return Math.sqrt(diffX*diffX+diffY*diffY); } } Le nouveau code comporte une autre modification. Dans la méthode setPoint, il n’est plus question de fixer les valeurs des variables d’instance mais de faire appel aux méthodes existantes. Il est plus naturel de décrire la modification de la coordonnée comme modifications conjointes de l’abscisse et de l’ordonnée. Le traitement des erreurs Afin de montrer l’utilité des paramètres d’une méthode main, nous allons améliorer quelque peu l’exemple qui précède. Nous introduirons aussi, à titre illustratif à ce stade, un concept qui sera développé avec un plus grand détail dans un des chapitres qui suit: le traitement des erreurs. Nous souhaitons que notre programme calcule la longueur d’un vecteur. Autrement dit, on fournit la coordonnée d’un point au niveau de la ligne de commande et le programme calcule la distance qui sépare ce point de l’origine. La commande ressemblera à la suivante java Application3 12 9. Les valeurs 12 et 9 iront remplir le tableau args aux positions 0 et 1. Les tableaux en Java comme dans beaucoup d’autres langages sont indexés à partir de 0. Ces informations peuvent donc être récupérées par le programme qui fait ses calculs habituels. Cependant, si les paramètres ne sont pas fournis, la référence à des arguments du tableau qui n’existent pas génère ce que l’on appelle une exception. Hé oui! En POO, il n’y a pas d’erreurs qui se produisent, il y a des objets qui se créent. Et les exceptions sont des objets comme les autres auxquels il est possible de s’adresser. Java, comme d’autres langages de POO, intègre le Chapitre 3 Java: notions de base - 24 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 concept des exceptions. Il fournit une solution élégante à leur gestion. Le code qui suit illustre le processus. public class Application3{ public static void main(String[] args){ try{ int x,y; x=Integer.parseInt(args[0]); y=Integer.parseInt(args[1]); Point p1=new Point(x,y); Point p2=new Point(); System.out.println("La distance entre p1 et l'origine \ est " +p1.distanceJusque(p2)+"."); } catch(ArrayIndexOutOfBoundsException ai){ System.out.println("Vous n'avez pas fourni de valeurs \ pour le point."); } } } Les arguments de la méthode main sont toujours des chaînes de caractères. Dès lors, pour pouvoir traiter les informations qu’ils contiennent comme des nombres, il va falloir les convertir. Cette conversion nécessite une méthode et donc, il faut s’adresser à une classe particulière. Cette classe, c’est la classe Integer. Les objets de cette classe sont les entiers. Dit d’une autre manière, dans cette classe, les entiers sont assimilés à des objets 32. Il est donc possible d’invoquer les méthodes de la classe. Parmi celles-ci, la méthode parseInt qui prend comme argument une chaîne de caractères et renvoie un nombre du type primitif entier. L’expression Integer.parseInt(...) signifie que la méthode est statique. C’est une méthode propre à la classe Integer qui ne demande pas qu’un objet soit créé pour pouvoir être invoquée. x et y sont du type primitif entier comme les résultats renvoyés par la méthode. Le bloc try{...} contient les instructions qui feront l’objet d’un examen plus attentif des erreurs éventuelles. Le bloc catch(...){...} contient les instructions qui gèrent l’exception qui a été générée dans ce cas. L’exception qui peut avoir été créée ici résulte de l’absence d’un ou des deux arguments dans l’appel de la méthode main. Dans ce cas, l’instruction x=Integer.parseInt(args[0]); ou l’instruction y=Integer.parseInt(args[1]); provoque la consultation d’une valeur du tableau dont l’index est hors limites. L’exception générée est du type (prédéfini) ArrayIndexOutOfBoundsException33. L’exception est récupérée par une instruction qui, dans ce cas, affiche un message d’absence de données. 32 Cette classe contient naturellement un seul champ de type entier mais plusieurs méthodes. 33 Si vous êtes à l’aise dans les explications qui ont été fournies jusqu’ici, vous pouvez déjà deviner qu’il existe des exceptions de toutes sortes. Vous pouvez aussi imaginer que toutes les classes d’exceptions héritent d’une classe générique, la classe Exception. Il n’était donc pas faux d’écrire catch(Exception ai){...} Chapitre 3 Java: notions de base - 25 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 La gestion des exceptions par Java est une façon élégante de résoudre les problèmes à l’endroit précis où ils peuvent se poser. Quand un bloc d’instructions est critique, l’utilisation du try - catch est une bonne précaution. Nous reviendrons beaucoup plus en détail sur cette opportunité de gérer les exceptions dans les chapitres qui suivent. Chapitre 3 Java: notions de base - 26 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Exercices Ex1 Vous allez devoir gérer une base de données des personnes qui fréquentent, à titre professionnel ou non, votre établissement. Imaginez la création d’une classe Personne, les champs et les méthodes qui pourraient correspondre à sa définition. Si c’est possible, procédez par composition. Par exemple, une personne possède une adresse; qu’est-ce qu’une adresse? Ne faudrait-il pas créer une autre classe? Comment cette dernière serait-elle définie? Ex2 Dans le cadre de la programmation d’un jeu de cartes, imaginez une classe CarteAJouer et une classe JeuDeCartes. Pensez à la possibilité d’utiliser des tableaux. Quels en seraient les champs? Quelles en seraient les méthodes respectives34? Ex3 Imaginez une classe Parallélogramme. Quels seraient les champs intéressants à définir? Décrivez différents constructeurs, selon les données disponibles. Prévoyez quelques méthodes d’accès et d’altération. Décrivez plusieurs méthodes pour le calcul du périmètre, de la surface, de la longueur des diagonales. Ex4 Supposez que nous travaillions dans un espace à deux dimensions. En réutilisant la classe Point que nous venons de définir, créez une classe Droite. • Quelles informations devrait-elle renfermer? • Imaginez plusieurs constructeurs possibles: deux points, un point et un coefficient angulaire, un point et une droite qui donne la direction35. • Prévoyez les cas à problème: le point se trouve sur la droite, les deux points sont identiques (mêmes coordonnées),... • Imaginez différentes méthodes: - une méthode qui vérifie si un point appartient à une droite - une méthode qui vérifie si deux droites sont parallèles - ... • Améliorez la classe Point en lui ajoutant une méthode qui vérifie si un point est dans l’alignement de deux autres points donnés. 34 Donnez leur un nom et des arguments. Pour ce qui est des traitements qu’elles doivent renfermer, contentez-vous de {...}. 35 L’imagination peut ne pas s’arrêter là. On peut construire une droite à partir d’un point et de deux droites sécantes en un autre point, etc. Chapitre 3 Java: notions de base - 27 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Attention, nous avons défini la classe Point en imaginant que les abscisses et ordonnées des points étaient toujours des nombres entiers. C’est évidemment fort contraignant et nous corrigerons cette situation par la suite. Toutefois, ne modifiez rien à cela pour l’instant car cela nous permettra de découvrir certaines choses à propos de la manière de calculer de Java. Ebauche de solution et commentaires Tâchons d’apporter un maximum de réponses au problème qui vient d’être décrit. Elaborons d’abord la classe Droite et ses constructeurs. Cet exemple illustre bien le fait que la construction d’un objet peut se faire sur base d’éléments de connaissance différents. Dans cet exemple apparaissent pour la première fois des structures de test. Leur syntaxe n’est pas très compliquée. Un chapitre leur sera consacré ainsi qu’aux structures de contrôle en général. Dans le chapitre qui suit, il sera également question des opérateurs. Nous en découvrons ici quelquesuns. Les opérateurs de comparaison: égalité (==), inégalité (!=), inférieur à (<), l’opérateur logique ET (&&), les opérateurs arithmétiques: soustraction (-), division (/), multiplication (*), concaténation (+) et l’opérateur d’affectation élargie (+=). public class Droite{ Point unPoint; int saPente; public Droite(Point p, int ca){ unPoint=p; saPente=ca; } public Droite(Point p1, Point p2){ unPoint=p1; if (p1.equals(p2)){ saPente=0; } else{ saPente = (p1.getOrdonnee() - p2.getOrdonnee()) / (p1.getAbscisse() - p2.getAbscisse()); } } public Droite(Point p, Droite d){ unPoint=p; saPente=d.saPente; } public boolean passePar(Point p){ if (unPoint != p && saPente != (unPoint.getOrdonnee() p.getOrdonnee()) / (unPoint.getAbscisse() - p.getAbscisse())){ return false; } else{ Chapitre 3 Java: notions de base - 28 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 return true; } } public String toString(){ String aux; int b= unPoint.getOrdonnee() - saPente * unPoint.getAbscisse(); if (saPente == 0){ aux = "y = " + b; } else{ aux = "y = " + saPente + "x "; if (b<0){ aux+=b; } else{ aux += "+" + b; } } return aux; } public boolean estParalleleA(Droite d){ if (saPente == d.saPente) return true; else return false; } public String intersectionAvec(Droite d1){ // Cette méthode devrait renvoyer un message indiquant // si les droites sont parallèles distinctes, confondues // ou précisant la coordonnée de leur point d'intersection. return "Inactif"; } } Le constructeur de la droite à partir de deux points s’assure que les deux points ne sont pas identiques. Dans le cas contraire, la droite est considérée comme horizontale 36. La vérification de l’égalité de deux objets ne peut se faire par l’utilisation de l’opérateur d’égalité qui se contenterait de vérifier l’égalité des références. Or les deux objets pourraient être identiques alors que les références sont différentes. C’est ce qui justifie l’emploi de la méthode equals qui est évidemment une méthode prédéfinie de la classe Object dont héritent automatiquement, rappelons-le, tous les objets. La pente est calculée on ne peut plus classiquement sur base des abscisses et ordonnées des deux points, ce qui nécessite un appel aux méthodes d’accès de la classe Point. 36 Un autre choix stratégique aurait pu être fait. Chapitre 3 Java: notions de base - 29 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Le troisième constructeur est en quelque sorte récursif. Sa définition demande un paramètre qui est une droite. Elle n’aurait pas de sens sans l’existence d’au moins un autre constructeur. Observez que tous les constructeurs ont une signature différente, ce qui est une obligation. La méthode passePar vérifie si un point appartient à une droite ou non. La valeur renvoyée est un booléen. De par la manière dont le test est effectué, cette méthode donnera souvent de mauvais résultats. Si le point donné est identique au point qui caractérise la droite, le test s’arrête là et la valeur renvoyée est true. La valeur renvoyée est false si les points sont différents et que le calcul du coefficient angulaire donne un résultat différent de la pente de la droite. Cet algorithme peut paraître correct. Il ne l’est pas dans la mesure où la division entre deux entiers fournira un résultat entier. L’application AppDroites dont le code suit permet de mettre cette erreur en évidence. Le calcul du coefficient angulaire n’est pas nécessaire si l’équation de la droite est connue sous la forme y = mx + p. La valeur de p n’est pas connue mais pourrait être calculée à la construction de la droite. Le test serait alors plus simple à exécuter. Il suffirait de vérifier que les coordonnées du point vérifient l’équation ce qui résoudrait les problèmes dans le cas où m et la coordonnée d’un point sont donnés. Il n’y aurait plus de division à effectuer. Dans le cas ou m est calculé par le constructeur, des erreurs d’arrondi pourraient encore générer des réponses fausses, mais par la faute de la x- ième décimale. Ceci montre que la structure d’une classe d’objets, de même que la définition des méthodes peuvent évoluer en fonction des problèmes rencontrés. Parfois aussi, des améliorations de la structure permettent des économies d’échelles. Voyez, à la fin de cet exercice, comment on peut envisager une évolution de la structure précédente. La méthode toString a pour objectif de renvoyer l’équation de la droite sous forme de chaîne de caractères. Cette méthode est en réalité réécrite puisqu’elle est prédéfinie dans la classe Object. Toutefois, le fait qu’elle soit réécrite est ici anecdotique. Le terme indépendant (p dans y=mx+p) est calculé et l’affichage est adapté selon que les valeurs de m et p sont positives ou négatives37. Signalons encore que l’apparition d’un nombre dans une expression contenant des chaînes de caractères provoque la conversion implicite de ces nombres en chaînes de caractères. La méthode estParalleleA ne demande pas de commentaires bien particuliers. L’application est définie dans une classe qui porte le nom de AppDroites. Dans la méthode main de cette classe, les points et les droites sont créés de manières variées. public class AppDroites{ // Que va faire afficher cette application? public static void main(String [] args){ Point p1=new Point(12,3), p2=new Point(-1,1); Droite d1=new Droite(p1,3), d2=new Droite(p1, p2), 37 Le cas où le terme indépendant est nul n’a pas été traité. L’amélioration de la méthode pour répondre à cette remarque vous est laissée à titre d’exercice. Chapitre 3 Java: notions de base - 30 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 d3=new Droite(new Point(4,2), d1); Point p4=p1; Droite d4=new Droite(p1, p4); System.out.println("Droite System.out.println("Droite System.out.println("Droite System.out.println("Droite D1: D2: D3: D4: " " " " + + + + d1); d2); d3); d4); } } Dans l’état actuel du programme, l’équation affichée pour la droite d2 ne sera pas correcte car la pente calculée sera nulle. Les corrections qui suivent ont notamment pour objectif de résoudre ce problème. Evolution des classes Point et Droite La classe correspondant à l’application reste inchangée. Dans la classe Point, les abscisse et ordonnée sont cette fois de type float de même que les valeurs renvoyées par les méthodes d’accès et les paramètres numériques des autres méthodes. public class Point{ private float x; private float y; public Point(){ setPoint(0,0); } public Point(float a,float b){ setPoint(a,b); } public float getAbscisse(){ return x; } public float getOrdonnee(){ return y; } public void setAbscisse(float x){ this.x=x; } public void setOrdonnee(float y){ this.y=y; } public void setPoint(float x,float y){ setAbscisse(x); setOrdonnee(y); Chapitre 3 Java: notions de base - 31 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 } public double distanceJusque(Point p){ float diffX=x-p.x; float diffY=y-p.y; return Math.sqrt(diffX*diffX+diffY*diffY); } public String toString(){ return "(" + x + "," + y + ")"; } } Dans la classe Droite, les changements sont plus importants. Un nouveau champ apparaît, c’est le champ termeIndependant qui est de type float comme saPente. Il est calculé par la méthode calculduTermeIndependant. Les constructeurs sont également modifiés. Les méthodes passePar et toString peuvent être réécrites autrement. public class Droite{ Point unPoint; float saPente; float termeIndependant; public Droite(Point p, float ca){ unPoint=p; saPente=ca; termeIndependant=calculDuTermeIndependant(); } public Droite(Point p1, Point p2){ unPoint=p1; if (p1.equals(p2)){ saPente=0; } else{ saPente=(p1.getOrdonnee() - p2.getOrdonnee()) / (p1.getAbscisse() - p2.getAbscisse()); } termeIndependant=calculDuTermeIndependant(); } public Droite(Point p, Droite d){ unPoint=p; saPente=d.saPente; termeIndependant=calculDuTermeIndependant(); } private float calculDuTermeIndependant(){ return unPoint.getOrdonnee() - saPente * unPoint.getAbscisse(); } Chapitre 3 Java: notions de base - 32 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 } Observez enfin qu’il est aussi pensable de créer une droite à partir de son équation. Nous vous laissons l’écriture du constructeur à titre d’exercice supplémentaire. Ex 5 Un étudiant peut s’inscrire à un maximum de 10 cours ou modules. Chaque cours est donné par un professeur titulaire ou un assistant et suit un calendrier qui indique les jours et les périodes horaires auxquels il a lieu. Imaginez une structure minimale (classes, champs et méthodes) qui décrit cette situation. Si vous deviez définir une méthode qui renseigne si un étudiant est occupé à suivre un cours, à un moment précis (date et heure), comment vous y prendriez-vous? Il n’est pas nécessaire de décrire, à ce stade, les algorithmes qui garnissent les méthodes comme la vérification de la compatibilité des horaires lors d’une inscription à un nouveau module. En revanche, il est possible de déclarer les méthodes et leurs signatures. Ebauche de solution et commentaires Ce petit exemple fait déjà percevoir la nécessité de faire précéder la construction d’une application, d’une sérieuse étude des données du système d’information et de leurs relations. C’est sans doute là que se marque, de la manière la plus évidente, la différence entre la programmation impérative et la programmation orientée objet. L’importance accordée par la POO au SI implique que les traitements (méthodes) sont imaginés par la suite en fonction des classes d’objets et ne constituent plus, comme dans la programmation impérative, la base de la programmation. L’exemple montre également, si nécessaire, que la programmation de points plus délicats faisant appel à l’algorithmique classique peut être différée en permettant de se consacrer aux “macro-traitements”. Cette programmation pourrait d’ailleurs très bien être confiée à une tierce personne. La création de certaines classes semblent s’imposer: une classe Etudiant et une classe Module. On peut aussi imaginer une classe Enseignant qui regrouperait les professeurs et leurs assistants, bien que, dans le cadre strict de ce problème, on puisse en faire l’économie. Nous l’avons toutefois décrite dans la solution. class Etudiant{ private private private private String nom; String prenom; int nombreCours=0; Module [] occupation; public Etudiant(String nom, String prenom){ this.nom=nom; this.prenom=prenom; } public boolean inscriptionAuCours(Module m){ // OK si le nombre de modules auquel l'étudiant est déjà Chapitre 3 Java: notions de base - 33 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 // inscrit est inférieur à 10 et s’il n’y a pas de problème // avec les horaires de ces autres modules // La méthode qui suit pourra éventuellement être utile. // La méthode effectue un traitement et renvoie une valeur // booléenne qui renseigne si le traitement s’est bien passé. } public boolean estOccupe(Date moment){ // Le paramètre est du type Date prédéfini. // La vérification renverra un booléen. } } class Module{ private String code, titre; private Date debut, fin; private Enseignant titulaire, assistant; public Module(String c, String t, Date d, Date f){ code=c; titre=t; debut=d; fin=f; } public Module(String c, String t, Enseignant tit, Enseignant assist, Date d, Date f){ code=c; titre=t; titulaire=tit; assistant=assist; debut=d; fin=f; } // D'autres constructeurs sont possibles (dates non encore fixées,...) public String getCode(){ return code; } public Date getDateDebut(){ return debut; } public Date getDateFin(){ return fin; } // D'autres méthodes d'accès et d'altération peuvent être définies Chapitre 3 Java: notions de base - 34 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 // en fonction des besoins réels } class Enseignant{ private String nom, prenom; private String matricule; public Enseignant(String n, String p, String m){ setName(n,p); setMatricule(m); } public void setName(String nom, String prenom){ this.nom=nom; this.prenom=prenom; } public void setMatricule(String matricule){ this.matricule=matricule; } public String getNom();{ return nom; } public String getPrenom();{ return prenom; } public String getMatricule();{ return matricule; } } L’étudiant possède un nom et un prénom et est occupé par des modules de cours. Lorsque les modules sont construits, on ne connaît pas nécessairement les enseignants mais bien leur code et leur titre. Il est possible qu’on ne connaisse pas non plus les dates et heures correspondantes. La référence this est quelque fois utilisée. Il est possible de l’éviter en imaginant d’autres noms de variables. La classe Date est, vous vous en doutez, une classe prédéfinie. Nous aurons l’occasion de découvrir plus en avant sa structure et ses méthodes. Dans le code proposé, il n’y a pas d’application qui exploite les structures mises en place. Nous nous sommes limités à créer les classes et les méthodes qui pouvaient caractériser le SI décrit. Ex 6 La somme de deux nombres entiers Chapitre 3 Java: notions de base - 35 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Cet exercice résolu est une réponse, à la fois très sérieuse et pouvant faire sourire au défi lancé par un ami lors d’une discussion intéressante à propos de la POO: “Comment programmer la somme de deux nombres entiers en OO?”. Si cette question ressemble plus à une gageure qu’à un vrai problème, elle montre toutefois que les approches impérative et objet sont assez différentes, de même d’ailleurs que leurs objectifs. Si le problème est d’obtenir la somme de ces deux nombres, sans plus, l’approche impérative fournit une réponse assez évidente. L’existence d’un opérateur d’addition et son utilisation fournissent une réponse assez immédiate. En pseudo-code: lire a, b somme w a + b écrire somme Voici ce que cela pourrait donner en Java: public class AdditionDEntiersImperatif{ public static void main(String [] args){ int n1 = 12; int n2 = 54; int somme = n1 + n2; System.out.println("La somme des nombres n1 et n2 est " + somme + "."); } } Mais en approche objet, le point de départ est essentiellement une description du système d’information, indépendamment des traitements que l’on souhaite développer. On admettra donc qu’il y a des nombres entiers (une classe de) dont un des champs de données sera inévitablement la valeur. class Entier{ private int valeur; On pourrait d’ailleurs y ajouter un constructeur élémentaire. public Entier(int v){ valeur=v; } Une réflexion ultérieure sur la nature des messages à adresser à un nombre conduira sans doute à admettre qu’un nombre devrait pouvoir s’additionner à un autre nombre. public Entier plus(Entier e){ return new Entier(valeur + e.valeur); } Chapitre 3 Java: notions de base - 36 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 public int getValeur(){ return valeur; } } La méthode plus renvoie une référence de type Entier. Il s’agit d’un objet sans nom dont le champ valeur contiendra la somme (au sens propre du terme) des contenus des champs valeur des deux nombres de type Entier. La méthode getValeur est nécessaire compte tenu du type d’accès au champ valeur (private). Cette réflexion peut paraître un peu lourde mais il ne faut pas perdre de vue que d’autres éléments peuvent caractériser un nombre et que d’autres traitements utiles peuvent apparaître progressivement. On pourrait, par exemple, demander à un nombre de s’afficher en toutes lettres, de s’écrire en chiffres romains,... Pour compléter la réponse, on peut écrire une petite application du genre de celle qui suit. public class AdditionDEntiersObjet{ public static void main(String [] args){ Entier n1 = new Entier(12); Entier n2 = new Entier(54); System.out.println("La somme des nombres n1 et n2 est " + n1.plus(n2).getValeur()); } } Voici donc une version complète de l’addition de deux nombres entiers en approche objet. On devine que le terrain est prêt pour des extensions et des améliorations. Notez encore que des classes enveloppant les types primitifs (wrapping classes) sont prédéfinies en Java. Elles portent les noms de ces types en commençant par une majuscule évidemment: Boolean, Float,... Deux exceptions à cette homonymie: le type int est emballé dans la classe Integer et le type char dans le classe Character. public class AdditionDEntiers{ public static void main(String [] args){ Entier n1 = new Entier(12); Entier n2 = new Entier(54); System.out.println("La somme des nombres n1 et n2 est " + n1.plus(n2).getValeur()); } } class Entier{ private int valeur; public Entier(int v){ valeur=v; Chapitre 3 Java: notions de base - 37 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 } public Entier plus(Entier e){ return new Entier(valeur + e.valeur); } public int getValeur(){ return valeur; } } Ex 7 Définissez une classe Heure capable d’instancier tous les moments de la journée. Imaginez divers constructeurs sur base de données différentes: • • • • • heure minute (0-24) heure minute seconde (0-24) heure minute (0-12) heure minute seconde (0-12) une valeur entière Imaginez diverses méthodes: • • • • renvoi de l’heure, de la minute, de la seconde,... sur base de la valeur et l’inverse calcul de la durée entre deux informations de type Heure ajout d’une durée à une Heure pour obtenir une autre Heure ... Ecrivez une classe d’application. Chapitre 3 Java: notions de base - 38 - Programmer avec des objets Chapitre 4 Etienne Vandeput ©CeFIS 2002 Quelques compléments utiles Le passage des paramètres en Java Les deux principales manières de passer des paramètres en programmation sont le passage par valeur et le passage par référence. Pour éviter toute ambiguïté à ce propos, signalons qu’un passage par valeur fournit à la méthode, la fonction, la procédure, une copie de la valeur du paramètre . Il est donc impossible que la valeur effective du paramètre soit modifiée par la méthode, la fonction, la procédure puisque les traitements sont effectués sur une copie. Le passage par référence, au contraire, fournit l’emplacement de la variable concernée, ce qui permet à la méthode, la fonction, la procédure de modifier cette variable. Que se passe-t-il en Java? Pour bien le comprendre, il faut assimiler deux points importants. Premier point En Java, (et contrairement à Pascal ou C++) le passage par valeur est le seul existant. Il n’y a pas de passage par référence. Supposons qu’il existe une méthode public static void auCarre(int x){ x = x * x; } Une séquence d’instructions telle int x=10; auCarre(x); System.out.println(x); fournira à l’affichage la valeur 10. L’explication est la suivante: la méthode auCarre reçoit une copie de la valeur du paramètre formel x. La valeur de cette copie est élevée au carré, puis la méthode se termine détruisant la copie. En voici une illustration plus parlante: public class ParValeur{ public static void main(String [] args){ int x=10; auCarre(x); System.out.println("APRES SORTIE DE LA METHODE: x vaut " + x); Chapitre 4 Quelques compléments utiles - 39 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 } public static void auCarre(int x){ System.out.println("DEBUT DE LA METHODE: x vaut " + x); x = x * x; System.out.println("FIN DE LA METHODE: x vaut " + x); } } Affichage: 10 FIN DE LA METHODE: x vaut 100 DEBUT DE LA METHODE: x vaut APRES SORTIE DE LA METHODE: x vaut 10 Second point Les variables objets en Java sont des adresses mémoires d’objets. On dit d’ailleurs aussi que les variables objets contiennent des références (aïe!38) à des objets. Ceci n’est pas fait pour faciliter la compréhension. Vous comprendrez toutefois, en combinant les deux choses, que lorsque des objets sont passés en paramètres, ce sont les adresses de ces objets qui sont passées et que le passage de ces adresses se fait par valeur. Une méthode ne peut donc modifier l’adresse d’un objet puisque ce qui est fourni à la méthode est une copie de la valeur de l’adresse, copie qui disparaîtra à la fin de la méthode. Dès lors, la méthode disposant d’une copie de l’adresse des objets fournis en paramètres peut, si elle y a accès, modifier les champs de données de ces derniers. L’application qui suit montre ce qui est possible et ce qui ne l’est pas. On observe notamment que le passage de paramètres objets permet d’agir sur l’état de ces objets mais ne permet pas d’en modifier la référence. La méthode permute ne fonctionne donc pas comme on pourrait s’y attendre. Pour les besoins de la cause, la classe Point a été modifiée et transformée en une classe Point2 dont voici la définition. Deux nouvelles méthodes y apparaissent, une méthode qui renvoie le symétrique d’un point et une méthode statique (méthode de classe) qui est sensée permuter deux points. C’est cette dernière qui pose problème car elle tente de modifier (permuter) les références des objets, ce qui marche bien à l’intérieur de la méthode mais est sans effet en dehors. public class Point2{ private float x; private float y; public Point2(){ setPoint(0,0); 38 C’est de cette confusion que naissent des erreurs fréquentes dans la littérature faisant dire à tort qu’en Java, les paramètres sont passés par référence. C’est une ineptie. Chapitre 4 Quelques compléments utiles - 40 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 } public Point2(float a,float b){ setPoint(a,b); } public float getAbscisse(){ return x; } public float getOrdonnee(){ return y; } public void setAbscisse(float x){ this.x=x; } public void setOrdonnee(float y){ this.y=y; } public void setPoint(float x,float y){ setAbscisse(x); setOrdonnee(y); } public String toString(){ return "(" + x + "," + y + ")"; } public Point2 symetrique(){ Point2 sym = new Point2(-x,-y); return sym; } public static void permute(Point2 p1, Point2 p2){ Point2 aux = p2; p2 = p1; p1 = aux; // Les références aux deux objets ont été permutées à l'intérieur de // la méthode. System.out.println("APRES PERMUTATION ET AVANT LA SORTIE DE \ LA METHODE..."); System.out.println("p1: " + p1); System.out.println("p2: " + p2); System.out.println(); } } Chapitre 4 Quelques compléments utiles - 41 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Voici l’application qui met en oeuvre cette nouvelle classe et met en évidence le passage par valeur des paramètres. public class AppPoint2{ public static void main(String [] args){ Point2 p1=new Point2(3,-1); Point2 p2=new Point2(2,2); // L'invocation de la méthode "symetrique" sur l'objet p1 renvoie un // objet anonyme qui est "imprimé ". System.out.println("LE SYMETRIQUE DE " + p1 + " EST " + p1.symetrique()); System.out.println(); System.out.println("p1: " + p1); System.out.println("p2: " + p2); System.out.println(); // La permutation des deux objets est une permutation de copies de // ces objets. Point2.permute(p1,p2); // Lorsque la méthode se termine, ces copies disparaissent. // La permutation est donc sans effets apparents. System.out.println("APRES PERMUTATION ET SORTIE DE LA \ METHODE..."); System.out.println("p1: " + p1); System.out.println("p2: " + p2); System.out.println(); // Il est pourtant possible de modifier de manière définitive la // coordonnée d'un point par l'intermédiaire d'une méthode. p1.setPoint(p2.getAbscisse(),p2.getOrdonnee()); System.out.println("APRES MODIFICATION..."); System.out.println("p1: " + p1); System.out.println("p2: " + p2); System.out.println(); } } La classe String Chapitre 4 Quelques compléments utiles - 42 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Si vous êtes habitués des langages de programmation, vous aurez sans doute été étonnés de ne pas trouver dans les types primitifs un type “chaîne de caractères”. Ce type n’existe pas en Java, toutefois, il existe une classe String dans la bibliothèque standard de Java. La manière habituelle de travailler est en effet de regrouper les classes dans des librairies et d’importer celles-ci lorsqu’elles sont nécessaires. Les librairies dont nous parlerons davantage plus tard sont organisées en arborescence et les chemins sont décrits un peu comme les répertoires mais en utilisant le point. La classe String (remarquez la majuscule) fait partie de la librairie ou du package java.lang. Des déclarations telles celles qui suivent sont valides sans que des instructions d’importation de ce package ne soient nécessaires. String nom; String societe=”CeFIS”; String message=””; La définition de cette classe comprend des champs et des méthodes que le programmeur peut évidemment utiliser à souhait. En voici quelques exemples. String message = ”Bienvenue au cours Java”; String mot = message.substring(13,18)39; int l = message.length(); char c = mot.charAt(3); int s = l + message.indexOf(“Java”); La plupart de ces instructions pourraient être détaillées pour séparer la déclaration des variables de leurs affectations. Le langage offre cette souplesse de pouvoir réaliser les deux choses en une seule instruction. int l, s; l = message.length(); s = message.indexOf(“Java”); s += l; Cette dernière instruction est équivalente à s = s + l; Comme avec beaucoup d’autres langages, la concaténation des chaînes de caractères s’effectue grâce à l’opérateur +. 39 La méthode substring est particulière. Le premier argument est l’index du premier caractère à extraire (en commençant la numérotation par 0), le second est celui du premier caractère à ne pas extraire. Chapitre 4 Quelques compléments utiles - 43 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 La séquence suivante String message = “Bienvenue au cours Java”; String auteur = “Etienne Vandeput”; String entete = message + ” (“ + auteur + “)”; System.out.println(entete); provoquera l’affichage: Bienvenue au cours Java (Etienne Vandeput) De plus, toute concaténation d’une chaîne de caractères avec un élément d’un autre type provoque une conversion automatique de ce type. String marque=”Opel”; String type=”Vectra” int cylindree=2000; System.out.println(marque + “ ” + type + “ ” + cylindree); provoquera l’affichage: Opel Vectra 2000 La méthode toString() Que se passe-t-il, dès lors, si le type de l’élément de la concaténation n’est pas un type primitif mais un type d’objet? Tous les classes héritant de la classe Object, et cette dernière possédant une méthode toString, tout objet peut donc être affiché sous forme d’une chaîne de caractères40. Nous avons déjà ajouté à la définition de la classe Point: public String toString(){ return "(" + x + "," + y + ")"; } De la sorte, les instructions Point p1=new Point(3,4); System.out.println(“La coordonnée du point est “ + p1 + “.”); provoquent l’affichage du message La coordonnée du point est (3,4). 40 Par défaut de réécriture au sein d’une nouvelle classe, la méthode toString() renvoie le nom de la classe et une adresse en mémoire. Il est courant de redéfinir cette méthode au sein des classes dont on aimerait afficher les objets sous forme de chaînes de caractères. Cette remarque anticipe sur les mécanismes d’héritage et de polymorphisme dont nous parlerons dans les chapitres qui viennent. Chapitre 4 Quelques compléments utiles - 44 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 La chaîne renvoyée par la méthode toString d’un objet est laissée à l’appréciation du programmeur. Pour une droite, nous avons choisi de fournir une équation. Pour une personne, nous pourrions fournir une partie de ses coordonnées41. Les méthodes statiques et les variables de classe Les exemples qui ont été pris jusqu’ici ont montré des classes dont les instances sont également structurées en champs et en méthodes. La classe A permet de créer des objets de type A. Sa définition pourrait ressembler à ce qui suit: class A{ B champ1; C champ2; ... public A(){...} // constructeur d’objet de type A public D methode1(){...} public void methode2(...){...} ... } class Application{ public static void main(String[] args){ ... A objetDeTypeA = new A(); ... } L’objet créé possède des champs nommés champ1 (de type B), champ2 (de type C),... La méthode nommée methode1renvoie une référence vers un objet de type D alors que la méthode nommée methode2 ne renvoie rien. La première méthode ne prend pas de paramètres alors que ceux qui sont pris par la deuxième méthode ne sont pas détaillés. La classe Application possède une méthode main dont une des instructions crée un objet de type A. Voici, par exemple, une manière d’invoquer une des méthodes: ... D objetDeTypeD = new D(); // Utilisation du constructeur de la classe D objetDeTypeD = objetDeTypeA.methode1(); ... Les méthodes sont invoquées sur des objets précédemment créés. ObjetdeTypeA est une référence vers un objet de type A. L’expression objetDeTypeA.methode1() a donc du sens. Cette méthode renvoie une référence vers un objet de type D. L’affectation objetDeTypeD = objetDeTypeA.methode1() est donc correcte. 41 Pour que la chaîne renvoyée s’affiche sur plusieurs lignes, utilisez la séquence \n à l’intérieur de la chaîne. Chapitre 4 Quelques compléments utiles - 45 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Voilà comment fonctionne donc de manière dynamique un programme Java. La méthode principale d’une classe s’exécute provocant la création de nouveaux objets ayant les mêmes champs de données contenant des valeurs propres et dont il sera possible d’invoquer les méthodes. Mais tout ne fonctionne pas complètement de cette manière. Faisons les observations suivantes: • il est possible que certains champs contiennent une information qui caractérise la classe plutôt que chacun de ses objets; • il est possible que certaines méthodes ne demandent pas que des objets soient créés pour agir. Illustrons d’un exemple chacun de ces deux cas. Vous avez décidé de mettre de l’ordre dans votre collection de cassettes vidéos. Outre certains renseignements utiles tels le titre du film, la date d’enregistrement, etc., vous souhaiterez sans doute les numéroter. Le numéro d’identification doit être unique et vous n’avez aucune envie de retenir les numéros que vous avez déjà utilisés. La classe peut conserver cette information pour vous. Lors de la construction d’un nouvel objet Cassette, l’information concernant le prochain numéro à utiliser (et qui est une information propre à la classe) sera exploitée pour éviter un double emploi. On dira que le champ contenant cette information est statique car l’information qu’il contient ne dépend pas d’un objet particulier mais est lié à la classe. Voici à quoi pourrait ressembler une utilisation de cette information: Dans la définition de la classe Cassette, on trouvera: public static int prochainNumero = 1; // champ statique de la classe Cassette public int numero; // champ dynamique de type primitif entier public String Titre; public Date enregistreLe; ... Dans la partie de code qui crée un nouvel objet Cassette, on trouvera: Cassette c = new Cassette(); // création d’un objet c de type Cassette c.numero = Cassette.prochainNumero++ // affectation du numéro à la cassette sur base du dernier numéro utilisé Quelques commentaires à ce propos: • la variable prochainNumero est déclarée statique ce qui veut dire qu’elle caractérise la classe à un moment donné du cycle d’exécution et ce qui ne veut pas dire qu’il est impossible de la modifier • il existe une classe Date prédéfinie Chapitre 4 Quelques compléments utiles - 46 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 • les champs sont déclarés public dans cet exemple car on suppose que les affectations dont ils sont les objets se font à l’extérieur de la classe (alors que les constructeurs pourraient s’en charger) • c.numero est une variable d’instance du type primitif entier alors que prochainNumero est une variable de classe de même type, donc l’affectation a du sens L’écriture a = b++ cache une double instruction. La valeur de b est d’abord affectée à a puis est augmentée d’une unité. Il est aussi possible d’écrire a = ++b qui augmente b avant d’en affecter la valeur à a. Il n’y a pas que les champs qui puissent être déclarés statiques. Les méthodes peuvent l’être aussi. Comme c’était le cas pour les champs, il suffit qu’elles soient propres à la classe et non à un objet particulier. Vous avez déjà rencontré un exemple de méthode statique ou méthode de classe, c’est la méthode sqrt de la classe prédéfinie Math. Cette classe n’a pas d’instances possibles, mais comprend un certain nombre de fonctionnalités intéressantes. Ainsi, pour faire effectuer un calcul un peu particulier, on peut s’adresser à la classe Math42 et invoquer une méthode en lui passant les nombres en arguments. La classe Math contient notamment la déclaration suivante: static double sqrt(double a){...} qui définit la méthode sqrt comme renvoyant un nombre du type primitif double sur base de la fourniture d’un autre nombre de type double. Comme pour référencer un champ statique, l’invocation d’une méthode statique se fait auprès de la classe et non d’un objet particulier. return Math.sqrt(diffX*diffX+diffY*diffY); L’utilisateur (le programmeur dans ce cas) peut définir lui-même des méthodes de classes qu’il considère comme statiques. Par exemple, une méthode qui fournirait le prochain numéro à utiliser pour une cassette serait statique en ce qu’elle ne concerne qu’un champ statique et n’a pas besoin d’informations concernant un quelconque objet. Il y a une différence fondamentale entre les méthodes statiques et les méthodes normales qui sont associées à un processus dit de liaison dynamique. Cette différence sera mise ne valeur lors de l’étude des mécanismes d’héritage et de polymorphisme. Nous verrons que lorsqu’une méthode est invoquée sur un objet (qui en est implicitement un paramètre), le compilateur doit déterminer la définition idéale 42 Extrait de la documentation Java: “The class Math contains methods for performing basic numeric operations such as the elementary exponential, logarithm, square root, and trigonometric functions.” Chapitre 4 Quelques compléments utiles - 47 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 de la méthode à appliquer. Dans une classe, une méthode peut en effet être définie de plusieurs manières et est même fréquemment redéfinie dans ses sous-classes. Pour ce qui est des méthodes statiques, cette résolution n’a pas de raison d’être. Les champs et les méthodes déclarés finals Il arrive que certains champs d’une classe doivent être considérés comme inaltérables. C’est vrai pour certains champs static. Un exemple évident est la définition d’une constante. Au fond, une constante n’est rien d’autre qu’une variable qui ne changera plus dans la suite de l’exécution du programme. C’est vrai aussi pour les variables d’instance. Lorsqu’un objet est créé, le programmeur peut souhaiter que la valeur de certains champs ne soit plus modifiée pendant toute la durée de vie de cet objet: le nom d’une personne, son numéro de matricule, le numéro de TVA d’une entreprise... Le mot-clé est le mot final. En voici des utilisations possibles: class Application{ private static final int MAXIMUM = 1000; ... } Il s’agit ici d’une déclaration qui équivaut à celle d’une constante. Le mot-clé static précisant que la variable est une variable de classe. class Personne{ private final String nom; ... Personne(String n){ nom=n; ... } ... } Observez que la variable déclarée final doit être initialisée au plus tard par le constructeur. Si ce n’est pas le cas, le compilateur détectera une erreur. Le mécanisme d’héritage, dont nous parlerons dans un des chapitres qui suit, va permettre de redéfinir, au sein de sous-classes, des méthodes qui avaient été définies à un niveau supérieur. Dans certains cas, on souhaitera soit empêcher cet héritage, soit empêcher la re-définition de certaines méthodes. On empêche l’héritage en déclarant la classe final. De la sorte, aucune classe ne peut hériter d’elle, c’est-à-dire en étendre la définition43. On fait de même pour les méthodes qui ne peuvent être réécrites. 43 L’extension de la définition d’une classe sera examinée dans le détail. Toutefois, il me paraissait intéressant de mentionner dès maintenant la possibilité de rendre les classes et les Chapitre 4 Quelques compléments utiles - 48 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Signalons encore que les méthodes d’une classe déclarée final sont automatiquement final mais ce n’est pas le cas des champs de la classe. Les opérateurs Ils sont assez classiques pour qui a déjà fait un peu de programmation. Les opérations se font, bien entendu, avec une certaine priorité. De plus, les conversions implicites sont nombreuses et il convient de les connaître au risque d’aller au devant de certaines surprises. En voici les principaux. Pour le reste, reportez-vous à la documentation. Les opérateurs arithmétiques Pour rappel, il existe six types primitifs numériques: quatre entiers et deux flottants. Par ordre croissant du nombre d’octets nécessaires, on note le type byte (1), le type short (2), le type int (4), le type long (8) chez les entiers, le type float (4) et le type double (8) chez les nombres en virgule flottante. Nous avons déjà remarqué que l’utilisation de l’opérateur + dans une expression contenant une chaîne de caractères entraînait la conversion des autres informations, quel que soit leur type, en chaînes de caractères, même lorsqu’il s’agit d’objets. Il se passe des choses semblables entre les types primitifs numériques. Par exemple, lorsque l’opérateur d’addition (+) est utilisé entre des nombres qui sont du type byte ou short, ceux-ci sont promus au rang de int. Considérez les déclarations suivantes: byte b1, b2; short s; int i; Les expressions b1+3, b1+b2, s+1, s+i fournissent toutes un résultat de type int. Attention! s++ ne pose pas de problème dans la mesure où l’incrémentation s’applique directement à la variable s qui reste de type short. Si les types long, float ou double interviennent, ils provoquent aussi des conversions implicites44. Notez encore que 2. est de type double alors que 2.f est de type float. Considérez les déclarations: byte b1, b2; short s; int i; long l; float f; double d; méthodes inaltérables. 44 Si un nombre entier est additionné à un nombre en virgule flottante, le résultat sera converti en nombre à virgule flottante. Chapitre 4 Quelques compléments utiles - 49 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 L’expression s*l fournit un résultat de type long. L’expression l+2. est de type double (car 2. l’est). b1*l*2./f est de type double alors que b1*l*2.f/d est de type float. Les opérateurs arithmétiques classiques sont l’addition, la soustraction, la multiplication, la division et le reste de la division. Leurs symboles respectifs sont: +, -, *, / et %. La division d’un entier par un entier fournit un résultat entier. int x=10, y=12; int z=5*x; System.out.println(“x : y = “ + x/y); System.out.println(“z : y = “ + z/y); L’affichage donnera: x:y=0 z:y=4 Les variables de type char n’échappent pas aux conversions implicites. Lorsqu’elles sont employées dans des expressions utilisant les opérateurs arithmétiques, leurs valeurs sont converties dans le type int. Considérez les déclarations: char c1=’a’, c2=’d’,c3=90; short s=100; Les expressions c1+c2, c1*c3, c2-c1, c2*s sont toutes de type int. Leurs valeurs respectives sont 197, 8.730, 3 et 10.000. La variable c3 est bien de type char (le caractère dont le code est 90). Les opérateurs logiques et de comparaison On retrouve les opérateurs classiques (conjonction, disjonction et négation) auxquels il convient d’ajouter les opérateurs logiques optimisés (à court-circuiter). Les symboles sont les suivants: opérateurs classiques optimisés ET & && OU | || OU exclusif ^ Chapitre 4 Quelques compléments utiles - 50 - Programmer avec des objets NON Etienne Vandeput ©CeFIS 2002 ~ Lorsqu’un opérateur optimisé est rencontré, le second opérande n’est pas examiné si ce n’est pas nécessaire. int i=10, j=50; if (i>12 && j<100) System.out.println(“Condition remplie”); Dans ce cas, la première comparaison échouant, la seconde expression n’est pas examinée. Il en est de même dans le cas suivant: if (i<12 || j<100) System.out.println(“Condition remplie”); Cette opportunité est intéressante lorsque les opérateurs d’incrémentation sont utilisés. int i=10, j=50; if (i>12 & j++<100) System.out.println(“Condition remplie avec j = “+j); else System.out.println(“Condition non remplie avec j = “+j); Le texte affiché sera Condition non remplie avec j = 51. int i=10, j=50; if (i>12 && j++<100) System.out.println(“Condition remplie avec j = “+j); else System.out.println(“Condition non remplie avec j = “+j); Le texte affiché sera Condition remplie avec j = 50. Les principaux opérateurs relationnels sont l’opérateur d’égalité (==) et de différence (!=) ainsi que les autres opérateurs classiques de comparaison tels <=, >=, <, >. Les opérateurs d’incrémentation, de décrémentation et d’affectation élargie Si vous êtes habitués des langages C et C++, l’existence de ces opérateurs ne devrait pas vous étonner outre mesure. L’opération qui consiste à ajouter ou retrancher une unité à une variable 45 étant très courante en programmation, ces opérateurs peuvent s’avérer utiles. On distingue les formes “préfixe” et “suffixe” suivant que l’incrémentation s’effectue en priorité ou non. Quel est celui des deux messages qui sera imprimé? int i=5; if ( i++>5) System.out.println(“Le test précède l’incrémentation”); if ( ++i>5) System.out.println(“L’incrémentation précède le test”); 45 Ces opérateurs doivent obligatoirement s’appliquer à des variables. Chapitre 4 Quelques compléments utiles - 51 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 C’est le second car lors du premier test, la condition est d’abord évaluée à false avant que i ne soit augmenté d’une unité. Ces opérateurs peuvent être utilisés dans diverses expressions. int a = 2, b = 5; int i = a * ++b System.out.println(“i vaut “ + i + “.”); L’affichage donnera i vaut 12. int i = a * b++ aurait provoqué l’affichage de i vaut 10. tout en observant que la valeur de b, à la fin de l’exécution de l’instruction, est également 6. Les opérateurs arithmétiques peuvent être associés à l’affectation pour une opération dite d’affectation élargie. C’est, en fait, un raccourci d’écriture. Pour doubler la valeur d’une variable, par exemple, on utilisera l’écriture a *= 2. Elle est équivalente à a = a * 2. Les principaux opérateurs de cette catégorie sont +=, -=, *=, /= et %=. L’opérateur conditionnel Il existe en Java un opérateur ternaire qui permet de préciser une condition, un traitement à effectuer si cette condition est vérifiée et le traitement à effectuer sinon. Les trois opérandes sont séparés par les symboles ? et :. L’expression a<b ? a : b fournit le minimum de a et b. min = a<b ? a : b; Les commentaires Nous avons déjà évoqué la possibilité de mettre des commentaires dans les programmes Java. Cette nécessité se faisant rapidement sentir, précisons qu’il y a trois manières de placer des commentaires dans du code Java. Vous connaissez la première qui consiste à utiliser une double barre oblique indiquant au compilateur que tout ce qui suit jusqu’à la fin de la ligne est un commentaire. Chapitre 4 Quelques compléments utiles - 52 - Programmer avec des objets Point p = new Point(3,8); Etienne Vandeput ©CeFIS 2002 // Construction du point p Il est parfois nécessaire d’écrire des commentaires plus abondants et de les répartir sur plusieurs lignes. Dans ce cas, on utilise les séquences /* et */ pour débuter et clôturer le bloc concerné. /* Ce programme permet de... Copyright... Février 2002 */ Il existe une troisième manière de réaliser des commentaires. Elle est liée à la possibilité d’employer un outil de génération automatique de documentation (javadoc) sous forme de documents HTML. On utilise dans ce cas les séquences de symboles /** et */. Les tableaux Nous avons déjà évoqué la possibilité de définir des tableaux en Java. Examinons cette possibilité dans plus de détails. Les tableaux sont déclarés en précisant leur nom et le type de données qu’ils sont appelés à contenir. La dimension du tableau est à préciser au moment de sa création et non lors de sa déclaration. int[] t; ou int t[]; sont des déclarations équivalentes. La première est cependant plus parlante (t est un tableau d’entiers). Tous les données d’un tableau sont évidemment du même type. Chaque élément est accédé en utilisant un index dont la première valeur est 0. public class Tableau{ public static void main(String[] args){ Point[] triplet = new Point[5]; for (int i = 0; i<5; i++){ triplet[i] = new Point(i, i*i); System.out.println("t[" + i + "] = " + triplet[i]); } } } Cette application fait appel à la classe Point que nous avions améliorée en ajoutant une re-définition de la méthode toString(). Chapitre 4 Quelques compléments utiles - 53 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 L’affichage donne: t[0] = (0,0) t[1] = (1,1) t[2] = (2,4) t[3] = (3,9) t[4] = (4,16) Dans cet exemple, la variable triplet désigne un tableau de quatre objets de type Point. Ce tableau est ensuite initialisé en utilisant de manière répétitive un des constructeurs de cette classe Point. Comme triplet[i] désigne un objet de type Point, sa concaténation entraîne l’utilisation implicite de la méthode toString() de cette classe qui affiche un objet de type Point en fournissant sa coordonnée. Nous découvrons aussi une des structures de contrôle des actions bien connue de ceux qui pratiquent la programmation impérative. Nous reviendrons sur la syntaxe de cette structure qui est facile à deviner. Entrées et sorties Nous nous sommes contentés, dans les exemples que nous avons examinés, de “hardcoder46” les données ou de les fournir en paramètres à la ligne de commande. Pour ce qui est des résultats à produire, nous nous sommes satisfaits de la méthode println de System.out. Ces choix se justifient par la nécessité, à mon sens, de se focaliser sur l’essentiel. Maintenant que nous avons eu l’occasion d’illustrer le fonctionnement d’une application à travers la vie (naissance et mort47) d’objets, nous pouvons nous permettre d’indiquer une voie plus agréable pour fournir des données de manière interactive. Pour cela, nous aurons besoin d’une classe qualifiée de classe graphique car elle caractérise une boîte de dialogue (optionpane en anglais) de l’interface graphique de communication de l’utilisateur avec l’application. Cette classe s’appelle JOptionPane et fait partie du paquetage 48 de classes appelé javax.swing. En règle générale, et à quelques rares exceptions près, pour qu’un programme puisse disposer des classes prédéfinies auxquelles il fait appel, il faut utiliser une instruction d’importation des paquetages auxquels elles appartiennent. Cette instruction indique au compilateur quelles classes il doit charger en mémoire afin que le programme fonctionnen correctement. 46 Pour tester un programme ou une application, il est souvent plus simple de fournir les données (qui correspondent généralement à des batteries de tests) via des instructions d’affectation. 47 Nous n’avons pas encore parlé de la manière dont mouraient les objets 48 Nous traiterons des paquetages à la fin du chapitre suivant. Sachez simplement que ces paquetages correspondent à des librairies de classes prédéfinies. Chapitre 4 Quelques compléments utiles - 54 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 import javax.swing.*; public class EntreeDeDonnees{ public static void main(String [] args){ String nom = JOptionPane.showInputDialog("Quel est votre prénom?"); System.out.println("Bienvenue au cours Java, " + nom + "."); System.exit(0); } } La première instruction du fichier est l’instruction d’importation pour le compilateur. La méthode showInputDialog est une méthode statique qui prend une information de type Object comme paramètre (le message à fournir) et renvoie une information de type String. La documentation des classes vous permettra d’observer que cette méthode possède plusieurs signatures. La dernière instruction active la méthode statique exit de la classe System 49 qui prend comme paramètre un nombre de type primitif int. La valeur est renvoyée au niveau du shell de commande qui peut l’exploiter pour faire autre chose (enchaîner une application en fonction de cette valeur). La valeur 0 indique conventionnellement que le programme s’est terminé correctement. Signalons encore que la boîte de dialogue simplifiée contient un bouton OK et un bouton Annuler. Un clic sur ce dernier bouton correspond à une absence de référence au retour. Le message affiché sera donc Bienvenue au cours Java, null. Bien entendu, dans le cadre d’une programmation correcte, cet événement doit être récupéré. Les conversions automatiques et le transtypage Pour terminer ce chapitre, je vous propose d’aborder une première fois un sujet un peu délicat, celui du transtypage. Pour introduire le sujet, rappelez-vous que l’emploi de certains opérateurs provoque des conversions de types, par le fait de la recherche d’un type commun. Lorsque vous voulez additionner une valeur int à une valeur double, par exemple, la valeur int est d’abord convertie en double. Il est assez simple d’imaginer le schéma des promotions de types possibles. Un byte est converti au besoin en short ou en tout ce dont un short peut être converti. Il en est de même pour le short qui est converti en int, pour l’int qui est converti en long, le long en float et le float en double. Il faut mentionner que trois conversions peuvent poser un problème de précision: la conversion d’un int en float et celle d’un long en float ou en double. Ces imprécisions sont dues au nombre d’octets utilisés pour les différentes représentations. 49 Rappelons que la classe System fait partie du paquetage java.lang qui est importé automatiquement. Chapitre 4 Quelques compléments utiles - 55 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Ajoutons qu’un char peut également être promu en int. En dehors de ces conversions implicites, le programmeur peut souhaiter l’inverse, à savoir, demander au programme d’utiliser une information de type double comme une information de type int. Cette demande ne se fait évidemment pas sans perte d’information. double x = 2.35, y = 1.56; int z = (int)x // x se mue de double en int et perd sa partie fractionnaire La démarche est possible tous les types d’objets. Un objet de type plus spécialisé se muant en objet de type moins spécialisé. Elle est connue en anglais sous le nom de casting (mue). Comme il est un peu tôt pour en parler davantage, l’opération de transtypage sera rediscutée dans le chapitre suivant car c’est le mécanisme d’héritage nous conduira parfois à l’utiliser. Exercices L’apprentissage d’un langage ne peut se faire dans l’ignorance totale des primitives de ce langage. Evidemment, avec des langages de la trempe de Java, on peut prétendre que les primitives sont extrêmement nombreuses, vu le nombre considérable de classes et de méthodes prédéfinies. On peut aussi apprendre à programmer avec ce langage dans l’ignorance de la majorité de ces classes. Ce serait du snobisme de se passer d’une classe comme la classe String, par exemple. Nous adopterons donc une position intermédiaire en considérant que la consultation régulière de la documentation, pour une telle classe (quels en sont les champs, les méthodes, les constructeurs et leurs signatures) est une occasion de découverte autonome du langage. Ex 1 Comment définiriez-vous une classe Cercle? Quels en seraient les champs et les méthodes? Pensez à ce que nous avons déjà défini. Imaginez de réécrire la méthode toString() de la classe Object dont hérite implicitement la classe Cercle. Ex 2 Ecrivez une application qui crée des objets de type Personne. Chaque personne a au moins un nom et un (ou plusieurs) prénom(s), une adresse et éventuellement un numéro de téléphone. Imaginez des méthodes d’accès et d’altération uniquement pour les champs qui le nécessitent. Ex 3 Améliorez l’application précédente en ajoutant à chaque personne un matricule unique et inaltérable. Ex 4 Réécrivez la méthode toString() de la classe Droite du chapitre précédent pour qu’elle renvoie un système d’équations paramétriques de la droite. Ex 5 Ecrivez une méthode de la classe Cercle qui détermine si une droite donnée lui est tangente. Ex 6 Utilisez la classe Point que vous pouvez améliorer en donnant la possibilité de nommer les points. Modifiez la méthode toString pour que la chaîne renvoyée ressemble à “Le point b a pour coordonnée (3,-2)”. Définissez une classe Segment. Quels seront ses constructeurs? Imaginez une Chapitre 4 Quelques compléments utiles - 56 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 méthode pour obtenir le milieu d’un segment, d’autres pour déplacer l’origine, l’extrémité, pour renvoyer sa longueur. Ex 7 Imaginez une méthode toString() pour la classe personne qui renvoie une chaîne semblable à celle-ci: Vandeput Etienne P F M G mat 123456 Ex 8 Créez la classe CarteAJouer évoquée dans le chapitre précédent. Chaque instance aura une couleur et une valeur (exemples: carreau et trois, trèfle et valet,...). Prévoyez un constructeur et des méthodes d’accès. Créez aussi une classe JeuDeCartes qui contienne une information concernant le nombre de cartes du jeu. Une instance de cette classe sera considérée comme le résultat d’un mélange des cartes. Dans la classe JeuDeCartes un certain nombre de méthodes sont définies pour mélanger le jeu, retourner une carte (fournir celle qui se trouve au-dessus du tas), fournir la x-ième carte du jeu. Définissez ces méthodes, leurs paramètres et les résultats qu’elles produisent. Que serait une donne? Comment la définir? Ex 9 Inventez une classe Vecteur et imaginez des méthodes pour le calcul des produits scalaires, produits vectoriels, multiplication par un réel, de la norme et une méthode permettant la réalisation de l’addition des vecteurs. Ex 10 On veut réaliser des statistiques sur le lancer d’une paire de dés (on s’intéresse à la somme des points obtenus). Imaginez une application qui simule le lancer de cette paire de dés et qui dresse un tableau des résultats après x lancers. Exercices de compréhension Le but de ces exercices est de vous faire percevoir les petites subtilités du langage. Ils doivent vous permettre de vous assurer que vous contrôlez bien les déclarations qui sont faites et les instructions qui sont données, de même que vous dominez bien la syntaxe du langage. La sémantique des programmes proposés est parfois inexistante, mais ce n’est pas le but. Ex 11 Examinez le programme suivant. Quelles sont les numéros des instructions qui poseront problème à la compilation? 1 2 3 4 5 6 7 8 9 10 11 public class Ex11ch4{ public static void main(String [] args){ byte b1 = 5, b2 = 127; short s1, s2; int i; char c = 'b'; b2 += b1; b2 = b2 + b1; b1 += 2; Chapitre 4 Quelques compléments utiles - 57 - Programmer avec des objets 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Etienne Vandeput ©CeFIS 2002 s1 = b1 + 2; s2 = s1 * 2; c += 2; c = c + 2; i = b1 + b2; i += 5; System.out.println(b1); System.out.println(b2); System.out.println(s1); System.out.println(s2); System.out.println(i); System.out.println(c); } } En admettant que vous transformiez ces instructions problématiques en commentaires, quels seront les résultats affichés par les instructions qui restantes? Ex 12 Déterminez les instructions qui posent problèmes dans le programme suivant. Les noms des variables, des méthodes et des classes sont volontairement dépourvus de sémantique de façon à mettre l’accent sur les mécanismes. public class Ex12ch4{ public static void main(String [] args){ Bidon b = new Bidon(); long valeur = 100; b.reagir(valeur); b.agir(valeur); agir(valeur); } } class Bidon{ private static final long k = 5; private long champ; static void agir(long valeur){ champ = valeur; } void reagir (long valeur){ champ = valeur; k = valeur; } } Chapitre 4 Quelques compléments utiles - 58 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Ex 13 Que va faire afficher le programme suivant, compte tenu des accès possibles et de la résolution de la surcharge? Cet exercice, tiré de C. DELANNOY Exercices en Java Eyrolles Paris 2001 demande une bonne connaissance des principes de résolution de la surcharge. Son énoncé a été repris pratiquement comme tel. public class Ex13ch4{ public static void main(String [] args){ A a = new A(); a.g(); System.out.println("A partir de la méthode MAIN"); System.out.println("---------------------------"); int n =1; long q = 12; float x =1.5f; double y = 2.5; a.f(n,q); a.f(q,n); a.f(n,x); a.f(n,y); } } class A{ public void f(int n, float x){ System.out.println("f(int n, float x)\tn = " + n + " x = " + x); } private void f(long q, double y){ System.out.println("f(long q, double y)\tq = " + q + " y = " + y); } public void f(double y1, double y2){ System.out.println("f(double y1, double y2)\ty1 = " + y1 + " y2 = " + y2); } public void g(){ int n =1; long q = 12; float x =1.5f; double y = 2.5; System.out.println("A partir de la méthode g"); System.out.println("------------------------"); f(n,q); f(q,n); Chapitre 4 Quelques compléments utiles - 59 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 f(n,x); f(n,y); } } Chapitre 4 Quelques compléments utiles - 60 - Programmer avec des objets Chapitre 5 Etienne Vandeput ©CeFIS 2002 Java: héritage et polymorphisme Héritage Spécialisation et généralisation Lorsqu’on examine le domaine d’une application, on se rend vite compte que des classes d’objets apparaissent. L’appartenance d’un objet à une classe est liée au fait que cet objet possède un état (des champs) et un comportement (des méthodes) qui sont communs à tous les éléments de la classe. Il arrive cependant que plusieurs de ces objets possèdent un état et/ou un comportement que l’on souhaiterait définir plus finement que les autres. En d’autres termes • les caractéristiques de l’état demeurent mais sont éventuellement plus nombreuses (davantage de champs); • le comportement change ou se développe (autre implémentation de certaines méthodes et/ou méthodes supplémentaires). Cet état de chose se caractérise par le fait que, dans la description du système d’information analysé, des relations de type “est un...” ou “est une...” apparaissent. Un carré est un quadrilatère, un directeur d’école est un enseignant,... Jusqu’à présent, nous nous sommes limités aux relations de type “a un...” ou “a une...” qui donnaient lieu à de la composition. Une droite a une pente, un cercle a un centre,... La meilleure façon de répondre aux relations “est un...” est d’introduire la notion d’héritage. La notion d’héritage doit être comprise dans le sens où une classe qui hérite d’une autre classe la spécialise. La description de la classe héritée est donc en principe plus fine que la description de la classe parente. Le directeur a une date d’engagement comme directeur qui est une autre information que sa date d’engagement comme enseignant. De la même manière, il est possible que plusieurs classes d’objets apparaissent à un moment donné comme faisant partie d’une classe plus générale. On peut alors décider, a posteriori, que ces classes héritent de cette classe plus générale. Cette démarche porte le nom de généralisation. Chapitre 5 Java: héritage et polymorphisme - 61 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Après avoir défini des carrés, des cercles, des polygones irréguliers,... on peut souhaiter définir une classe plus générale de formes fermées qui, avec une autre classe de formes ouvertes, pourrait à son tour hériter d’une classe encore plus générale de formes. Le schéma se lit de la manière suivante: • un carré est un quadrilatère; • un quadrilatère est une forme fermée; • une forme fermée est une forme; • une forme ouverte est une forme. Le schéma n’est pas nécessairement exhaustif et peut être complété vers le bas ou vers le haut. C’est également un des avantages de la POO que de pouvoir traiter de manière progressive des parties de problèmes. Forme Forme fermée Forme ouverte Quadrilatère Carré La classe dont hérite une classe est appelée sa superclasse. Intérêt L’intérêt d’introduire le mécanisme d’héritage dans la programmation est évident. Le programmeur peut se contenter de spécialiser des classes assez générales plutôt que de tout inventer. La réutilisabilité logicielle est manifeste. Un autre avantage réside dans le polymorphisme des objets. Nous en parlons plus loin. Héritage, surcharge et remplacement Ne confondez pas surcharge et remplacement. La surcharge existe indépendamment de la notion d’héritage. On peut dire d’une méthode qu’elle est surchargée lorsqu’elle possède plusieurs signatures dans la définition de la classe. Le mécanisme d’héritage vient enrichir cette possibilité de surcharge. Une méthode définie dans la superclasse peut être surchargée dans la classe qui en hérite. Il faut pour cela que la nouvelle signature soit différente de toutes les signatures de la méthode dans la superclasse. Si elle est identique à une des signatures de la superclasse, on parle alors de remplacement plutôt que de surcharge. Syntaxe en Java Voyons comment traduire le phénomène de l’héritage en Java. On dira d’une classe qui hérite d’une autre classe, qu’elle étend cette dernière. L’extension concerne évidemment la possibilité d’ajouter des champs mais aussi des méthodes ou de surcharger, voire remplacer50 ces dernières. La représentation schématique de l’extension est une flèche dont la pointe est un triangle creux et dirigée de la classe qui étend vers celle qui est étendue. 50 Bien qu’il s’agisse d’une nouvelle écriture d’une méthode, il existe une nuance entre le remplacement et la surcharge. Elle est expliquée quelques paragraphes plus en avant. En anglais, les mots utilisés sont respectivement overriding et overloading. Chapitre 5 Java: héritage et polymorphisme - 62 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Le mot réservé du langage est le mot extends . class Carre extends Quadrilatere{ ... } Grâce à l’héritage, il n’est pas nécessaire de redéfinir les champs et les méthodes qui sont héritées. Toutefois, le programmeur garde la possibilité de modifier la description des méthodes. Il peut aussi enrichir la définition de la classe héritée en ajoutant des champs, en ajoutant des méthodes ou en surchargeant les méthodes héritées (c-à-d. en en écrivant de nouvelles, portant le même nom, avec un nombre ou des types différents de paramètres). Le mécanisme d’héritage est somme toute assez simple à comprendre. La richesse de ce qu’il propose vous obligera cependant à être très attentifs à une foule de petit détails qui risquent, au début, de vous faire découvrir l’univers implacable des erreurs de compilation. Voici un exemple qui met en évidence les caractéristiques principales de l’héritage et ses liaisons avec la surcharge et le remplacement. /* Illustration de l'héritage, de la surcharge et du remplacement */ public class AppHSR{ /** Classe d’application */ public static void main(String [] args){ /** Création de trois objets: */ Eleve e = new Eleve("Vandeput", "", "5TTr"); Personne p1 = new Personne("Vandeput"); Personne p2 = new Personne("Vandeput", "Etienne"); // Impression des objets comme chaînes de caractères System.out.println(e + "\n\n" + p1 + "\n\n" + p2 + "\n"); } } class Personne{ protected final String nom; protected final String prenom; protected Residence r; //------------------------>Composition public Personne(String nom){ this.nom = nom; this.prenom = ""; } public Personne(String nom, String prenom){ //--->Surcharge Chapitre 5 Java: héritage et polymorphisme - 63 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 this.nom = nom; this.prenom = prenom; } public String toString(){ return "Identité: " + nom + " " + prenom; } } class Eleve extends Personne{ private String classe; public Eleve(String nom, String prenom, String c){ super(nom, prenom); classe = c; } public String toString(){ //--------------------->Remplacement return "Identité " + nom + " " + prenom + "\nClasse: " + classe; } } class Residence{ private private private private String rue; String numero; int codePostal; String localite; } La méthode main de l’application crée trois objets: deux de type Personne et un de type Eleve. La classe Personne possède trois champs de données dont un est de type Residence. Ceci illustre le principe de composition (relation “a un...” ou “a une...”). Une Personne a une Residence. La classe Residence est écrite pour éviter les erreurs à la compilation mais aucun objet de type Residence n’est instancié dans le programme. La classe Personne possède deux constructeurs: l’un sur base d’une chaîne qui est le nom, l’autre sur base de deux chaînes que sont le nom et le prénom. Cette classe qui hérite implicitement de la classe Object, redéfinit (remplacement) la méthode toString de cette dernière. La classe Eleve hérite de la classe Personne. Elle possède un seul constructeur sur base des nom, prénom et classe de l’élève. Il est à noter que ce constructeur fait appel au constructeur de la superclasse pour ce qui est d’initialiser les champs nom et prenom. Lorsqu’il est nécessaire, l’appel au constructeur de la superclasse doit être la première instruction du constructeur. Cet appel Chapitre 5 Java: héritage et polymorphisme - 64 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 est réalisé au moyen du mot clé super51. Le constructeur de la superclasse reçoit deux des trois arguments reçus par le constructeur de la classe. Le modificateur d’accès “protected” Tout cela ne se fait pas sans observer que la méthode toString doit accéder aux champs de la superclasse. Cet accès ne lui est pas autorisé si ces champs sont déclarés private. C’est pourquoi un nouveau modificateur d’accès est nécessaire. Le modificateur protected appliqué aux champs (ou aux méthodes) d’une classe permet aux méthodes de ses sous-classes et de toutes les classes qui en héritent directement ou indirectement d’accéder à ces champs et à ces méthodes. La méthode toString de la classe Eleve remplace la méthode correspondante de la classe Personne qui aurait été héritée sinon. Constructeurs et héritage Lorsque le constructeur d’une classe est invoqué, c’est d’abord le constructeur sans argument de la superclasse qui est invoqué avant que les instructions du constructeur de la classe ne soient exécutées. Si ce constructeur sans argument n’existe pas, il est impératif d’en appeler un explicitement en utilisant le mot-clé super. Il est également possible d’appeler un autre constructeur de la classe plutôt que de s’adresser aux constructeurs de la superclasse. Voici, pour illustrer, un petit programme qui construit un objet de type A et un autre de type B hérité du type A. La classe A possède un constructeur sans arguments52 et pourrait d’ailleurs en posséder d’autres. public class ConstrHerit{ public static void main(String [] args){ A a = new A(); B b = new B(2); System.out.println(a + "\n" + b); } } class A{ protected int x; A(){ System.out.println("Un objet A est construit."); } public String toString(){ return "x: " + x; 51 Attention, super n’est pas comme this une référence à un objet mais une mot-clé spécial qui permet d’invoquer une méthode ou un constructeur de la superclasse. 52 Notez que si une classe ne possède aucun constructeur dans sa définition, elle possède tout de même un constructeur par défaut qui est fatalement sans arguments. Chapitre 5 Java: héritage et polymorphisme - 65 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 } } class B extends A{ private int y; B(int y){ this.y = y; } public String toString(){ return "x: " + x + "\t" + "y: " + y; } } Observez que la construction de l’objet de type B provoque un nouvel affichage de la phrase: “Un objet A est construit”, preuve, si nécessaire, que le constructeur sans argument de A est invoqué. Dans ce cas précis, la suppression du constructeur sans argument de A ne poserait pas de problème. En effet, l’absence de toute définition de constructeur pour A provoque l’appel du constructeur par défaut. En revanche, si au moins un constructeur de A avec argument(s) existe, l’absence de constructeur sans argument provoque une erreur de compilation. Il y a toutefois d’autres manières de procéder. Il est possible d’appeler un constructeur de la superclasse avec arguments (mot-clé super) ou un autre constructeur de la même classe (this). Voici une autre version du programme précédent qui fonctionne en l’absence d’un constructeur sans argument dans A. Les changements sont en caractères gras. public class ConstrHerit2{ public static void main(String [] args){ A a = new A(5); B b = new B(2,4); System.out.println(a + "\n" + b); } } class A{ protected int x; A(int x){ this.x = x; } public String toString(){ return "x: " + x; } } class B extends A{ private int y; B(int x, int y){ Chapitre 5 Java: héritage et polymorphisme - 66 - Programmer avec des objets super(x); this.y = y; Etienne Vandeput ©CeFIS 2002 // Appel explicite d’un constructeur de A } public String toString(){ return "x: " + x + "\t" + "y: " + y; } } Polymorphisme Grâce au mécanisme d’héritage, le programmeur peut s’adresser à un objet par l’intermédiaire d’une classe parente. Il ne doit donc pas se préoccuper du type réel de l’objet et de la méthode à mettre en oeuvre. Si la classe Carre hérite de la classe Quadrilatere qui elle-même hérite de la classe Forme et qu’une méthode dessineToi est définie au niveau de cette dernière, le programmeur peut référencer un objet Carre comme étant une Forme et lui demander de se dessiner. class Forme{ ... public void dessineToi(){ ... } } class Carre extends Quadrilatere} int cote; public Carre(int c){ cote = c; } ... } class Quadrilatere extends Forme{ ... } public class ApplHeritage{ public static void main(String [] args){ ... Forme c = new Carre(10); ... c.dessineToi(); ... } } Chapitre 5 Java: héritage et polymorphisme - 67 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Intérêt Le polymorphisme permet au développeur de s’occuper des généralités en laissant l’environnement d’exécution s’occuper des spécificités. Grâce à lui, un appel à une méthode provoque des actions différentes selon le type des objets concernés. Forme c = new Carre(10); Forme r = new Rectangle(12,20); /* En supposant qu’il existe une classe Rectangle et un constructeur adéquat */ ... c.dessineToi(); r.dessine-toi(); ... La même méthode appliquée à des objets Formes référençant des objets de sous-classes différentes vont produire des actions différentes53. La technique de dessin d’un carré ne sera pas identique à celle d’un rectangle. En réalité, le programmeur demande aux objets de se dessiner sans avoir à se préoccuper de la manière dont cela se fera. Mécanisme Compte tenu de ce qui vient d’être dit, il faut être vigilant quant à la manière dont les méthodes sont choisies, car des choses se passent à la compilation et d’autres choses se passent à l’interprétation. Ceci nous amène à expliquer qu’un programme utilise soit la liaison statique , soit la liaison dynamique pour appeler une méthode. Si une méthode est déclarée static (ou private, ou final) le compilateur sait que c’est bien elle qui sera exécutée en cas d’appel. La résolution de la surcharge se limite aux méthodes de la classe et le choix, basé sur le nombre et le type de paramètres, permet de connaître immédiatement la méthode à appliquer comme dans l’exemple suivant. public class ResSurSta{ public static void main(String [] args){ System.out.println(Essai.auCarre(9) + "\t" + Essai.auCarre(3,4)); } 53 Dans la réalité, l’implantation complète d’une classe comme la classe Forme est difficile. Comment, en effet, décrire une méthode comme dessineToi? Néanmoins, une telle classe a son utilité dès lors qu’il est possible, comme le montre l’exemple, de s’adresser à des objets différents en considérant que ce sont tous des formes. On s’en sort en qualifiant une telle classe de classe abstraite, c-à-d. dont une partie au moins des méthodes n’est pas implantée mais seulement déclarée. Nous traitons des classes abstraites et des interfaces dans un des chapitres qui suivent. Chapitre 5 Java: héritage et polymorphisme - 68 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 } class Essai{ public static int auCarre(int x){ return x*x; } public static int auCarre(int x, int y){ return x*x + y*y; } } En revanche, lorsque ce n’est pas le cas (méthode public et aucun des modificateurs final et static), le compilateur recherche la meilleure des méthodes (résolution de la surcharge) en tenant compte du type déclaré de l’objet invoqué . A l’issue de cette résolution, le compilateur connaît la signature de la méthode à appliquer. Ce n’est pas suffisant. Lors de l’exécution, il est possible que l’objet déclaré de type A soit en fait du type B (hérité de A). Un Directeur est un Enseignant. Dès lors, des instructions comme celles qui suivent ont du sens... Enseignant e = new Directeur(“Duchâteau”); Forme f = new Carre(5); Quadrilatere q = new rectangle(12,20); ...pour autant que les classes Carre et Rectangle héritent de la classe Quadrilatère et que cette dernière hérite de la classe Forme, par exemple. Ainsi, un objet f déclaré de type Forme, peut être en réalité un Carre. En revanche, il en est d’autres qui sont dangereuses et qui sont à proscrire, telle: Directeur d = new Enseignant(“Duchâteau”); Si un Directeur est aussi un Enseignant, l’inverse n’est pas vrai. On pourra demander à un objet e de type Enseignant, tout ce qu’on peut demander à ce type d’objet. Si l’objet réel est un Directeur, il n’y a pas de problème puisque le Directeur hérite du comportement de l’Enseignant. On ne pourra demander à un objet de type Enseignant de se comporter totalement comme un objet de type Directeur puisque le comportement de ce dernier a certainement été enrichi54 ou modifié. 54 Enfin, vous me comprenez! Chapitre 5 Java: héritage et polymorphisme - 69 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Dans l’exemple qui suit, la première construction est correcte, la seconde entraîne une erreur de compilation. public class Poly1{ public static void main(String [] args){ A a = new B(); B b = new A(); } } class A{ ... } class B extends A{ ... } Mais revenons au problème de la résolution de la surcharge. Le compilateur a déterminé la signature de la méthode à utiliser lors de l’appel à un objet o de type O. A l’exécution, l’interpréteur identifie le type réel de l’objet (O ou une de ses sous-classes, soit O1). Il recherche d’abord dans O1, une méthode qui a la signature déterminée par le compilateur. S’il la trouve, c’est elle qu’il utilise, sinon, il cherche en remontant dans la hiérarchie. Voilà tout le mécanisme du polymorphisme expliqué. En résumé: • le compilateur détermine la signature de la méthode sur base du type déclaré de l’objet; • l’interpréteur recherche la méthode qui porte cette signature dans la classe correspondant au type réel de l’objet au moment de l’exécution et s’il ne la trouve pas, il la recherche en remontant dans la hiérarchie. Exercices Ex 1 Le programme suivant sera-t-il compilé? Si non dites pourquoi, si oui, précisez quels seront les résultats affichés. public class Ex1ch5{ public static void main(String [] args){ A a1 = new A(); A b1 = new B(); Chapitre 5 Java: héritage et polymorphisme - 70 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 A b2 = new B(5); A a2 = new B(-1); System.out.println(a1.valeur); System.out.println(b1.valeur); System.out.println(b2.valeur); System.out.println(a2.valeur); } } class A{ int valeur; } class B extends A{ B(){ valeur=100; } B(int v){ valeur = valeur + v; } } L’initialisation des variables b1, b2 et a2 sont correctes. Un objet de type A pourra assurer le comportement des objets de ce type s’il est réellement un objet de type B plus spécialisé que le type A. La définition de la classe A ne comprend pas de définition de constructeur. C’est donc le constructeur par défaut qui est utilisé. Celui-ci attribuera la valeur par défaut au champ valeur, c-à-d. 0. Deux constructeurs garnissent la définition de la classe B. L’initialisation de b1 utilise le premier qui affecte 100 à la variable valeur. L’initialisation de b2 utilise le second qui invoque nécessairement le constructeur par défaut de la superclasse55. La variable valeur contient donc d’abord 0 avant d’être augmentée de la valeur de v par le constructeur de B. Finalement, le champ valeur contiendra 5. L’initialisation de a2 passe par la construction d’un objet de type B. Le processus est donc pareil à celui de la construction précédente. Le champ valeur contiendra -1. Ex 2 Cet exercice n’est pas trop facile. Les noms des classes, des champs et méthode sont volontairement dépourvus de sémantique. Voici les définitions de deux classes. 55 C’est ce qui se passe lorsqu’aucun constructeur de la superclasse (avec super) ou de la même classe (avec this) n’est invoqué explicitement. Chapitre 5 Java: héritage et polymorphisme - 71 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 class A{ A autre; void methode(A v){ autre = new A(); } } class B extends A{ int valeur void methode(A v){ super.methode(v); valeur++; } void methode(B v){ autre = v; } } Parmi les instructions qui suivent, quelles sont celles qui ont du sens. Si elles n’en ont pas, dites pourquoi. Décrivez ce qui se passe effectivement dans le cas où les instructions sont valables. 1. A x = new A(); A y = new A(); x.methode(y); 2. A x = new A(); A y = new B(); x.methode(y); 3. A x = new A(); B y = new B(); x.methode(y); 4. B x = new B(); B y = new B(); x.methode(y); 5. A x = new B(); B y = new B(); x.methode(y); 6. B x = new B(); B y = new A(); x.methode(y); Ex 3 Considérez la hiérarchie suivante: Chapitre 5 Java: héritage et polymorphisme - 72 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 les classes Carre, Rectangle et Parallelogramme héritent de la classe Quadrilatere qui, avec la classe Triangle hérite de la classe Polygone, elle-même héritant de la classe Forme. Que va imprimer le programme qui suit? public class Hierarchie1{ public static void main(String [] args){ Parallelogramme p = new Parallelogramme(); Rectangle r = new Rectangle(); Carre c = new Carre(); Quadrilatere q = new Quadrilatere(); Triangle t = new Triangle(); Polygone g = new Polygone(); Forme f = new Forme(); p.seDefinir(); r.seDefinir(); c.seDefinir(); q.seDefinir(); t.seDefinir(); g.seDefinir(); f.seDefinir(); System.out.println(); f = c; g = r; q = p; f.seDefinir(); g.seDefinir(); q.seDefinir(); } } class Forme{ public void seDefinir(){ System.out.println("Je suis une forme."); } } class Polygone extends Forme{ public void seDefinir(){ super.seDefinir(); System.out.println("Je suis aussi un polygone."); } } class Triangle extends Polygone{ } Chapitre 5 Java: héritage et polymorphisme - 73 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 class Quadrilatere extends Polygone{ } class Carre extends Quadrilatere{ public void seDefinir(){ System.out.println("Je suis un carré."); } } class Rectangle extends Quadrilatere{ public void seDefinir(){ super.seDefinir(); System.out.println("Je suis aussi un rectangle."); } } class Parallelogramme extends Quadrilatere{ } A peu de choses près, la seule méthode invoquée dans la méthode main s’appelle seDefinir et ne possède pas de paramètre. Il est clair que, quel que soit le type d’objet déclaré sur lequel cette méthode est invoquée, la signature de la méthode à appliquer est, dans tous les cas, seDefinir(). A l’exécution, l’interpréteur va rechercher cette méthode dans la classe réelle de l’objet. Cela donne: Invocation Output p.seDefinir(); Je suis une forme. Je suis aussi un polygone. r.seDefinir(); Je suis une forme. Je suis aussi un polygone. Je suis aussi un rectangle. c.seDefinir(); Je suis un carré. q.seDefinir(); Je suis une forme. Je suis aussi un polygone. t.seDefinir(); Je suis une forme. Je suis aussi un polygone. g.seDefinir(); Je suis une forme. Je suis aussi un polygone. f.seDefinir(); Je suis une forme. Après les nouvelles affectations f.seDefinir(); Je suis un carré. Chapitre 5 Java: héritage et polymorphisme - 74 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 g.seDefinir(); Je suis une forme. Je suis aussi un polygone. Je suis aussi un rectangle. q.seDefinir(); Je suis une forme. Je suis aussi un polygone. Lorsque la classe ne possède pas de méthode seDefinir, l’interpréteur examine la superclasse. C’est le cas pour l’objet q, par exemple. Il faut aller voir dans la classe Polygone. Dès qu’il trouve une méthode, il l’exécute. Cette méthode peut éventuellement faire appel à une méthode de la superclasse. C’est le cas pour la méthode seDefinir de la classe Polygone qui fait appel à la méthode seDefinir de sa superclasse Forme. Dans ce cas, il y a d’abord impression du texte “Je suis une forme.” avant l’impression du texte “Je suis aussi un polygone.”. Ex 4 Cet exercice illustre le double processus de détermination, à la compilation, de la signature de la méthode qu’il faudra rechercher et de recherche, à l’interprétation, d’une méthode portant cette signature à partir de la classe correspondant au type réel et éventuellement dans les classes dont elle hérite. Que va afficher le programme suivant: public class Poly2{ public static void main(String [] args){ A A A C B A a = new A(); b = new B(); c1 = new C(); c2 = new C(); d1 = new D(); d2 = new D(); a.m("Hello",5); a.m(5,"Hello"); a.m(5,10); a.m("5",10); b.m(5,"Hello"); b.m(5,10); c1.m(5,10); c1.m(5,"Hello"); c2.m(5,10); d1.m(5,"Hello"); d2.m("Hello",5); d2.m(5,10.f); d2.m(5.f,10); } } class A{ void m(String s, int i){ Chapitre 5 Java: héritage et polymorphisme - 75 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 System.out.println("Dans A: m(String , int)\t" + s + "\t" + i); } void m(int i, String s){ System.out.println("Dans A: m(int, String)\t" + i + "\t" + s); } void m(int i1, int i2){ System.out.println("Dans A: m(int, int)\t" + i1 + "\t" + i2); } void m(float f1, float f2){ System.out.println("Dans A: m(float, float)\t" + f1 + "\t" + f2); } } class B extends A{ void m(int i, String s){ System.out.println("Dans B: m(int, String)\t" + i + "\t" + s); } void m(float f, int i){ System.out.println("Dans B: m(float, int)\t" + f + "\t" + i); } } class C extends A{ void m(int i1, int i2){ System.out.println("Dans C: m(int, int)\t" + i1 + "\t" + i2); } } class D extends B{ void m(int i1, int i2){ System.out.println("Dans D: m(int, int)\t" + i1 + "\t" + i2); } void m(int i, String s){ System.out.println("Dans D: m(int, String)\t" + i + "\t" + s); } } Seule la méthode m imprime des choses. Bien qu’elle demande toujours deux paramètres, elle possède un certain nombre de signatures. Le diagramme d’héritage est assez simple: • D hérite de B qui hérite de A • C hérite de A Chapitre 5 Java: héritage et polymorphisme - 76 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Dans les définitions des classes, la définition de la méthode m vient parfois surcharger les méthodes existantes ( m(float f, int i) dans B, par exemple) et parfois les remplacer (m(inti1, int i2) dans C, par exemple). Chapitre 5 Java: héritage et polymorphisme - 77 - Programmer avec des objets Chapitre 6 Etienne Vandeput ©CeFIS 2002 Les structures de contrôle Utilité Les structures de contrôle existent dans de nombreux langages. Les langages OO n’y échappent pas en ce sens que la création d’objets et la communication entre ces derniers n’exclut pas la possibilité d’écrire des algorithmes. Nous nous contenterons ici de les énoncer, en vrac et pour information, sachant que les lecteurs de ces notes ont connaissance certaines généralités concernant ces structures56. Les structures alternatives Alternative simple On retrouve en Java, comme dans de nombreux langages, le bon vieux “si... alors...sinon...” Sa syntaxe est la suivante: if(expression_booléenne) { bloc_de_traitement1 } else{ bloc_de_traitement2 } Les remarques principales concernent la condition qui est évidemment une expression booléenne devant se trouver entre parenthèses, le caractère optionnel de la partie else et le fait que les traitements à effectuer dans l’un ou dans l’autre cas sont regroupées dans un bloc57. if (p1.equals(p2)){ System.out.println("Ces deux points ne définissent pas une droite!"); } else{ Droite d = new Droite(p1,p2); System.out.println("La droite d a pour équation: ", d); } 56 Ce chapitre ne traite pas, par exemple, de l’opportunité d’utiliser telle ou telle structure dans telles ou telles circonstances, ni de la manière, plus ou moins raisonnable, de créer ces structures. Vous pouvez vous reporter pour cela aux publications du CeFIS traitant d’algorithmique. 57 Lorsque le bloc ne comprend qu’une seule instruction, les accolades ne sont évidemment pas nécessaires. Chapitre 6 Les structures de contrôle - 78 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 L’écriture de la condition peut faire intervenir des variables de types primitifs, des objets, des invocations de méthode et, bien entendu, des opérateurs en tous genres. Alternative multiple Il convient de faire une remarque à propos de cette structure. Elle concerne la manière dont l’analyse d’un problème logiciel est menée. Il est clair que les observations faites dans le chapitre précédent concernant l’héritage et surtout le polymorphisme, pousseront souvent les développeurs d’applications à envisager une hiérarchie de classes d’objets avec une spécialisation des méthodes au niveau des sousclasses. Dès lors, le polymorphisme aidant, la structure dont il est question ici s’avère inutile. A un choix multiple, se substitue la résolution de la surcharge et la recherche de la méthode possédant une signature convenable. Toutefois, la nécessité d’employer une structure avec alternative multiple n’exige pas toujours le passage par une hiérarchie de classes. C’est au programmeur d’en juger. La syntaxe de cette structure est la suivante: switch (valeur_entière){ case valeur1: bloc_de_traitement1 case valeur2: bloc_de_traitement2 ... default: bloc_de_traitement_par_défaut } Quelques remarques importantes sont à faire: • le paramètre à fournir à la structure switch doit correspondre à une expression ayant une valeur entière; • si la valeur de cette expression ne se trouve pas dans les valeurs mentionnées, c’est le traitement par défaut qui est réalisé; • s’il n’y a pas de traitement par défaut et que la valeur n’est pas rencontrée, la structure est simplement ignorée; • la plupart des blocs de traitement doivent se terminer par l’instruction break; qui assure que les autres possibilités ne sont pas examinées; • une instruction return équivaut à l’utilisation d’une instruction break. Chapitre 6 Les structures de contrôle - 79 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Voici, à titre d’illustration, un petit programme qui affiche la valeur décimale d’un caractère (supposé être) hexadécimal. Le programme saisit le caractère sous forme d’une chaîne 58 et affiche un message s’il n’y a pas ou plus d’un caractère. public class HexaVersDeci{ public static void main(String [] args){ String car = Clavier.lireString(); if (car.length()!=1){ System.out.println("ERREUR: un seul caractère"); } else{ char c=car.charAt(0); int val=deci(c); if(val!=-1){ System.out.println("Valeur du caractère:" + val); } } } public static int deci(char c){ switch(c){ case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return c-'0'; case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': return c-'A'+10; case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': return c-'a'+10; default: System.out.println("Ce caractère n’est pas employé en\ hexadécimal"); return (-1); } } } La méthode main n’affiche la valeur du caractère que si celui-ci est valide. La chaîne (même si elle ne comprend qu’un seul caractère) doit être transformée en caractère par les vertus de la méthode charAt. 58 Il vous est loisible d’améliorer la classe Clavier en y ajoutant une méthode lireChar(). Chapitre 6 Les structures de contrôle - 80 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Dans la structure switch, les cas semblables sont regroupés. Les opérations arithmétiques entre caractères donnent bien des entiers de type int (promotion). Les instructions return ont aussi l’effet de l’instruction break. Vous pourrez modifier sans trop de peine la méthode main pour que le programme fonctionne lorsqu’une chaîne de plus d’un caractère est fournie. Il y a lieu, dans ce cas, de distinguer les messages: • “Vous n’avez pas fourni de caractère” • “Vous avez fourni plus d’un caractère. Seul le premier a été pris en compte.” Les structures répétitives On retrouve ici les trois structures classiques permettant d’effectuer un traitement tant qu’une condition est vérifiée, jusqu’à ce qu’une condition soit vérifiée ou encore, un nombre connu59 de fois. La boucle “while” Les instructions contenues dans une telle boucle sont exécutées de 0 à n fois60. La valeur booléenne de la condition peut en effet être false dès le départ ce qui fait que les instructions de la boucle sont ignorées. Voici la syntaxe: while (expression_booléenne){ bloc_de_traitement } Un (très) petit exemple: public class BoucleWhile{ public static void main(String [] args){ int i = 0; while (i++<8){ System.out.println("Bienvenue à la séance " + i + " du cours Java."); } } } Les accolades ne sont pas nécessaires si le bloc ne contient qu’une seule instruction. 59 Ce nombre peut être déterminé par la connaissance d’une valeur initiale et d’une valeur finale. 60 Il faut espérer que le programmeur ne s’est pas arrangé pour que n tende vers l’infini. Chapitre 6 Les structures de contrôle - 81 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 La boucle “do - while” Le bloc d’instructions est exécuté au moins une fois puisque la condition n’est testée qu’en fin de boucle. do bloc_de_traitement while (expression_booléenne); Vous remarquerez que les instructions à l’intérieur d’une structure do - while constituent toujours un bloc. De même, l’expression while(expression_booléenne) est obligatoirement suivie d’un pointvirgule. Un exemple assez semblable au précédent: public class BoucleDoWhile{ public static void main(String [] args){ int i = 1; do System.out.println("Bienvenue à la séance " + i + " du cours Java."); while (i++<8); } } Notez encore que l’expression booléenne pouvait s’écrire ++i<=8. La boucle “for” Dans cette structure, sont à préciser, les valeurs initiale(s) et finale(s) de la (des) variable(s) à prendre en considération. En voici la syntaxe: for (initialisations; expression_booléenne; incrémentations){ bloc_de_traitement { Et toujours le même exemple... public class BoucleFor{ public static void main(String [] args){ for(int i=1;i<=8;++i){ System.out.println("Bienvenue à la séance " + i + " du cours Java."); } } Chapitre 6 Les structures de contrôle - 82 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 } Comme le laisse supposer la syntaxe, il est possible d’initialiser plusieurs variables et de préciser plusieurs incrémentations (au sens large). La méthode qui suit renvoie l’exposant de la plus grande puissance de 2 supérieure ou égale à un nombre donné (en paramètre). public static int puissanceDeDeux(int valeur){ int exp; for(exp=0, valeur-=1; valeur>0; exp++, valeur/=2){ } return exp; } } Quelques commentaires sont nécessaires. A chaque passage dans la boucle, il faudra diviser par deux (division entière) et augmenter de 1 la valeur de l’exposant. Cette dernière devra être initialisée à 0 et la valeur à diviser, à la valeur fournie moins une unité pour assurer qu’il s’agit bien d’un nombre supérieur ou égal (si valeur vaut 2, il faudra deux divisions avant que v vaille 0, alors que l’exposant doit valoir 1). Il y a donc deux instructions d’initialisation et deux instructions d’incrémentation (augmenter de 1, diviser par 2). Il n’y a pas d’instruction dans le bloc. Ce n’est pas nécessaire puisqu’elles sont incluses dans la structure for. En revanche, l’absence d’accolades entraîne une erreur de compilation. La variable exp ne peut être initialisée dans la boucle car elle est valeur de retour. “break”, “label”, “continue” et autre “return” De nombreux discours tentent, sans doute à raison, de pousser les programmeurs dans la voie d’une programmation structurée. Il arrive cependant parfois que l’on confonde recommandation et impératif. Lorsque plusieurs structures sont imbriquées, on souhaite parfois échapper à l’imbrication dans des cas spécifiques. Sans pour cela en faire une habitude, l’emploi des instructions qui suivent peut parfois s’avérer intéressant. L’instruction break ne permet pas seulement de quitter une structure switch comme vu précédemment. Elle permet de quitter le bloc dans lequel elle est écrite. Elle interrompt ainsi le flux d’exécution. Interruption classique: do int revenus = Clavier.lireInt() ; if(revenus<100000) break; ... while(...); Chapitre 6 Les structures de contrôle - 83 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 L’instruction peut être utilisée dans l’ensemble des structures qui viennent d’être présentées: if -else, switch, while, do - while, for. Lorsque plusieurs blocs sont imbriqués, il est possible de quitter un bloc plus externe que le bloc dans lequel se trouve l’instruction. Il suffit alors d’étiqueter ce bloc et d’utiliser cette étiquette à la suite de l’instruction break. L’étiquetage d’une instruction se fait de la manière suivante: étiquette: instruction Interruption labélisée: taxation: do int revenus = Clavier.lireInt() ; if(revenus<100000){ System.out.println("Pas de taxation"); break taxation; } ... while(...); L’instruction continue dans une boucle permet également de modifier le flux d’exécution en renvoyant l’exécution à la fin de la boucle et donc à l’évaluation de la condition de poursuite ou d’arrêt. Enfin, vous connaissez déjà l’instruction return utilisée pour renvoyer une valeur ou une référence d’objet à un appelant. Lorsqu’il n’y a pas de valeur de retour, l’instruction rend tout simplement la main à l’appelant, ce qui est également une façon de rompre le flux normal d’exécution du programme. Exercices Ex1 Construisez un type abstrait Ensemble implanté à partir d’une liste chaînée simple. Rappelons qu’un ensemble est une liste non nécessairement ordonnée d’éléments de même type et qu’un même élément ne peut se retrouver plusieurs fois dans l’ensemble. Il faudra notamment pouvoir créer un ensemble, ajouter un élément à l’ensemble, vérifier si un élément appartient à l’ensemble, obtenir le nombre d’éléments qui le composent. Ex 2 Améliorez le programme de statistiques sur les lancers de dés en affichant un histogramme sous forme d’étoiles (une étoile par x unités), comme par exemple: * *** ****** ** * Chapitre 6 Les structures de contrôle - 84 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Ex 3 Ecrivez une application qui prend en paramètres de la ligne de commande les coefficients a, b et c d’un trinôme du second degré et qui affiche un message décrivant le nombre et la valeurs des racines éventuelles. Ex 4 Ecrivez une application qui simule le lancer d’un dé et qui affiche le nombre de lancers qu’il a fallu: • avant d’obtenir deux fois six consécutivement; • avant d’obtenir le résultat six après le résultat un. Nous pourrions allonger indéfiniment la liste des exercices qui mettent en jeu les structures de contrôle disponibles en Java. Ces problèmes ne sont toutefois pas spécifiques à la POO. Tous les problèmes de cette nature traités habituellement avec les langages associés à la programmation impérative peuvent donc être repris ici, et en particulier, tous ceux qui ont pour objectif de remplir des tableaux, par exemple. Nous n’allons donc pas allonger la liste des exercices possibles de manière inconsidérée. Nous vous renvoyons, pour cela, à des ouvrages plus anciens mais toujours bien d’actualité pour ce qui est de mettre en place les concepts liés à la programmation classique. Chapitre 6 Les structures de contrôle - 85 - Programmer avec des objets Chapitre 7 Etienne Vandeput ©CeFIS 2002 D’autres mécanismes Aperçu Outre les mécanismes déjà largement évoqués de l’encapsulation, de l’héritage, de la surcharge, du polymorphisme, des liaisons dynamiques, il en existe d’autres qui contribuent, chacun à leur manière, à rendre les programmes plus efficaces, plus sûrs, mieux élaborés. Les classes abstraites et les interfaces sont des notions de Java qui vont permettre, les unes, d’enrichir le mécanisme d’héritage, les autres de proposer une version un peu faussée mais pas trop complexe de l’héritage multiple. Nous dirons aussi un mot, dans ce chapitre, de la manière dont ils peuvent être détruits. Enfin, nous évoquerons et nous expliciterons le regroupement possible des classes et des interfaces en packages. Les classes abstraites Lorsque nous avons développé le mécanisme d’héritage, nous avons signalé qu’il était à la fois, un moyen de spécialiser une classe existante, mais aussi un moyen d’en généraliser plusieurs d’entre elles, ayant des points communs non pris en compte au moment de leur création. En s’intéressant au processus de généralisation, on se rend compte que, plus on grimpe dans la hiérarchie, plus les classes deviennent abstraites, dans le sens où il est possible de les caractériser sans trop spécifier la manière dont ces caractéristiques peuvent être établies. Considérez les trinômes du second degré du genre ax² + bx + c dont on souhaite connaître les racines. Nous pouvons imaginer une classe Trinome très générale dont héritent les classes TrinomeSansRacine, TrinomeAvecRacineDouble, TrinomeAvecDeuxRacinesReelles, ou même une classe TrinomeAvecDeuxRacinesComplexes. La définition de la méthode calculerRacines sera différente selon les classes qui héritent de la classe Trinome. Pour les raisons que nous avons évoquées lorsqu’il fût question de polymorphisme, il est utile de mentionner cette méthode au niveau de la classe Trinome, chaque classe d’héritage devant alors en remplacer la définition. Il est cependant difficile de la décrire à ce niveau sans faire perdre toute utilité au graphe d’héritage. Ce type de difficulté est à la base de l’idée des méthodes et des classes abstraites. La méthode calculerRacines est déclarée au niveau de la classe Trinome sans toutefois être définie. La méthode est abstraite. De ce fait, la classe Trinome le devient également et cela, même si d’autres méthodes de cette classe sont concrètes (une méthode getValeurDeA, par exemple). Pour devenir concrètes, les classes d’héritage doivent implanter la méthode calculerRacines61. 61 Les classes d’héritage ne sont pas forcées d’implanter la méthode abstraite auquel cas elles restent abstraites. Ce sera alors à une des sous-classes de l’implanter. Chapitre 7 D’autres mécanismes - 86 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Voici comment se traduisent, syntaxiquement, les propriétés d’abstraction de la méthode et, en conséquence, de la classe. public abstract class Trinome{ protected double a, b, c; ... public abstract void calculerRacines(); ... } Observez l’absence d’accolades derrière la définition de la méthode abstraite. L’intérêt des méthodes abstraites réside dans le fait qu’elles simplifient le travail du programmeur (qui, rappelons-le, ne s’occupe que des généralités) sans qu’il soit nécessaire de définir la procédure correspondante. Celle-ci sera obligatoirement et concrètement définie, soit dans ses sous-classes directes ou à un niveau de profondeur plus important dans la hiérarchie des classes. Une classe ne devient concrète que si toutes ses méthodes (y compris celles dont elle hérite) sont concrètes. Signalons encore, même si c’est une évidence, qu’une classe abstraite ne peut être instanciée. L’exemple suivant illustre l’emploi de la classe abstraite Trinome de même que le polymorphisme. Dans la description de la méthode main, le programmeur se contente de créer des trinômes dont il ne sait, a priori, de quel type véritable ils seront. public class AppAbstract{ public static void main(String [] args){ Trinome t1 = Trinome.creer(1.,2.,1.); Trinome t2 = Trinome.creer(1.,-1.,1.); Trinome t3 = Trinome.creer(1.,5.,6.); t1.calculerRacines(); t2.calculerRacines(); t3.calculerRacines(); } } class Rho{ private double valeur; public Rho(double a, double b, double c){ valeur = b*b - 4*a*c; } public double getValeur(){ return valeur; } Chapitre 7 D’autres mécanismes - 87 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 } abstract class Trinome{ protected double a, b, c; Rho d; public Trinome(double a, double b, double c, Rho d){ this.a = a; this.b = b; this.c = c; this.d = d; } public static Trinome creer(double a, double b, double c){ Rho d = new Rho(a,b,c); if (d.getValeur()<0){ return new TrinomeSansRacine(a,b,c,d); } else{ if (d.getValeur()==0){ return new TrinomeAvecRacineDouble(a,b,c,d); } else{ return new TrinomeAvecDeuxRacines(a,b,c,d); } } } public abstract void calculerRacines(); } class TrinomeSansRacine extends Trinome{ public TrinomeSansRacine(double a, double b, double c, Rho d){ super(a,b,c,d); } public void calculerRacines(){ System.out.println("Il n'y a pas de racines réelles."); } } class TrinomeAvecRacineDouble extends Trinome{ public TrinomeAvecRacineDouble(double a, double b, double c, Rho d){ super(a,b,c,d); } public void calculerRacines(){ System.out.println("La racine double est " + (-b/2*a)); } } Chapitre 7 D’autres mécanismes - 88 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 class TrinomeAvecDeuxRacines extends Trinome{ public TrinomeAvecDeuxRacines(double a, double b, double c, Rho d){ super(a,b,c,d); } public void calculerRacines(){ System.out.println("Les racines sont " + ((-b+Math.sqrt(d.getValeur()))/2*a) + " et " + ((-b-Math.sqrt(d.getValeur()))/2*a) + "."); } } Devant la longueur du développement dans cette approche objet du problème, vous objecterez sans doute 62 qu’une approche classique est bien plus immédiate. Pour éliminer cette objection, il faut s’imaginer que les exigences peuvent encore et toujours évoluer. Que faire, par exemple, si on souhaite envisager, de manière optionnelle, la production de racines complexes? Comment associer simplement une interface graphique a une telle application? Examinons de suite le premier problème. Le second sera résolu lors de la présentation et du développement des classes de l’interface graphique en Java. Le graphe d’héritage que nous avons établi ne demande qu’à être complété. Nous créons ainsi une classe supplémentaire appelée TrinomeAvecRacinesComplexes, dans laquelle nous adaptons la définition de la méthode calculerRacines. On pourrait s’étonner de ne pas voir cette nouvelle classe remplacer la classe TrinomeSansRacine. C’est que l’on veut se ménager la possibilité de faire le choix d’un calcul ou non des racines complexes. Il ne s’agit donc pas ici de créer une partition de l’ensemble des trinômes du second degré mais bien d’envisager une partition des possibilités de calcul que l’on veut se ménager. Au niveau de la procédure (méthode) de création d’un trinôme, il faudra envisager cette possibilité et aussi prévoir le passage d’un booléen précisant si, oui ou non, d’éventuelles racines complexes doivent être calculées. La définition des classes de trinômes existantes n’est en rien modifiée par ces changements. Voici la nouvelle application. Les quelques changements sont indiqués en caractères gras. public class AppAbstract2{ public static void main(String [] args){ Trinome Trinome Trinome Trinome t1 t2 t3 t4 = = = = Trinome.creer(1,2,1,true); Trinome.creer(1,-1,1,true); Trinome.creer(1,-1,1,false); Trinome.creer(1,5,6,true); t1.calculerRacines(); t2.calculerRacines(); 62 Ce n’est pas un mauvais jeu de mots! Chapitre 7 D’autres mécanismes - 89 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 t3.calculerRacines(); t4.calculerRacines(); } } class Rho{ private double valeur; public Rho(double a, double b, double c){ valeur = b*b - 4*a*c; } public double getValeur(){ return valeur; } } abstract class Trinome{ protected double a, b, c; Rho d; public Trinome(double a, double b, double c, Rho d){ this.a = a; this.b = b; this.c = c; this.d = d; } public static Trinome creer(double a, double b, double c, boolean complexe){ Rho d = new Rho(a,b,c); if (d.getValeur()<0){ if (complexe){ return new TrinomeAvecRacinesComplexes(a,b,c,d); } else{ return new TrinomeSansRacine(a,b,c,d); } } else{ if (d.getValeur()==0){ return new TrinomeAvecRacineDouble(a,b,c,d); } else{ return new TrinomeAvecDeuxRacines(a,b,c,d); } } } public abstract void calculerRacines(); } Chapitre 7 D’autres mécanismes - 90 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 class TrinomeSansRacine extends Trinome{ public TrinomeSansRacine(double a, double b, double c, Rho d){ super(a,b,c,d); } public void calculerRacines(){ System.out.println("Il n'y a pas de racines réelles."); } } class TrinomeAvecRacineDouble extends Trinome{ public TrinomeAvecRacineDouble(double a, double b, double c, Rho d){ super(a,b,c,d); } public void calculerRacines(){ System.out.println("La racine double est " + (-b/2*a)); } } class TrinomeAvecDeuxRacines extends Trinome{ public TrinomeAvecDeuxRacines(double a, double b, double c, Rho d){ super(a,b,c,d); } public void calculerRacines(){ System.out.println("Les racines sont " + ((-b+Math.sqrt(d.getValeur()))/2*a) + " et " + ((-b-Math.sqrt(d.getValeur()))/2*a) + "."); } } class TrinomeAvecRacinesComplexes extends Trinome{ public TrinomeAvecRacinesComplexes(double a, double b, double c, Rho d){ super(a,b,c,d); } public void calculerRacines(){ System.out.println("Les racines complexes sont " + ((-b+Math.sqrt(-d.getValeur()))/2*a) + "i et " + ((-b-Math.sqrt(-d.getValeur()))/2*a) + "i."); } } Il est évident que le polymorphisme peut s’étendre à d’autres méthodes qui seraient déclarées abstraites dans la classe Trinome et définies dans chacune des classes d’héritage. A titre d’exercice, vous pouvez imaginer une méthode qui précise, par le renvoi d’une valeur booléenne, si une valeur donnée se trouve entre les racines du trinôme. Chapitre 7 D’autres mécanismes - 91 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Pour ce faire, il semble intéressant de définir une classe Racine dont les champs seraient garnis lors du calcul de celles-ci. Si les champs de cette classe sont déclarés privés, des méthodes d’accès et d’altération sont nécessaires. Les interfaces Un des problèmes évoqués dans la section précédente consistait à lier l’application du calcul des racines à une interface graphique d’une certaine convivialité. Ce problème apparaît comme un cas particulier du sujet qui sera traité dans cette section, à savoir, les interfaces (tout court). Sa résolution exigera également que nous nous intéressions aux classes graphiques prédéfinies de Java et que nous nous penchions sur le problème de la gestion des événements. Mais intéressons-nous d’abord au concept d’interface dans sa généralité. Nous venons de découvrir que la généralisation (à outrance) conduisait à ce que les classes situées dans les couches hautes de la hiérarchie soient des classes abstraites. Comprenez, par là, que la définition de ces classes contient au moins une méthode abstraite et que ces classes ne peuvent avoir d’instances. L’interface est un peu comme une classe abstraite mais dont la description ne comporte aucune méthode concrète. L’interface définit, en quelque sorte, un contrat que certaines classes s’engagent à respecter sans préciser les termes de ce contrat. Il ne s’agit pas ici d’héritage au sens habituel. On ne dira d’ailleurs pas qu’une classe étend une interface, mais qu’elle l’implante. interface Forme{ public abstract double surface(); public abstract double volume() } Par ailleurs on trouvera... class Point implements Forme{ private double x, y; ... // autres champs, constructeurs et méthodes public double surface(){ return 0.0; } public double volume(){ return 0.0; } } Par ailleurs encore... Chapitre 7 D’autres mécanismes - 92 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 class Carre implements Forme{ private final String nom; private double cote; public Carre(String n, double c){ nom = n; cote = c; } public double surface(){ returne cote*cote; } public double volume(){ return 0.0; } public String getNom(){ return nom; } } Commentaires Par essence, les interfaces ont un rôle de porte d’entrée ce qui rend inutile le modificateur d’accès public. Les interfaces ne comporteront pas de champs d’instances. En revanche, il est possible d’y définir des constantes63 (champs déclarés public, static et final). L’exemple choisi pourrait laisser croire qu’il est préférable d’utiliser une interface plutôt qu’une classe abstraite. Il n’en est rien. Si le programmeur est conscient de l’existence de points, de carrés, de cercles,... dans son application, il est préférable qu’il crée une hiérarchie dont les classes supérieures seront des classes abstraites. Les interfaces sont intéressantes parce qu’elles garantissent que les classes qui les implantent disposent des fonctionnalités qu’elles déclarent. A ce titre, le package java.lang comporte notamment deux interfaces qui portent les noms de Clonable et Comparable. On voit que les noms d’interface font plutôt référence à des propriétés que les classes d’implantation devraient assurer à leurs instances. L’interface Clonable assure que les objets des classes d’implantation sont “reproductibles”, l’interface Comparable qu’il existe une technique pour les comparer. Héritage multiple Pourquoi ce nouveau concept, alors qu’on pouvait se contenter des classes abstraites? Il faut se souvenir qu’une classe ne peut hériter que d’une seule autre classe. Il arrive que dans certaines 63 On peut d’ailleurs se limiter à cela dans la description d’une interface. Chapitre 7 D’autres mécanismes - 93 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 situations, le programmeur souhaite disposer d’un mécanisme d’héritage multiple. L’héritage multiple n’est cependant pas simple à gérer64. C’est pourquoi Java propose ce concept d’interface qui se contente de déclarer les signatures de méthodes qui devront être définies au sein des classes qui prétendent l’implanter. Dans l’exemple qui suit, l’application s’intéresse à des athlètes sélectionnés pour les JO. Ceux-ci sont, notamment et à la fois, des personnes dont on peut souhaiter obtenir le nom, et des sportifs dont on souhaite connaître la discipline principale. La classe SelectionneJO va donc implanter les deux interfaces Personne et Sportif. C’est dire que les méthodes déclarées dans ces deux interfaces doivent y être définies si on veut que cette classe soit concrète. Le polymorphisme y est également mis en évidence dans la mesure où les objets p1 à p4 qui sont réellement des objets de type SelectionneJO sont soit déclarés comme Personne, soit comme Sportif 65 . Bien entendu, vous pourriez rétorquer que cette façon de procéder en mélangeant les personnes et les sportifs n’est pas très intéressante ici. Il faut se dire que plusieurs applications sont susceptibles d’employer ces interfaces et que, dans certains cas, le programmeur d’une application aura besoin d’effectuer un traitement sur les personnes sans se préoccuper de savoir si ces personnes sont des sportifs, des étudiants, des touristes ou qui que ce soit d’autre. Dans d’autres cas, il devra effectuer des traitements concernant les sportifs, ce qui justifie des déclarations plus larges et moins spécifiques. C’est évidemment le prix à payer pour bénéficier du polymorphisme. Le fichier contient en tout premier lieu, une instruction d’importation, celle du package java.util. Cette importation est rendue nécessaire par l’utilisation, plus en avant dans le programme, des classes prédéfinies que sont GregorianCalendar et la classe abstraite Calendar dont elle hérite. import java.util.*; public class AppInterface{ public static void main(String [] args){ // Testé le 11/04/2002 (à adapter) Personne p1 = new SelectionneJO("Durant", "10/04/1983", "javelot"); Personne p2 = new SelectionneJO("Dupont", "11/04/1983", "110m haies"); Personne p3 = new SelectionneJO("Dupuis", "12/04/1983", "poids"); Sportif p4 = new SelectionneJO("Dupont", "13/12/1983", "100m"); System.out.println("Le nom de la personne: " + p1.getNom()); 64 Imaginez que trois classes B, C et D héritent d’une même classe A et que, dans chacune de ces quatre classes, une méthode m() soit (re)définie. Imaginez qu’une classe E hérite à la fois de C et de D (héritage multiple) sans redéfinir m(). Quelle méthode le compilateur doit-il choisir pour une invocation de la méthode m sur un objet instance de la classe E? 65 ...ce qui empêche l’invocation de méthodes qui ne sont pas connues, selon le cas, de Personne ou de Sportif. Ainsi, l’invocation p2.getNom() provoquerait une erreur à la compilation. Chapitre 7 D’autres mécanismes - 94 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 System.out.println("L'âge de la personne: " + p1.calculeAge() + " ans"); System.out.println("Le nom de la personne: " + p2.getNom()); System.out.println("L'âge de la personne: " + p2.calculeAge() + " ans"); System.out.println("Le nom de la personne: " + p3.getNom()); System.out.println("L'âge de la personne: " + p3.calculeAge() + " ans"); System.out.println("La discipline du sportif: " + p4.getDisciplinePrincipale()); } } interface Personne{ public abstract String getNom(); public abstract long calculeAge(); } interface Sportif{ public abstract String getDisciplinePrincipale(); } class SelectionneJO implements Personne, Sportif{ private String nom, discipline; private GregorianCalendar dateDeNaissance; public SelectionneJO(String nom, String ddn, String discipline){ this.nom = nom; this.discipline = discipline; int annee = Integer.parseInt(ddn.substring(6,10)); int mois = Integer.parseInt(ddn.substring(3,5)) - 1; int jour = Integer.parseInt(ddn.substring(0,2)); dateDeNaissance = new GregorianCalendar(annee,mois,jour); } public String getNom(){ return nom; } public String getDisciplinePrincipale(){ return discipline; } public long calculeAge(){ GregorianCalendar now = new GregorianCalendar(); int deltaM = now.get(Calendar.MONTH) dateDeNaissance.get(Calendar.MONTH); Chapitre 7 D’autres mécanismes - 95 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 int deltaD = now.get(Calendar.DAY_OF_MONTH) dateDeNaissance.get(Calendar.DAY_OF_MONTH); int deltaY = now.get(Calendar.YEAR) dateDeNaissance.get(Calendar.YEAR); if (deltaM > 0 || (deltaM == 0 && deltaD >= 0)) return deltaY; else return deltaY - 1; } } L’interface Personne annonce les deux méthodes getNom et calculeAge. L’interface Sportif annonce la méthode getDisciplinePrincipale. Cela signifie que, d’une manière ou d’une autre, la classe SelectionneJO devra les implanter toutes les trois. D’autre part, une interface ne pouvant posséder de champs d’instance, les champs nécessaires à l’implantation de ces méthodes devront se trouver dans la définition de la classe SelectionneJO. On y trouvera notamment les champs nom et discipline qui sont de type String et le champ dateDeNaissance qui est de type GregorianCalendar. Le nom quelque peu étonnant de cette classe sera justifié sous peu. Rappelons que les interfaces ont un accès public ce qui ne nécessite aucune précision explicite. Il en est de même pour leurs méthodes qui sont nécessairement abstraites. Les modificateurs d’accès public et abstract ne sont donc pas nécessaires. Nous les avons mentionnés cette fois pour mettre en évidence les propriétés de ces méthodes. Nous ne le ferons plus dans la suite. Il nous faut maintenant parler de la classe GregorianCalendar. En fait, ce dont nous avons besoin, c’est d’une classe permettant d’instancier des objets correspondant à des dates et comprenant un certain nombre d’opérations liées à ce type d’information. Il existe deux classes prédéfinies portant le nom Date, l’une faisant partie du package java.util, l’autre faisant partie du package java.sql. En réalité, ces classes représentent en interne les dates comme des nombres entiers exprimant le nombre de millisecondes s’étant écoulées depuis le 1er janvier 1970 à 0h. Une telle description de date correspond à l’utilisation d’un point de repère particulier dans un calendrier tout aussi particulier (en l’occurrence, le calendrier grégorien, celui qui est le plus utilisé dans notre civilisation). Parce que cette façon de voir est assez limitative, les concepteurs de Java ont imaginé une classe abstraite, Calendar, décrivant les propriétés générales des calendriers dont pourraient hériter diverses classes correspondant à différentes manières de se repérer dans le temps. En particulier, la classe GregorianCalendar est une classe concrète qui hérite de la classe Calendar. D’autres calendriers pourraient être implantés comme des extensions de cette classe Calendar. La classe GregorianCalendar possède plusieurs constructeurs intéressants, dont un qui prend trois entiers (type int) comme paramètres: le millésime, le numéro du mois66 et le numéro du jour dans le mois. Dans la définition du constructeur de la classe SelectionneJO, on l’utilise après avoir affecté les champs nom et discipline et après que la chaîne de caractères correspondant à la date de naissance ait été soigneusement décomposée. 66 Piège pour programmeur: les mois sont numérotés de 0 à 11 alors que les numéros des jours commencent à 1. Chapitre 7 D’autres mécanismes - 96 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 La méthode calculeAge demande aussi un petit commentaire. Un autre constructeur est employé pour créer la date du jour. Il reste à calculer la différence des années mais aussi celle des mois qui est absolument nécessaire et celle des jours qui l’est parfois. Puis on renvoie la valeur en fonction du résultat des tests effectués. Vous observerez encore que la classe GregorianCalendar possède une méthode get dont les paramètres peuvent être divers. La constante YEAR est définie au niveau de la superclasse Calendar, de même que les constantes MONTH et DAY_OF_MONTH. Elles sont évidemment static et final. Enfin, la méthode main met tout cela en application. Les méthodes invoquées sont celles des classes de déclaration des objets. Héritage multiple et interfaces prédéfinies Dans l’illustration suivante, la classe MaClasse implante à la fois les interfaces Comparable et l’interface Clonable du package java.lang. Cela oblige MaClasse à décrire leurs méthodes. En fait, il n’y a que la seule méthode compareTo de l’interface Comparable à décrire car c’est la seule méthode de cette interface et l’interface Clonable n’en comporte pas. Elle garantit simplement aux classes qui l’implantent que la méthode clone de la classe Object fonctionnera sans générer d’exception. class MaClasse implements Comparable, Clonable{ public int compareTo(Object o){ ... } ... } Vous pouvez le constater ici, les interfaces sont également utilisées pour caractériser des propriétés que devraient posséder les objets des classes qui les implantent. Les interfaces graphiques et la gestion des événements Un autre secteur dans lequel les interfaces s’avèrent utiles est celui des applications fonctionnant par l’intermédiaire d’interfaces graphiques. En fait, le mot interface utilisé jusqu’ici n’a pas un sens complètement différent de celui qu’il recouvre dans l’expression interface graphique. L’interface graphique est aussi la couche supérieure par le biais de laquelle l’utilisateur de l’application va s’adresser au programme sans se préoccuper de ce qui se passe en arrière-plan. Bien entendu, il est difficile de mettre au point une application interactive sans gérer des événements tels que des clics de souris sur des boutons, sur des items de listes ou encore la pression de la touche de validation après avoir rempli un champ de données telle une zone de texte, par exemple. Ceci nous amènera à nous intéresser au modèle des événements tel qu’il est conçu en Java. Nous en parlerons plus en détail dans le chapitre qui suit. Toutefois, afin d’illustrer le concept d’interface en liaison avec la gestion des événements, analysons une application pas très compliquée, le déclenchement et l’arrêt Chapitre 7 D’autres mécanismes - 97 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 d’un chronomètre. Nous n’intégrerons pas dans cette application élémentaire d’éléments graphiques pour l’instant. Quelques outils L’interface ActionListener est une interface prédéfinie. Les instances des classes qui l’implantent seront des “écouteurs d’action”. Dès lors, ces classes doivent posséder une définition de la méthode ActionPerformed qui décrit les traitements à effectuer lorsqu’un message de réalisation d’événement leur parvient. L’association entre un objet émetteur et un objet récepteur (l’écouteur) peut se faire de diverses manières. Dans le cas présent, la classe AfficheurDeTemps implante l’interface ActionListener. Chaque objet de cette classe possède un champ appelé topChrono, invariable (donc initialisé à la création de l’objet) qui contient (sous forme d’une valeur numérique liée au 1er Janvier 1970) le temps initial. La méthode actionPerformed construit un nouvel “instant” à chaque réception d’un message (en principe toutes les 5 secondes) et affiche la différence entre le moment présent et le moment de la création du chrono. La méthode main de l’application construit un chrono (objet de type déclaré ActionListener et réellement, objet de type AfficheurDeTemps) puis crée un objet de la classe prédéfinie Timer dont les paramètres de construction sont un délai (5000 millisecondes) et l’écouteur à avertir (le chrono). Le Timer est démarré grâce à la méthode start et une boîte de dialogue élémentaire permet de le stopper implicitement. Cette application est rudimentaire au niveau de l’affichage. Elle sera améliorée lors de notre découverte des classes graphiques (bouton de démarrage, d’arrêt, de réinitialisation du chrono et affichage du temps dans une zone de texte, par exemple). Le début du fichier contient des instructions d’importation nécessaires vu le nombre de classes prédéfinies employées. La justification de leur présence est donnée en commentaires. import import import import java.util.*; java.awt.event.*; javax.swing.*; javax.swing.Timer; // // // // // pour Date pour ActionEvent et ActionListener pour JOptionPane pour éviter le conflit avec la classe Timer de java.util public class AppChrono{ public static void main(String [] args){ ActionListener chrono = new AfficheurDeTemps(); Timer t = new Timer(5000,chrono); t.start(); JOptionPane.showMessageDialog(null, "Fin du programme?"); System.exit(0); } } Chapitre 7 D’autres mécanismes - 98 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 class AfficheurDeTemps implements ActionListener{ private final Date topChrono = new Date(); public void actionPerformed(ActionEvent e){ Date now = new Date(); System.out.println("Temps écoulé: " + (now.getTime() topChrono.getTime())/1000 + " secondes"); } } Le garbage collector Il est temps de dire un mot de la manière dont les objets disparaissent. Si la naissance d’un objet de type A correspond à une instruction équivalente à A a = new A(); sa destruction peut se faire en douceur ou de manière un peu plus brutale. En Java, il existe un mécanisme appelé Garbage Collector (littéralement “collecteur d’ordures” ou “éboueur67”) quis e charge de rendre disponible la mémoire occupée par un objet dès que celui-ci n’est plus référencé. Cela ne signifie pas que l’objet disparaît, mais qu’il est susceptible de disparaître si des ressources en mémoire sont nécessaires. Le programmeur ne doit donc pas se préoccuper de détruire les objets. Ceux-ci sont détruits de manière automatique68. Il y a donc des constructeurs mais pas de destructeurs. Il peut cependant arriver que le programmeur souhaite cette disparition immédiate. C’est le cas, notamment, si d’autres ressources que la mémoire sont monopolisées par un objet, comme un fichier, par exemple. Dans ce cas, il peut forcer la collecte en utilisant la méthode finalize que possèdent toutes les classes puisqu’elle est héritée de la classe Object. Dans cette méthode, il pourra s’arranger pour décrire les traitements permettant de libérer les ressources libérables comme la fermeture d’un fichier, par exemple. Les packages La prolifération des classes conduit rapidement à envisager des possibilités de regroupement de cellesci, mais aussi leur archivage, dès que le programmeur considère qu’elles constituent des modules relativement bien élaborés. La solution offerte en Java pour résoudre ces difficultés s’appelle packages. 67 Le Francophone, plus délicat, parle de ramasse-miettes! 68 “Automatique” ne signifie pas “systématique”. Ainsi, le Garbage Collector n’agit pas toujours sauf en cas d’absolue nécessité car il exécute un travail de très faible priorité par rapport aux autres tâches. Chapitre 7 D’autres mécanismes - 99 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Les packages sont des unités de regroupement logique de classes, d’interfaces et éventuellement d’autres packages. Outre leur fonction de regroupement, les packages fonctionnent un peu comme des barrières, permettant au programmeur d’utiliser des noms plutôt génériques dans un contexte assez particulier en évitant la confusion avec les mêmes noms utilisés dans d’autres contextes69. De même, ils jouent un rôle dans l’accessibilité et la disponibilité de certains membres de classes. Déclaration La déclaration d’appartenance à un package se fait au niveau du fichier source, au tout début de celuici. Une telle déclaration doit être unique et engage donc toutes les classes et toutes les interfaces dont les définitions figurent dans le fichier source. Pour le compilateur, tous les noms des types déclarés dans un package sont préfixés du nom de ce package. Ce préfixe est ajouté automatiquement à la compilation. Voici un extrait d’un fichier source: package geometrie; class Point{ ... } class Droite{ ... } Dans cet exemple, les noms complets des classes sont en réalité geometrie.Point et geometrie.Droite. Lorsque le programmeur fait référence à une classe du package, il peut utiliser le nom court de la classe. A l’intérieur du package, la référence complète est inutile. En revanche, si la classe fait partie d’un autre package, il peut utiliser le nom complet. A supposer que la classe A ne fasse pas partie du package geometrie: class A{ ... geometrie.Point p = new geometrie.Point(); ... 69 Une classe Tableau pourrait être définie dans un package avec des fonctionnalités propre à une ou des applications particulières sans empêcher qu’une autre classe Tableau, avec d’autres fonctionnalités, soit définie dans un autre package. Chapitre 7 D’autres mécanismes - 100 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 } Il existe cependant une autre possibilité, celle d’importer tout ou partie de certains packages. Les classes de ces packages sont alors de nouveau accessibles par leur nom court. Toujours en supposant que la classe A ne fasse pas partie du package geometrie: import geometrie.*; public class A{ ... Point p = new Point(); ... } ou import geometrie.Point; ... Dans ce second cas, seule la classe Point sera accessible dans le package geometrie. Il est donc possible que des classes différentes portent le même nom court70. D’une manière générale, il est possible d’utiliser les noms longs des classes ou leur nom court, si leurs packages sont importés. Il est également possible de panacher les deux techniques. Les noms de packages L’idée de la réutilisabilité peut se limiter aux classes écrites par le programmeur lui-même, mais aussi s’étendre aux classes écrites par les programmeurs d’une organisation et, pourquoi pas, par les programmeurs du monde entier (la belle idée!). A cette fin, le choix des noms de packages doit être tel que l’unicité soit respectée dans toute l’étendue du domaine d’utilisation des classes qu’ils contiennent71. A l’échelle d’Internet, les choses ne sont pas simples à mettre en place. La recommandation qui est faite est d’utiliser le nom de domaine de l’organisation (dans l’ordre inverse) et une suite dont l’unicité peut être plus aisément gérée. 70 C’est particulièrement vrai en ce qui concerne les classes prédéfinies de Java comme, par exemple, les classes Date de java.util et de java.sql, Timer de java.util et javax.swing etc. 71 En interne, il est possible de mettre aux points diverses stratégies de nommage qui garantissent cette unicité. Chapitre 7 D’autres mécanismes - 101 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Le préfixe de tous les noms de packages regroupant les programmes développés par Etienne Vandeput travaillant aux FUNDP devraient commencer par BE.ac.fundp.eva (pour autant que les identifiants en trois lettres, première lettre du prénom et deux premières lettres du nom, soient gérés sans ambiguïté au niveau de l’organisation) Si chacun s’engageait à respecter cette règle, il existerait donc dans le monde un seul package dont le nom est BE.ac.fundp.eva.geometrie72. Il reste que les environnements de développement exigent que les fichiers contenant le code des classes d’un package se trouvent dans des répertoires particuliers et qu’il y ait des correspondances entre les noms des répertoires et ceux des packages. Nous allons examiner ces exigences par la suite. Accès Si les interfaces sont par essence d’accès public, il n’en est pas de même des classes. En l’absence de modificateur d’accès, elles sont accessibles de partout dans le package 73. De la sorte, l’accès par défaut est package. Rappelons qu’une classe a accès à toutes les classes de son package et aux classes publiques74 des autres packages. L’emploi des noms courts pour ces dernières n’a de sens que si les packages font l’objet d’une instruction d’importation. Dans le cas contraire, le nom long doit être employé. Hiérarchie Les packages peuvent être emboîtés à la manière des répertoires dans un système de fichiers. Cette possibilité permet aux programmeurs de créer une hiérarchie logique permettant aux utilisateurs potentiels de s’y retrouver sans trop de difficultés. Il n’y a pas d’accès privilégiés entre les membres de packages emboîtés. Les classes prédéfinies de Java se trouvent toutes dans la hiérarchie des packages java et javax. A titre indicatif, le package java ne contient pas de classes ni d’interfaces. Il ne contient que des packages comme, par exemple, le package java.lang. Le package java.lang contient des classes dont la classe Math, la classe String et bien d’autres mais contient aussi le package java.lang.ref qui, lui, ne contient que des classes. Il n’y a pas d’accès privilégiés entre les classes de java.lang.ref et celles de java.lang qui sont des packages tout à fait différents. 72 Le choix des majuscules pour le premier terme est une précaution supplémentaire pour éviter toute collision avec les noms de packages de ceux qui ne respectent pas la convention. 73 Si on ne précise pas à quel package appartiennent les éléments d’un fichier source, ils appartiennent au package par défaut qui est un package sans nom. 74 Pour rappel, dans un fichier contenant plusieurs définitions de classes, une seule peut être déclarée d’accès public. Les autres ne sont pas réutilisables! Souvent, il est nécessaire de répartir ces définitions dans plusieurs fichiers pour permettre un accès à ces différentes classes. Chapitre 7 D’autres mécanismes - 102 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Le programmeur est amené à créer ses propres packages, pour pouvoir réutiliser ses propres classes ou les faire partager à d’autres et pour créer une séparation logique entre les classes qu’il a développées et celles qu’il utilise (classes “système” et/ou classes provenant de diverses librairies). Stockage des classes Les classes sont stockées dans des fichiers .class mais peuvent aussi se cacher derrière des fichiers compressés (.jar pour Java archives)75. Le programmeur peut aussi créer ses propres fichiers .jar en se servant de l’utilitaire jar livré avec le kit de développement. La recherche des classes La notion de package trouve tout son sens dans un contexte de réutilisation des classes. Un fichier contient au plus une instruction package, fixant le package d’appartenance des classes et interfaces du fichier et éventuellement, une ou plusieurs instructions d’importation. Pour qu’une classe soit réutilisable, il faut qu’elle ait été déclarée public et enregistrée dans un package. La réutilisation se fait au moyen d’une instruction import. Les classes sont recherchées par l’interpréteur à divers endroits: • dans le répertoire courant, • dans certains répertoires d’installation de Java où les classes prédéfinies figurent à l’état compressé dans des fichiers particuliers, • en suivant le classpath et les informations correspondant aux packages des classes. Si des classes du packages par défaut (associé au classpath) font appel à des classes d’un package, ces dernières doivent se trouver dans des sous-répertoires correspondant à la hiérarchie définie par le package. Si le classpath est c:\jdk\mesclasses et qu’une classe AppDroite du package par défaut utilise une classe Point et une classe Droite, toutes deux importées du package eva.geometrie, les fichiers Point.class et Droite.class doivent se trouver en c:\jdk\mesclasses\eva\geometrie sans quoi l’interpréteur ne pourra accomplir son travail. Notez encore que le compilateur ne vérifie pas que les fichiers sources (.java) se trouvent dans les bons sous-répertoires. Il y a donc intérêt à leur faire respecter la hiérarchie des fichiers .class. Exercices Ex 1 Faites imprimer le calendrier du mois en cours en marquant d’un astérisque le chiffre du jour. Utilisez un modèle semblable au suivant: Mai 2002 75 Dans l’environnement Windows, les classes prédéfinies de Java se retrouvent dans les fichiers i18n.jar et rt.jar du sous-répertoire \jre\lib créé dans le dossier d’installation de Java. Chapitre 7 D’autres mécanismes - 103 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Lu Ma Me Je Ve Sa Di 1 2 3 4 5 6 7 8* 9 10 ... Ex 2 Faites afficher l’heure toutes les minutes dans une fenêtre de l’OS. Ex 3 Ecrivez une application utilisant les classes abstraites pour implanter le type abstrait file, de sorte que ce type soit utilisable avec n’importe quel type de données (files d’entiers, files de chaînes de caractères, files de booléens et même file de files de...). Solution Il existe plusieurs solutions à ce type de problème. Une solution consiste à créer une classe FileDe, puis de l’étendre par différentes classes telles FileDEntiers, FileDeChaines,... Une autre solution, celle qui est choisie ici, consiste à définir le type des informations stockées dans la file comme un paramètre. On définit les opérations habituelles sur une file. On fait un choix pour l’implantation de la file, par exemple, une liste simplement chaînée. On considère que le type T est un paramètre de la file et on définit une classe T qui va pouvoir être étendue de diverses manières. Les opérations sur une file: • • • • la créer; vérifier si elle est vide ou non; y ajouter un élément; en retirer un élément (le premier de la file) et en avoir connaissance; plus d’autres opérations moins fondamentales mais toutefois utiles telles: • afficher les éléments de la file; • vérifier si deux files sont égales ou non; • ... La file est implantée de la manière suivante: un pointeur vers le premier et un pointeur vers le dernier élément de la file, ce qui simplifie à la fois les procédures d’affichage, de retrait et d’ajout d’éléments. La procédure de création est déclarée static et le code des méthodes classiques de traitement de la file ne posent pas de gros problèmes. public class File { Element premier; Element dernier; public static File creer(){ return new File(); } public boolean estVide(){ return premier==null; Chapitre 7 D’autres mécanismes - 104 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 } public void ajouter(Element e){ if (estVide()){ premier = e; } else{ dernier.suivant = e; } dernier = e; } public T retirer(){ T t = premier.info; premier = premier.suivant; return t; } ... Observez qu’on retire un élément de type T (a priori inconnu). On suppose que cette méthode n’est d’application que lorsque l’on est sûr que la file n’est pas vide! La méthode d’ajout est légèrement différente selon que la file est vide ou non (affectation de premier si la file est vide). La méthode d’affichage demande un petit commentaire. L’affichage de la file est reporté sur l’affichage de chacun de ses éléments, à partir du premier. La méthode egal, qui permet de comparer deux files, démarre à partir des premiers éléments (s’ils existent) de chacune des files. La boucle n’est pas parcourue si la comparaison de deux éléments a fourni un résultat négatif (r a la valeur false) ou si l’un des deux éléments n’existe pas. Dans les autres cas, on avance dans les deux files et on compare. La valeur retournée est celle de r corrigée en observant que la valeur true n’a de sens que si les deux pointeurs s’évaporent en même temps. ... public boolean egal(File f) { Element e1, e2; e1 = this.premier; e2 = f.premier; boolean r = true; while (r && (e1!=null && e2!=null)){ r = (e1.info.egal(e2.info)); e1 = e1.suivant; e2 = e2.suivant; } return r && (e1==null && e2==null); } Chapitre 7 D’autres mécanismes - 105 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 public void afficher(){ Element e = premier; while (e != null){ e.affiche(); e = e.suivant; } } } Les files sont définies à partir de leur premier et de leur dernier élément. Il convient donc de définir une classe Element qui donnera à la file les caractéristiques d’une liste chaînée (un Element possède une référence vers un suivant. L’information contenu dans un Element est de type T (inconnu). public class Element { T info; Element suivant; public Element(T t) { info = t; } public void affiche(){ info.affiche(); } } On construit un Element en lui fournissant une info de type T. La responsabilité de l’affichage est reportée sur la classe T. Le type inconnu est représenté par une classe T qui est une classe abstraite définissant deux méthodes, une méthode egal pour la comparaison de deux éléments de type T et une méthode affiche, toutes deux indéfinissables à ce niveau. Ces méthodes sont donc abstraites. public abstract class T { public abstract boolean egal(T t); public abstract void affiche(); } Il reste à exploiter la définition de cette classe abstraite en l’étendant par des classes concrètes correspondant à des informations de divers types: entiers, chaînes de caractères,... Les définitions de classes qui suivent illustrent cette possibilité. Chapitre 7 D’autres mécanismes - 106 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 public class Entier extends T { int valeur; public Entier(int i){ valeur = i; } public boolean egal(T e){ Entier en = (Entier)e; return (valeur == en.valeur); } public void affiche(){ System.out.print(valeur + " "); } } Une instruction mérite un commentaire, c’est l’instruction: Entier en = (Entier)e; Il s’agit d’un transtypage. En effet, le compilateur oblige la méthode egal à prendre un paramètre de type T. De ce fait, l’information valeur n’est pas directement accessible via la variable e. Le transtypage permet d’affecter à une nouvelle variable, la référence vers l’objet réel, ce qui permet de retrouver l’information recherchée. On retrouve des définitions assez semblables dans la classe ChaineDeCaracteres. public class ChaineDeCaracteres extends T { String valeur; public ChaineDeCaracteres(String s){ valeur = s; } public boolean egal(T s){ ChaineDeCaracteres st = (ChaineDeCaracteres)s; return (valeur.equals(st.valeur)); } public void affiche(){ System.out.print(valeur + " "); } } On utilise également le transtypage et la méthode equals, particulière à la classe String, effectue la comparaison. Chapitre 7 D’autres mécanismes - 107 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Enfin, il reste à écrire une classe application pour tester tout cela. Deux files d’entiers sont constituées, par ajout et retraits d’éléments. Des affichages et des comparaisons sont effectués. Une file de chaînes de caractères est également créée et manipulée. import eva.abstracttypes.*; public class AppAbstract5 { public static void main(String [] args){ // Files de nombres entiers // Création d'une première file File f = File.creer(); f.ajouter(new Element(new Entier(4))); f.ajouter(new Element(new Entier(-2))); f.ajouter(new Element(new Entier(9))); // Affichage System.out.print("Première file: "); f.afficher(); System.out.println(); // Retrait d'éléments System.out.print("Je retire "); f.retirer().affiche(); f.retirer().affiche(); System.out.println(); // Nouvel affichage System.out.print("Il reste "); f.afficher(); System.out.println(); // Création d'une deuxième file File f2 = File.creer(); f2.ajouter(new Element(new Entier(9))); // Affichage System.out.print("Deuxième file: "); f2.afficher(); System.out.println(); // Comparaison des files if (f.egal(f2)) System.out.println("Les deux files sont égales."); else System.out.println("Les deux files sont différentes."); Chapitre 7 D’autres mécanismes - 108 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 // Nouveau retrait System.out.print("Je retire "); f.retirer().affiche(); System.out.println(); // Nouvelle comparaison if (f.egal(f2)) System.out.println("Les deux files sont égales."); else System.out.println("Les deux files sont différentes."); // File de chaînes de caractères // Création d'une file File f3 = File.creer(); f3.ajouter(new Element(new ChaineDeCaracteres("Programmation OO"))); f3.ajouter(new Element(new ChaineDeCaracteres("Java"))); f3.ajouter(new Element(new ChaineDeCaracteres("Machine virtuelle"))); // Affichage System.out.print("Troisième file: "); f3.afficher(); System.out.println(); // Retrait d'un élément System.out.print("Je retire "); f3.retirer().affiche(); System.out.println(); // Nouvel affichage System.out.print("Il reste "); f3.afficher(); System.out.println(); } } Toutes sortes de classes représentant divers types d’informations peuvent désormais étendre la classe T. Chapitre 7 D’autres mécanismes - 109 - Programmer avec des objets Chapitre 8 Etienne Vandeput ©CeFIS 2002 GUI et gestion des événements Le modèle des événements Lorsqu’une application se déroule en arrière-plan d’un environnement graphique, et c’est souvent le cas des logiciels qui doivent interagir avec leur utilisateur, l’environnement d’exécution doit gérer une série d’événements susceptibles de se produire (c-à-d., vérifier constamment s’ils se produisent ou non). Y a-t-il eu un clic de souris sur tel bouton, l’utilisateur a-t-il modifié le contenu de telle zone de texte, a-t-il choisi un item dans une liste, un délai s’est-il écoulé, etc. Avec certains langages76, les objets graphiques77 et les événements qui leur sont associés sont prédéfinis. Il reste au programmeur à décrire le code correspondant à tel et tel événement associé à tel ou tel contrôle graphique. En Java, le modèle des événements est un peu plus sophistiqué, permettant davantage de souplesse. Si les différentes sources d’événements (boutons, barres de défilement, zones de texte,...) sont fixés, Java laisse au programmeur la possibilité de désigner les objets qui sont à l’écoute de ces événements et qui peuvent décider des traitements à effectuer. Ce regain de souplesse s’obtient au détriment d’une légère complexification du code. Mais son analyse, un peu plus délicate, est une question d’habitude. Il faut donc distinguer plusieurs acteurs (classes d’objets) dans la gestion des événements. • Les sources d’événements sont des objets dont les classes possèdent des méthodes permettant leur association à un ou plusieurs écouteurs d’objets. Lorsqu’un événement leur “arrive”, ils notifient tous les écouteurs qui leur sont associés en leur envoyant un objet événement qui contient diverses informations concernant l’événement qui s’est produit. • Les événements sont des objets créés à l’initiative des sources d’événements et qui sont destinés aux écouteurs d’événements. • Les écouteurs d’événements qui peuvent être des objets de n'importe quelles classes pourvu qu'elles implantent une ou plusieurs interfaces spécifiques: < ActionListener < MouseListener < FocusListener < … ou qu'elles étendent une classe particulière: 76 Visual Basic, par exemple 77 ...aussi appelés contrôles graphiques. Chapitre 8 GUI et gestion des événements - 110 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 < WindowAdapter < MouseAdapter < ... Exemple de création d’une association source-écouteur: ActionListener objetEcouteur = new ...; // Objet quelconque dont la classe // implante ActionListener Button b = new Jbutton(“Démarrer”); // Crée un bouton dont le libellé // est “Démarrer” b.addActionListener(objetEcouteur); NB Ces instructions ne suffisent pas. Il en faut d’autres, par exemple pour que le bouton s’affiche. Comme nous l’avons déjà vu dans un exemple du chapitre précédent, une classe qui implante l’interface ActionListener doit décrire une méthode appelée actionPerformed et qui prend en paramètre un objet de type ActionEvent. public void actionPerformed(ActionEvent e){ ... } La gestion des événements est donc réalisée de la manière suivante: lorsque l’objet source constate que l’événement se produit, il invoque la méthode appropriée de tous les écouteurs de ce type d’événement en leur passant comme paramètre un objet événement dont les informations pourront servir aux objets écouteurs. Si l’objet écouteur est un MouseListener, il devra savoir que faire lors de l’invocation des cinq méthodes mouseClicked, mouseEntered, mouseExited, mousePressed et mouseReleased. Bien entendu, ces cinq méthodes devront être définies dans la classe de l’objet écouteur, quitte à ce que cette définition soit vide pour la plupart. Par exemple, on peut seulement s’intéresser à l’endroit où le clic s’est produit. Dans ce cas, seule la définition de la méthode mouseClicked contiendra des instructions. Comme il y a souvent plusieurs méthodes dans la définition d’une interface, il est parfois préférable d’étendre une classe, plutôt que d’implanter une interface. Bien sûr, une classe ne peut étendre qu’une seule autre classe, mais il n’est pas nécessaire de redéfinir les méthodes qui ne sont pas exploitées si celles-ci sont définies à plus haut niveau. Chapitre 8 GUI et gestion des événements - 111 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Par exemple, la gestion de la fermeture d’une fenêtre peut se faire par un objet d’une classe étendant la classe WindowAdapter Il existe aussi une classe MouseAdapter. Il n’est évidemment pas possible de décrire toutes les interfaces et toutes les classes prédéfinies qui sont au service des objets potentiellement écouteurs d’événements. Nous vous renvoyons pour cela à la documentation. Les objets événements générés dépendent du type d’événement qui s’est produit et ne sont envoyés aux objets écouteurs que dans la mesure où ces objets sont à l’écoute de ce type d’événement. Si la classe d’un objet implante l’interface FocusListener, elle devra définir deux méthodes: focusGained et focusLost. L’objet fera partie de la liste des écouteurs d’une ou de plusieurs sources. Lorsque l’une de ces sources perdra le focus, elle invoquera la méthode focusLost de l’objet avec comme paramètre un FocusEvent. AWT et Swing Le modèle des événements est développé au travers des classes et des interfaces du package java.awt. AWT signifie Abstract Windowing Toolkit. C’est une boîte à outil pour réaliser du fenêtrage de manière abstraite. Le package contient également des définitions pour les composants tels, les boutons de toutes sortes, les listes déroulantes, les zones de texte,... Toutefois, ces composants sont considérés comme lourds dans la mesure où ils ont été écrits en tenant compte des spécificités des OS. De nouveaux composants ont été réécrits en Java et sont moins dépendants de tel ou tel environnement. Ils font partie du package javax.swing. Pour ce qui est des composants, il est déconseillé d’utiliser les composants d’AWT78 qui seront, à terme, fortement dépréciés. Que de hiérarchies! Ces considérations nous amènent à examiner, une fois de plus, les nombreuses classes prédéfinies de Java. C’est une démarche inévitable dans un contexte de développement. Nous citerons simplement ici la hiérarchie des objets graphiques, que l’on retrouve en double, compte tenu de la remarque qui précède. Il existe une classe Button dans le package java.awt et une classe Jbutton dans le package javax.swing. Elles n’ont rien à voir l’une avec l’autre, même si leur but est d’offir les fonctionnalités attendues d’un bouton. Leurs branches hiérarchiques se rejoignent au niveau de la classe Component qui est une sous-classe directe de la classe Object. Il en est de même pour la plupart des autres 78 Un exemple: préférez la classe JTextField du package javax.swing à la classe TextField du package java.awt. Chapitre 8 GUI et gestion des événements - 112 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 composants graphiques avec toutefois une exception notable pour les classes Frame et JFrame, JFrame héritant directement de Frame. Nous pouvons évoquer également la hiérarchie des événements qui héritent tous de la classe AWTEvent, mais également la hiérarchie des exceptions dont nous parlerons plus loin. Il convient de vous documenter un peu à ce sujet, dans la mesure où presque toutes ces classes héritent d’un nombre considérable de méthodes. Interfaces et programmation événementielle Afin d’illustrer convenablement le concept d’interface développé au chapitre précédent et le fonctionnement du modèle des événements, intéressons-nous à deux applications. La première, montre que le concept d’interface est utile dans le cadre de la programmation événementielle. Plusieurs interfaces sont en effet prédéfinies et leur utilisation facilite ce type de programmation. La seconde met en évidence le fait que le programmeur est parfois amené à construire lui-même ses propres interfaces pour conférer à ses modules (objets, programmes, logiciels) des qualités de réutilisabilité. C’est particulièrement le cas lors de la conception de logiciels ayant une composante importante d’interaction avec leurs utilisateurs. On imagine qu’il est possible de développer des écrans de saisie, d’affichage, agrémentés de contrôles graphiques, soignant particulièrement certains aspects importants de la conception des IHMs (interfaces homme-machine). Par ailleurs, il est raisonnable de penser que le développement de l’application proprement dite, n’aura que peu à voir avec le développement de l’environnement de travail de l’utilisateur. Il serait également stupide de considérer que ces deux catégories d’objets (écrans et objets de l’application) n’ont rien en commun. Nous verrons que le concept d’interface, tel qu’il est prévu en Java, permet de dresser des sortes de contrats entre les développeurs des deux catégories, autorisant par là un travail assez indépendant des uns et des autres. Chacune de ces deux applications sera traitée en plusieurs phases, permettant l’introduction de l’un ou l’autre concept supplémentaire, suite à la rencontre de certaines difficultés. Le détecteur de multiples de 7 Dans un champ texte d’une boîte de dialogue classique, on veut pouvoir introduire des nombres dont un programme vérifie qu’ils sont multiples de 7 ou non. Première version Elle fait afficher une boîte de dialogue contenant un champ texte permettant d’introduire un nombre entier et un bouton demandant à l’application de vérifier si ce nombre est un multiple de 7. Le résultat Chapitre 8 GUI et gestion des événements - 113 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 est affiché sous forme d’une étiquette79. Cette version ne contrôle pas certains événements tels que la terminaison correcte du programme à la fermeture de la boîte de dialogue ou encore, l’introduction de valeurs erronées (des caractères qui ne soient pas des chiffres) ou de valeurs trop élevées. Intéressons-nous d’abord à la création de la boîte de dialogue. Celle-ci sera l’instance d’une classe qui héritera d’une classe prédéfinie, afin de profiter de tout une série de fonctionnalités (bouton de fermeture, de maximisation ou de réduction en icône, barre de titre, etc.). Cette classe est la classe JFrame80. L’événement essentiel est, évidemment, le clic sur le bouton de vérification. Un objet d’une classe implantant une interface d’écoute doit être à l’écoute de ce bouton qui détectera l’événement et lui enverra un message, le cas échéant. Il est toujours possible, et c’est parfois souhaitable, de créer de toute pièce une classe qui donnera naissance à ce type d’objet écouteur. Dans notre cas, puisqu’une classe est nécessaire à la construction de la boîte de dialogue, il nous est possible de faire d’une pierre deux coups en demandant à la boîte de dialogue elle-même d’être à l’écoute du bouton81. La classe à construire s’appelle BoiteMultiple et doit donc, à la fois, implanter l’interface ActionListener et étendre la classe JFrame. Les groupes d’instructions nouvelles sont partiellement commentés dans le code qui suit. La documentation sur les classes et méthodes utilisées est disponible en ligne. class BoiteMultiple extends JFrame implements ActionListener { JButton b; JTextField jt; JLabel lm; // Construction de la boîte de dialogue public BoiteMultiple(){ // Paramétrage de la fenêtre setTitle("Multiple de 7?"); setSize(450,80); setLocation(300,300); // Obtention d'une référence vers le conteneur Container c = getContentPane(); 79 Une étiquette ou label est un composant d’une boîte de dialogue que l’utilisateur ne peut modifier de manière dynamique. Cela ne signifie pas que le programme ne puisse pas le faire. C’est d’ailleurs ce qu’on lui demande ici. Normalement, les étiquettes sont utilisées pour étiqueter les champs de données. 80 Pour rappel, cette classe est une exception à la réécriture complète des composants graphiques puisqu’elle hérite directement de la classe Frame du package java.awt. 81 Cette façon de faire est souvent économique (en lignes de code) dans le cas où le nombre d’événements à contrôler n’est pas très élevé. Chapitre 8 GUI et gestion des événements - 114 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 // Création d'un gestionnaire de mise en page pour le conteneur FlowLayout fl = new FlowLayout(); c.setLayout(fl); // Création des composants de la fenêtre JLabel ln = new JLabel("Nombre à examiner:"); jt = new JTextField(8); b = new JButton("Vérifier"); lm = new JLabel("Multiple de 7: "); // Ajout des composants au conteneur c.add(ln); c.add(jt); c.add(b); c.add(lm); // Ajout de l'objet courant à la liste des écouteurs du bouton b.addActionListener(this); } // Implantation de la méthode d'écoute public void actionPerformed(ActionEvent ae){ if (ae.getSource()==b){ int n = Integer.parseInt(jt.getText()); if (n % 7 == 0) lm.setText("Multiple de 7: oui"); else lm.setText("Multiple de 7: non"); } } } Signalons encore quelques petits détails. Une boîte de dialogue doit être mise en page. Cette mise en page est réalisée grâce à un objet de la classe FlowLayout 82. La mise en page que gère83 cet objet est la mise en page la plus simple. Les composants se suivent à la queue leu leu. Par exemple, lorsque le gérant ne dispose plus de suffisamment de place, vu les dimensions de la fenêtre, il dispose les suivants en-dessous. Pour des raisons qui tiennent plus à des adaptations qu’à de la conception pure et simple, la mise en page ne peut s’appliquer à un objet de type JFrame, mais à un objet de type Container. C’est ce qui justifie l’emploi de la méthode getContentPane qui renvoie une référence vers le “panneau de contenu” de la boîte de dialogue 84. 82 Il existe aussi une classe GridLayout qui permet notamment de définir des lignes et des colonnes et une classe BorderLayout dont la mise en page un peu plus sophistiquée définit cinq régions: une région centrale et quatre autres correspondant aux points cardinaux. 83 C’est bien un gérant car sa classe implante l’interface LayoutManager. 84 Pour les curieux, un objet de type JFrame contient un champ rootPane de type JRootPane et les objets de type JRootPane ont un champ contentPane de type Container. C’est cette référence qui est renvoyée par la méthode getContentPane. Et vive la hiérarchie et l’héritage! Chapitre 8 GUI et gestion des événements - 115 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Les objets de type JButton, JTextField et JLabel, préférés aux objets de type Button, TextField et Label ont différents constructeurs. Nous en avons utilisé l’un ou l’autre. Voyez à nouveau la documentation pour de plus amples informations. Ces objets qui sont des composants de la fenêtre peuvent être ajoutés à l’objet Container associé grâce à la méthode add. Il faut aussi ajouter l’objet courant, en l’occurrence, la boîte de dialogue elle-même, à la liste des écouteurs du bouton. Pour les mêmes raisons, la méthode actionPerformed doit être implantée à ce niveau. Cette méthode effectue un simple calcul avant de modifier le label contenant le résultat du test. Enfin, signalons que le test sur la source de l’événement n’est pas bien nécessaire puisque le bouton est le seul objet écouté par l’écouteur. Il serait utile dans la mesure où l’écouteur écoute différents objets, ce qui est évidemment tout à fait possible. L’application qui exploite la classe venant d’être définie est relativement simple. Elle crée une boîte de dialogue et la rend visible85. public class AppGUI{ public static void main(String [] args){ BoiteMultiple bm = new BoiteMultiple(); bm.setVisible(true); } } Il convient de ne pas oublier les instructions d’importations nécessaires vu le nombre de classes prédéfinies utilisées. import java.awt.*; import java.awt.event.*; import javax.swing.*; Nous avons évoqué la possibilité de créer des objets écouteurs tout à fait indépendants des objets graphiques. C’est une possibilité qui donne de la souplesse à la programmation. Dans le cas qui nous occupe, et pour des raisons que nous avons déjà évoquées (trop peu d’événements à contrôler), cette façon de procéder complique un peu les choses. Voici toutefois une version du programme précédent qui s’en inspire, en définissant une classe EcouteBouton. Observez que les composants graphiques gardent l’accès package, qui est bien nécessaire à cette classe EcouteBouton. import java.awt.*; import java.awt.event.*; import javax.swing.*; 85 Il ne suffit pas de disposer des composants dans une fenêtre pour que celle-ci soit visible. L’instruction nécessaire invoque la méthode setVisible qui prend un booléen comme paramètre. Cette méthode peut être appelée par le constructeur comme par une application extérieure (c’est le cas ici). Chapitre 8 GUI et gestion des événements - 116 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 public class AppGUI1{ public static void main(String [] args){ BoiteMultiple bm = new BoiteMultiple(); bm.setVisible(true); EcouteBouton eb = new EcouteBouton(bm); bm.setEcouteBouton(eb); } } class BoiteMultiple extends JFrame { JButton b; JTextField jt; JLabel lm; private EcouteBouton eb; // Construction de la boîte de dialogue public BoiteMultiple(){ // Paramétrage de la fenêtre setTitle("Multiple de 7?"); setSize(450,80); setLocation(300,300); // Obtention d'une référence vers le conteneur Container c = getContentPane(); // Création d'un gestionnaire de mise en page pour le conteneur FlowLayout fl = new FlowLayout(); c.setLayout(fl); // Création des composants de la fenêtre JLabel ln = new JLabel("Nombre à examiner:"); jt = new JTextField(8); b = new JButton("Vérifier"); lm = new JLabel("Multiple de 7: "); // Ajout des composants au conteneur c.add(ln); c.add(jt); c.add(b); c.add(lm); } public void setEcouteBouton(EcouteBouton eb){ this.eb = eb; // Ajout de l'écouteur à la liste des écouteurs du bouton b.addActionListener(eb); } } Chapitre 8 GUI et gestion des événements - 117 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 class EcouteBouton implements ActionListener { BoiteMultiple bm; public EcouteBouton(BoiteMultiple bm){ this.bm = bm; } // Implantation de la méthode d'écoute public void actionPerformed(ActionEvent ae){ if (ae.getSource()==bm.b){ int n = Integer.parseInt(bm.jt.getText()); if (n % 7 == 0) bm.lm.setText("Multiple de 7: oui"); else bm.lm.setText("Multiple de 7: non"); } } } Deuxième version: contrôle de fermeture de la fenêtre Pour éviter que le programme ne soit inutilisable après que l’utilisateur ait cliqué sur le bouton de fermeture de la fenêtre, il convient de mettre un objet à l’écoute de cet événement. Ceci serait possible en demandant à la classe de cet objet d’implanter une interface qui s’appelle WindowListener. Nous allons cependant utiliser une autre opportunité, celle d’étendre une classe, plutôt que d’implanter une interface. L’avantage réside dans le fait qu’il n’est pas nécessaire de redéfinir toutes les méthodes de la classe étendue (en tous cas, pas celles qui ont déjà une définition à un niveau supérieur). L’inconvénient, c’est que cette classe ne peut plus en étendre une autre. Comme ce n’est pas toujours nécessaire, c’est au programmeur de choisir la solution qui lui convient. La classe étendue s’appelle WindowAdapter86. Des sept méthodes que possède cette classe, nous n’en réécrirons qu’une seule, la méthode windowClosing. Nous y invoquons la méthode exit de la classe System. Voici (en caractères gras) les changements par rapport au code de la première application... class EcouteFenetre extends WindowAdapter { public void windowClosing(WindowEvent we){ System.exit(0); } } ...et au niveau de l’application elle-même... public class AppGUI2{ 86 Inutile de dire que cette classe implante elle-même l’interface WindowListener. Et nous retombons sur nos pattes! Chapitre 8 GUI et gestion des événements - 118 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 public static void main(String [] args){ BoiteMultiple bm = new BoiteMultiple(); bm.setVisible(true); EcouteFenetre ef = new EcouteFenetre(); bm.addWindowListener(ef); } } De la sorte, la fermeture de la fenêtre entraîne la fin du programme et l’évaporation de l’interpréteur Java. Troisième version: classe anaonyme A nouveau, la solution qui vient d’être proposée nous oblige à définir une classe supplémentaire. Il existe une possibilité de créer l’objet nécessaire “en plein vol”. Il faut pour cela utiliser la technique des classes anonymes. Le principe est le suivant. L’objet est créé comme un objet d’une classe anonyme (héritant dans notre exemple de WindowAdapter) dont le code suit à l’intérieur même de l’instruction. public class AppGUI3{ public static void main(String [] args){ BoiteMultiple bm = new BoiteMultiple(); bm.setVisible(true); bm.addWindowListener(new WindowAdapter(){ public void windowClosing(WindowEvent we){ System.exit(0); } } ); } } L’écriture est un peu plus compliquée à déchiffrer mais si la classe ne sert qu’à une seule reprise, cela évite de devoir rajouter une définition de classe structurée comme à l’habitude. Quatrième version: autre système d’écoute et traitement des exceptions L’idée est ici de rendre l’application plus conviviale, d’une part, en n’obligeant pas l’utilisateur à cliquer inutilement sur un bouton, d’autre part, en multipliant les occasions d’effectuer la mise à jour. Concrètement: • le bouton disparaît; • la mise à jour est effectuée lorsque l’utilisateur valide le contenu du champ texte ou lorsqu’il place le focus sur un autre élément de la boîte de dialogue. Chapitre 8 GUI et gestion des événements - 119 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Il va s’agir ici de définir un nouveau type d’écouteurs, les “écouteurs de focus” et de faire en sorte que la boîte de dialogue soit à l’écoute des deux types d’événements. Il faudra également faire en sorte que des données incorrectes provoquent une remise à blanc des champs de données concernés. Voici la nouvelle définition de la classe BoiteMultiple avec mise en évidence des principaux changements. class BoiteMultiple extends JFrame implements ActionListener, FocusListener { // Disparition du bouton JTextField jt; JLabel lm; // Construction de la boîte de dialogue public BoiteMultiple(){ // Paramétrage de la fenêtre setTitle("Multiple de 7?"); setSize(450,80); setLocation(300,300); // Obtention d'une référence vers le conteneur Container c = getContentPane(); // Création d'un gestionnaire de mise en page pour le conteneur FlowLayout fl = new FlowLayout(); c.setLayout(fl); // Création des composants de la fenêtre JLabel ln = new JLabel("Nombre à examiner:"); jt = new JTextField(8); lm = new JLabel("Multiple de 7: "); // Ajout des composants au conteneur c.add(new JButton("OK")); // Pour tester la perte de focus c.add(ln); c.add(jt); c.add(lm); // Ajout de l'objet courant à la liste des écouteurs du champ texte jt.addActionListener(this); jt.addFocusListener(this); } // Implantation des méthodes d'écoute // Pour l'interface ActionListener public void actionPerformed(ActionEvent ae){ mettreAJour(); } // Pour l'interface FocusListener Chapitre 8 GUI et gestion des événements - 120 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 public void focusGained(FocusEvent fe){} public void focusLost(FocusEvent fe){ mettreAJour(); } // Méthode de mise à jour (économie d'échelle) public void mettreAJour(){ try{ int n = Integer.parseInt(jt.getText()); if (n % 7 == 0) lm.setText("Multiple de 7: oui"); else lm.setText("Multiple de 7: non"); } catch(NumberFormatException nfe){ jt.setText(""); lm.setText("Multiple de 7: "); } } } Observez que les instructions concernant le bouton ont disparu. Un bouton “bidon” a cependant été rajouté pour nous permettre de tester le programme sur la perte de focus. Sans ce bouton, il est impossible de donner le focus à un autre objet que le champ texte. La source d’événements est, cette fois, le champ texte avec deux événements possibles, la validation du champ (qui génère un ActionEvent) et la perte de focus (qui génère un FocusEvent). Dans les deux cas, c’est le même code qui doit être exécuté, ce qui justifie la création d’une méthode mettreAJour qui permet une économie d’échelle. La classe BoiteMultiple doit aussi implanter l’interface FocusListener. Il y a donc ici comme une espèce d’héritage multiple. Cette interface oblige la définition des deux méthodes que sont focusGained et focusLost. La première ne nous intéressant pas ici, nous laissons sa définition vide. Pour la seconde, nous l’avons dit, le code est le même que dans le cas d’une validation. Le traitement des exceptions Le test de validité de la donnée s’effectue grâce à la détection d’une exception et au moyen du bloc try... catch... dont il a déjà été question au chapitre 3. Si l’exception se produit, le bloc try est abandonné au profit du bloc catch. Il existe évidemment un nombre considérable de types d’exceptions. Tous ces types héritent du type Exception. Une des manières de les découvrir est de regarder les messages fournis par un programme qui ne rend pas les services espérés. En effet, en Java, lorsque l’on demande aux programmes des choses impossibles, ceux-ci génèrent des objets qui sont des exceptions 87. Les exceptions sont de deux sortes, les exceptions vérifiées et les autres. Les exceptions vérifiées sont rares. Elles 87 Belle manière de transformer la réalité: il n’y a plus d’erreurs, rien que des objets qui s’appellent Exception. Chapitre 8 GUI et gestion des événements - 121 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 correspondent aux situations qui nécessitent une terminaison correcte du programme88. Si le code du programme est tel que certaines de ces exceptions peuvent se produire, le compilateur réclame au programmeur un traitement particulier. La plupart des exceptions sont “non vérifiées” et le programmeur a la liberté de les traiter ou non. Pour ce faire, Java lui fournit la structure try... catch... Il n’est pas souhaitable de gérer tous les cas d’exceptions, au risque d’alourdir considérablement les programmes. Il vaut parfois mieux réagir en fonction du fonctionnement de ce programme et prendre en compte les exceptions qui se produisent le plus fréquemment, par exemple. Cinquième version: une mise à jour extrêmement dynamique Et pourquoi pas demander au programme de contrôler systématiquement les modifications effectuées dans le champ texte pour réaliser sa mise à jour? De nouveau, il s’agit d’un changement de source d’événement. Ce n’est plus le champ texte qui constate l’événement, c’est l’objet, de type Document, qui lui est associé. Une référence vers cet objet peut être obtenue par la méthode getDocument de la classe JTextField. L’interface à implanter est cette fois DocumentListener et les méthodes à définir sont insertUpdate, removeUpdate et changedUpdate. Seules les deux premières nous intéressent. Voici à nouveau les principaux changements dans la définition de la classe BoiteMultiple. class BoiteMultiple extends JFrame implements DocumentListener { JTextField jt; JLabel lm; // Construction de la boîte de dialogue public BoiteMultiple(){ // Paramétrage de la fenêtre setTitle("Multiple de 7?"); setSize(450,80); setLocation(300,300); // Obtention d'une référence vers le conteneur Container c = getContentPane(); // Création d'un gestionnaire de mise en page pour le conteneur FlowLayout fl = new FlowLayout(); c.setLayout(fl); // Création des composants de la fenêtre JLabel ln = new JLabel("Nombre à examiner:"); jt = new JTextField(8); lm = new JLabel("Multiple de 7: "); 88 Un programme ne peut monopoliser une ressource indéfiniment (une imprimante, par exemple). Chapitre 8 GUI et gestion des événements - 122 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 // Ajout des composants au conteneur c.add(ln); c.add(jt); c.add(lm); // Ajout de l'objet courant à la liste des écouteurs du "document" // associé jt.getDocument().addDocumentListener(this); } // Implantation des méthodes d'écoute // Pour l'interface DocumentListener public void insertUpdate(DocumentEvent de){ mettreAJour(); } public void changedUpdate(DocumentEvent de){} public void removeUpdate(DocumentEvent de){ mettreAJour(); } // Méthode de mise à jour (économie d'échelle) public void mettreAJour(){ try{ int n = Integer.parseInt(jt.getText()); if (n % 7 == 0) lm.setText("Multiple de 7: oui"); else lm.setText("Multiple de 7: non"); } catch(NumberFormatException nfe){ lm.setText("Multiple de 7: "); } } } Le compteur interactif Cette application va nous permettre d’utiliser des techniques d’implantation que nous connaissons déjà, mais aussi d’illustrer la possibilité et l’intérêt de créer nos propres interfaces. Elle va aussi nous donner l’occasion d’évoquer modestement la modélisation et les systèmes de représentation liés à la POO. Une boîte de dialogue très simple se compose d’un bouton et d’un champ texte. On souhaite que cette petite interface graphique affiche dans le champ texte la valeur d’un compteur tout en permettant d’interagir avec lui, par l’intermédiaire du bouton. Cette première description du problème nous amène à réfléchir séparément sur les deux objets dont il est manifestement question ici: le compteur et l’interface graphique qui permet de la contrôler. Diagramme de classes Chapitre 8 GUI et gestion des événements - 123 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Le monde de la POO fait la part belle aux objets, décrivant les programmes et les applications comme des conversations entre ces objets. On ne s’étonnera donc pas d’apprendre que le diagramme de classes est un des diagrammes les plus fréquemment cités dans les multiples techniques de modélisation OO. Le diagramme de classes est un diagramme statique. Il se contente de décrire les classes d’objets en présence et la manière dont les uns et les autres sont en relation. Dans les cas simples que nous traitons, les classes d’objets, et donc les relations entre elles sont assez facilement mises en évidence. Dans la pratique du génie logiciel, le diagramme des classes est généralement dérivé d’une étude sérieuse du système d’information et d’un schéma ERA89 complet. Dans le schéma, les classes sont représentées par des rectangles. Ces rectangles contiennent, au minimum le nom de la classe, au maximum, le nom de la classe, le nom et le type d’accès des champs, la signature des méthodes, les types des éventuels résultats et les types d’accès. Les types d’accès sont symbolisés par les signes + (public), - (private) et # (protected). Un objet de type Compteur possède un champ valeur de type int et doit pouvoir s’augmenter, se remettre à zéro ou encore, fournir sa valeur. Un objet de type EcranCompteur doit pouvoir s’afficher, s’associer à un compteur et réagir à l’événement qui est la pression du bouton d’incrémentation. Nous aurons donc le schéma suivant: Compteur - valeur: Entier = 0 + incrementer() + remettreAZero() + getValeur(): Entier EcranCompteur - jt: JTextField - b: JButton - unCompteur: Compteur + afficher() + setCompteur(unCompteur: Compteur) + traiterEvenement(e: ActionEvent) Quelques observations sont à faire. Les classes, les champs et les méthodes qui apparaissent dans le diagramme sont ceux et celles qui jouent un rôle important90. Certains objets sont des instances de classes prédéfinies. Les noms de ces classes ont été repris avec leur véritable nom (JTextField, JButton,...)91. Il en est de même pour la méthode prédéfinie (actionPerformed92). Sans autre contrainte, les champs sont déclarés d’accès privé et les méthodes d’accès public. Poursuivons la réflexion. Les relations entre les classes doivent être formalisées. On distingue trois types de relations dans un diagramme de classes: • les relations de composition; 89 Entités - Relations - Associations 90 Un constructeur, une méthode utilitaire ne seront pas nécessairement indiqués dans un schéma provisoire. 91 Il est possible de généraliser le schéma en utilisant des noms plus génériques (Bouton, ChampTexte,...) 92 Il était possible de choisir un nom plus général comme traiterEvenement, par exemple. Chapitre 8 GUI et gestion des événements - 124 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 • les relations d’héritage; • les relations d’utilisation. Les relations de composition sont celles qui traduisent l’appartenance d’un objet à un autre objet. Un objet de type EcranCompteur possède un objet de type Compteur. Cette possession se traduit au niveau des champs de données. La symbolique utilisée pour représenter cette relation est celle d’une ligne continue allant de l’objet possédé vers l’objet possesseur et se terminant par un losange plein. Les relations d’héritage ne demandent pas d’explication. La symbolique est celle d’une flèche continue de la classe héritière vers la classe parente. Les relations d’utilisation sont celles qui marquent les accès. Si une méthode particulière d’une classe a besoin des services d’une autre classe (principalement en invoquant une de ses méthodes sur un de ses objets), on dit qu’elle utilise cette classe. La relation d’utilisation est symbolisée par une flèche dont la ligne est discontinue. Cette symbolique est employée également lorsqu’une classe implante une interface93. Si nous admettons que la classe EcranCompteur est sensée représenter des boîtes de dialogue pour des incrémentations de compteurs, nous apprécierons de pouvoir bénéficier des fonctionnalités d’une telle interface graphique (boutons de fermeture, de réduction, barre de titre,...). Cette classe héritera donc d’une classe prédéfinie, en l’occurrence et en Java, la classe JFrame. Le diagramme s’étoffe donc un peu. JFrame Compteur EcranCompteur unCompteur - valeur: Entier = 0 + incrementer() + remettreAZero() + getValeur(): Entier - jt: JTextField - b: JButton - unCompteur: Compteur + afficher() + setCompteur(unCompteur: Compteur) + traiterEvenement(e: ActionEvent) Le nom de la variable d’instance apparaît au-dessus de la ligne qui traduit la relation de composition. Ce diagramme ne fait pas apparaître de relation d’utilisation. Si on y ajoutait une classe d’application, cette classe posséderait certainement une méthode main qui créerait des objets de type Compteur et EcranCompteur. Des relations d’utilisation associeraient cette classe application aux deux autres classes. 93 Dans ce cas, le label “implements” est ajouté à la flèche. Chapitre 8 GUI et gestion des événements - 125 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Première version Voici donc une première version de cette application qui reprend bon nombre des éléments mis en évidence dans l’application précédente et qui traduit assez fidèlement le schéma qui précède94. import java.awt.*; import java.awt.event.*; import javax.swing.*; public class AppInterfaceGraphique{ public static void main(String [] args){ EcranCompteur ec = new EcranCompteur(); Compteur c = new Compteur(); ec.setCompteur(c); Finisseur f = new Finisseur(); ec.addWindowListener(f); } } class Compteur{ private int valeur = 0; public void incrementer(){ valeur++; } public void remettreAZero(){ valeur = 0; } public int getValeur(){ return valeur; } } /* Une interface graphique à l'écoute d'un clic sur le bouton */ class EcranCompteur extends JFrame implements ActionListener { private private private private private 94 JButton bouton; JTextField texte; Container c; FlowLayout fl; Compteur unCompteur; Tout ne figure pas nécessairement dans un diagramme de classes. L’implantation de l’interface ActionListener et les classes Finisseur et WindowAdapter qu’elle étend ne sont pas représentée, de même que la classe d’application. Chapitre 8 GUI et gestion des événements - 126 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 // Construction de l'écran public EcranCompteur(){ super("Compteur"); setSize(200,70); setLocation(300,200); // Création d'un gestionnaire de mise en page fl = new FlowLayout(); c = getContentPane(); c.setLayout(fl); // Création des composants bouton = new JButton("Augmenter"); texte = new JTextField("0",4); // Ajout des composants dans le conteneur c.add(bouton); c.add(texte); // Ajout de l'objet courant à la liste des écouteurs du bouton bouton.addActionListener(this); // Affichage setVisible(true); } public void setCompteur(Compteur c){ unCompteur = c; } public void actionPerformed(ActionEvent e){ unCompteur.incrementer(); texte.setText(Integer.toString(unCompteur.getValeur())); } } /* Pour le contrôle de fermeture de la fenêtre */ class Finisseur extends WindowAdapter { public void windowClosing(WindowEvent we){ System.exit(0); } } Un diagramme de classes ne suffit évidemment pas à décrire le fonctionnement d’une application. Un programme étant une conversation entre des objets, cette conversation n’est évidemment pas figée. D’autres diagrammes, plutôt dynamiques ceux-là, doivent donc venir à la rescousse, pour ce qui est de décrire les interactions possibles. Un type de diagrammes très prisé pour cela est le diagramme de séquence (sequence diagram). Un diagramme de séquence traduit, sur une ligne du temps qui se développe vers le bas, quel objet est appelé, par quel autre objet et quel est celui qui possède la main à un moment donné. Bien entendu, le nombre d’échanges et surtout les circonstances font que ces Chapitre 8 GUI et gestion des événements - 127 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 schémas peuvent être assez nombreux. Les diagrammes de séquence traduisent donc souvent, d’abord le cas général, puis tous les cas particuliers qui peuvent se produire95. Notez qu’en programmation, le traitement des cas particuliers est généralement plus conséquent que le traitement du cas général. La POO n’échappe pas à ça. Avant de dresser des diagrammes de séquence, l’analyste s’intéresse habituellement à ce que l’on appelle les cas d’utilisation (use cases). Ces cas sont dérivés de la connaissance que l’on a du domaine dans lequel l’application est développée. Si cette connaissance n’est pas élevée, ce sont alors des interviews des acteurs du domaine qui permettent de mettre les choses au clair. Chaque cas d’utilisation et tous ses cas particuliers donnent lieu à des diagrammes de séquence. Ceux-ci permettent de déterminer si le programmeur n’a pas oublié de définir certaines méthodes au niveau de certaines classes d’objets. On le voit, l’analyse d’une application devant déboucher sur la réalisation d’un logiciel n’est pas une mince affaire. Les quelques éléments que nous en avons donnés ne servent qu’à vous donner une idée de la complexité de la démarche. Des ouvrages spécialisés traitent de la chose. De nombreuses tentatives de standardisation ont lieu à travers le développement d’UML (Uniform Modeling Language). Deuxième version Revenons à l’application pour y apporter quelques modifications, la rendre plus générale et, dans le même temps, enrichir notre diagramme de classes. Les principales améliorations que nous allons apporter sont les suivantes: • utiliser une classe anonyme pour l’écoute de la fermeture de la boîte de dialogue (problème déjà traité dans l’application précédente); • spécialiser, par héritage, la classe Compteur; • créer une interface (au sens de Java) pour permettre à d’autres classes d’objets de communiquer avec la classe EcranCompteur. Voici la modification concernant l’emploi d’une classe anonyme. commentaires. Elle ne demande pas de public class AppInterfaceGraphique2{ public static void main(String [] args){ EcranCompteur ec = new EcranCompteur(); Compteur c = new Compteur(); ec.setCompteur(c); 95 Imaginez, par exemple, tous les scénarios possibles lorsque vous vous rendez au distributeur de billets de banque: vous introduisez la carte à l’envers, vous introduisez une carte périmée, vous ne tapez pas le bon code, votre compte n’est pas assez approvisionné et ne peut être débité,... Chapitre 8 GUI et gestion des événements - 128 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 // Contrôle de la fermeture de la fenêtre ec.addWindowListener(new WindowAdapter(){ public void windowClosing(WindowEvent we){ System.exit(0); } }); } } Troisième version Un problème plus intéressant est celui qui consiste à se demander s’il ne serait pas possible de disposer d’un compteur qui puisse s’incrémenter (positivement ou négativement) avec une valeur de l’incrément différente de 1. Alors qu’une programmation impérative aurait sans doute du prévoir cette situation a priori, la programmation OO offre, au travers de l’héritage, une manière élégante de solutionner le problème. La création d’une classe CompteurVariable qui étend la classe Compteur permet de définir des compteurs dont la valeur de l’incrément peut être fournie comme paramètre à un constructeur. La méthode incrementer est évidemment remplacée dans la définition de cette classe. /* Cette classe améliore la classe Compteur (héritage) */ class CompteurVariable extends Compteur { private int increment; public CompteurVariable(int i){ increment = i; } public void incrementer(){ valeur += increment; } } JFrame Compteur - valeur: Entier = 0 + incrementer() + remettreAZero() + getValeur(): Entier unCompteur EcranCompteur - jt: JTextField - b: JButton - unCompteur: Compteur + afficher() + setCompteur(unCompteur: Compteur) + traiterEvenement(e: ActionEvent) CompteurVariable - increment + incrementer() Chapitre 8 GUI et gestion des événements - 129 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Le diagramme s’étoffe à nouveau, faisant apparaître cette nouvelle relation d’héritage entre CompteurVariable et Compteur. Quatrième version Une autre question mérite d’être examinée. Les objets de la classe EcranCompteur possèdent un objet de la classe Compteur. La classe d’écrans que nous avons défini ne sert donc qu’avec des objets de type Compteur. Ceci est évidemment un peu restrictif. Imaginez que vous désiriez utiliser les mêmes écrans pour une autre classe d’objets qui possèdent aussi des méthodes d’incrémentation, de remise à zéro et de fourniture de valeur. Il faut redéfinir une nouvelle classe d’écrans dont les objets posséderont un objet de cet autre type. Le concept d’interface permet d’éviter ce genre de problème. Souvenez-vous: l’interface définit un contrat que les classes qui l’implantent s’engagent à respecter. C’est bien de cela qu’il s’agit ici. Les méthodes à implanter sont incrementer, remettreAZero et getValeur. L’interface permettra une interaction de l’écran avec les objets de toute classe qui définit ces méthodes. Concrètement, il faut: • définir une interface, appelons-la GenreDeCompteur, • préciser que la classe Compteur l’implante, • préciser que l’objet possédé par les objets de type EcranCompteur est de type GenreDeCompteur, • modifier le type de paramètre de la méthode setCompteur. Partant de là, il sera possible de définir d’autres classes qui implantent cette interface et utiliser la même classe d’écran avec les objets de cette nouvelle classe. Le diagramme devient: JFrame Compteur GenreDeCompteur Interface implements - valeur: Entier = 0 - valeur: Entier = 0 + incrementer() + remettreAZero() + getValeur(): Entier + incrementer() + remettreAZero() + getValeur(): Entier EcranCompteur unCompteur - jt: JTextField - b: JButton - unCompteur: Compteur + afficher() + setCompteur(unCompteur:GenreDeCompteur) + traiterEvenement(e: ActionEvent) CompteurVariable - increment + incrementer() Chapitre 8 GUI et gestion des événements - 130 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Voici les modifications principales effectuées dans le code pour adapter la situation. import java.awt.*; import java.awt.event.*; import javax.swing.*; public class AppInterfaceGraphique3{ public static void main(String [] args){ EcranCompteur ec = new EcranCompteur(); GenreDeCompteur c = new Compteur(); ec.setCompteur(c); // Contrôle de la fermeture de la fenêtre ec.addWindowListener(new WindowAdapter(){ public void windowClosing(WindowEvent we){ System.exit(0); } }); } } /* Création d'une interface en vue d'abstraire la solution */ interface GenreDeCompteur{ void incrementer(); void remettreAZero(); int getValeur(); } /* Cette classe implémente l'interface qui précède */ class Compteur implements GenreDeCompteur { int valeur = 0; public void incrementer(){ valeur++; } public void remettreAZero(){ valeur = 0; } public int getValeur(){ return valeur; } } /* Une interface graphique à l'écoute d'un clic sur le bouton */ class EcranCompteur extends JFrame implements ActionListener { private JButton bouton; private JTextField texte; private Container c; Chapitre 8 GUI et gestion des événements - 131 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 private FlowLayout fl; private GenreDeCompteur unCompteur; // Construction de l'écran public EcranCompteur(){ super("Compteur"); setSize(200,70); setLocation(300,200); // Création d'un gestionnaire de mise en page fl = new FlowLayout(); c = getContentPane(); c.setLayout(fl); // Création des composants bouton = new JButton("Augmenter"); texte = new JTextField("0",4); // Ajout des composants dans le conteneur c.add(bouton); c.add(texte); // Ajout de l'objet courant à la liste des écouteurs du bouton bouton.addActionListener(this); // Affichage setVisible(true); } public void setCompteur(GenreDeCompteur c){ unCompteur = c; } public void actionPerformed(ActionEvent e){ unCompteur.incrementer(); texte.setText(Integer.toString(unCompteur.getValeur())); } } Enfin, imaginons qu’une nouvelle classe veuille pouvoir être exploitée par cette interface, il lui suffit de l’implanter. En voici un exemple. import java.awt.*; import java.awt.event.*; import javax.swing.*; /* Pas d'importation nécessaire pour l'interface GenreDeCompteur et la classe EcranCompteur qui sont dans le même package que les classes ci-dessous. */ public class AppInterfaceGraphique5{ Chapitre 8 GUI et gestion des événements - 132 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 public static void main(String [] args){ EcranCompteur ec = new EcranCompteur("1"); GenreDeCompteur c = new AutreSorteDeCompteur(3); ec.setCompteur(c); // Contrôle de la fermeture de la fenêtre ec.addWindowListener(new WindowAdapter(){ public void windowClosing(WindowEvent we){ System.exit(0); } }); } } /* Cette autre classe implante aussi l'interface en question */ class AutreSorteDeCompteur implements GenreDeCompteur { private int valeur = 1; private int increment; public AutreSorteDeCompteur(int i){ increment = i; } public void incrementer(){ valeur *= increment; } public void remettreAZero(){ valeur = 1; } public int getValeur(){ return valeur; } } Les classes internes Pour des raisons qui tiennent plus aux contraintes d’accès qu’à une hypothétique structuration, le programmeur a la possibilité de définir des classes comme membres d’autres classes. En d’autres termes, les membres d’une classe ne sont pas seulement les champs et les méthodes, mais également d’autres classes. Cette opportunité s’avère intéressante lorsque ces dernières sont seulement utiles dans un contexte restreint, pour des services qu’elles peuvent rendre à la classe “mère”. L’autre avantage, c’est que les champs de la classe “mère” peuvent être déclarés d’accès private et malgré tout, rester accessibles aux classes internes96. Nous exploitons cette possibilité dans la solution de l’exercice qui suit. Des écouteurs d’événements sont nécessaires. Les classes de ces écouteurs sont définies comme des classes internes, ce qui permet de simplifier le problème des accès aux objets graphiques, notamment. 96 Ce qui n’est pas le cas pour une classe qui hérite d’une autre classe... Chapitre 8 GUI et gestion des événements - 133 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Exercice Modifiez le programme qui simule le chronomètre en faisant afficher, toutes les secondes, le temps écoulé dans un champ texte. Solution Voici une solution qui utilise plusieurs classes pour implanter les écouteurs. Ces classes sont des classes internes. Une classe Chronometre représente l’interface graphique qui est donc à construire. Cette interface97 graphique comprend deux zones disposées verticalement. La première zone contient un label et une zone de texte pour l’affichage du temps qui s’écoule, la seconde comprend trois boutons: un pour le lancement, un pour l’arrêt et un pour la remise à zéro du chrono. Cette classe contient également un champ arret qui détecte si le chrono doit continuer à fonctionner malgré l’arrêt de l’affichage dynamique. Construction de l’interface class Chronometre extends JFrame{ private Timer t; private JTextField jt; private JButton b1, b2, b3; // Variable d'état: le chrono a-t-il été arrêté? private boolean arret = false; // Constructeur public Chronometre(){ super("Chronomètre"); Container c = getContentPane(); // Division verticale (2 cellules) c.setLayout(new GridLayout(2,1)); // Première cellule (une étiquette et une zone de texte) JPanel p1 = new JPanel(); p1.setLayout(new FlowLayout()); 97 Il convient de ne pas faire l’amalgame entre la notion d’interface au sens des langages de POO et la notion d’interface graphique qui désigne ici un écran, même si ces deux notions ne sont pas complètement étrangères. Chapitre 8 GUI et gestion des événements - 134 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 p1.add(new JLabel("Temps écoulé: ")); jt = new JTextField("0",3); p1.add(jt); c.add(p1); // Deuxième cellule (trois boutons) JPanel p2 = new JPanel(); p2.setLayout(new FlowLayout()); b1 = new JButton("(Re)Lancer"); b2 = new JButton("Arrêter"); b3 = new JButton("Remettre à zéro"); b2.setEnabled(false); b3.setEnabled(false); p2.add(b1); p2.add(b2); p2.add(b3); c.add(p2); // Dimensionnement, positionnement et affichage du cadre setSize(350,100); setLocation(200,200); setVisible(true); // Définition d’un objet écouteur ChangeChrono change = new ChangeChrono(); // Association des objets écouteurs aux sources b1.addActionListener(change); b2.addActionListener(change); b3.addActionListener(change); ActionListener at = new AfficheurDeTemps(); t = new Timer(1000,at); } ... La définition n’est pas terminée puisque les classes d’écouteurs vont être définies comme des membres de cette classe. Ce sont des classes dites internes. Elles risquent d’être peu utiles ailleurs. L’avantage est évidemment que les classes internes ont accès aux membres de la classe dont elles font partie. Ainsi, les boutons déclarés private dans Chronometre sont accessibles dans les classes internes. Un objet de la classe ChangeChrono est à l’écoute des différents boutons. Cette classe est définie comme classe interne, ce qui lui permet d’accéder aux boutons de l’interface. Cet accès est nécessaire pour le test concernant la source de l’événement. Les boutons sont activés ou désactivés selon les circonstances (méthode setEnabled). Un nouveau Timer est créé lorsque le bouton Lancer est activé, mais seulement dans le cas où le chrono n’a pas été arrêté. Dans ce dernier cas, le chrono reprend comme s’il ne s’était pas arrêté. Chapitre 8 GUI et gestion des événements - 135 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 private class ChangeChrono implements ActionListener{ public void actionPerformed(ActionEvent ae){ // Le bouton "(re)lancer" est pressé. if(ae.getSource() == b1){ // Accessibilité des boutons b1.setEnabled(false); b2.setEnabled(true); b3.setEnabled(true); // Pas de nouveau Timer si l'ancien a juste été arrêté. if(!AppLayout.arret){ t = new Timer(1000,new AfficheurDeTemps()); } t.start(); } // Le bouton "arrêter" est pressé. if(ae.getSource() == b2){ AppLayout.arret = true; t.stop(); // Accessibilité des boutons b1.setEnabled(true); b2.setEnabled(false); } // Le bouton "remettre à zéro" est pressé. if(ae.getSource() == b3){ AppLayout.arret = false; t.stop(); jt.setText("0"); // Accessibilité des boutons b1.setEnabled(true); b2.setEnabled(false); b3.setEnabled(false); } } } Chapitre 8 GUI et gestion des événements - 136 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Enfin, la classe AfficheurdeTemps n’a pas changé. Un de ses objets est à l’écoute du Timer qui contrôle l’écoulement du temps. Son code est repris tel quel, mais également sous forme d’une classe interne. private class AfficheurDeTemps implements ActionListener{ private final Date topChrono = new Date(); public void actionPerformed(ActionEvent e){ Date now = new Date(); jt.setText((now.getTime() - topChrono.getTime())/1000 + " "); } } Il reste a fournir une classe application qui construit un objet Chronometre et ajoute à la liste de ses écouteurs, un écouteur anonyme pour la fermeture de la fenêtre. On pense aussi à fournir toutes les instructions d’importation nécessaires à cause de l’utilisation de plusieurs classes et interfaces prédéfinies98. import import import import import java.awt.*; java.awt.event.*; javax.swing.*; javax.swing.Timer; java.util.Date; public class AppLayout{ public static void main(String [] args){ Chronometre chrono = new Chronometre(); chrono.addWindowListener(new WindowAdapter(){public void windowClosing(WindowEvent we){System.exit(0);}}); } } 98 Vous retrouverez la justification de ces instructions au chapitre précédent, dans la première version de ce programme. Chapitre 8 GUI et gestion des événements - 137 - Programmer avec des objets Chapitre 9 Etienne Vandeput ©CeFIS 2002 JavaScript, Java: quels rapports? Un dilemme A l’heure où les cours de programmation réinvestissent dans le calme les classes de l’enseignement secondaire, la question du choix d’un langage d’approche de la programmation orientée objet avec des étudiants se pose de manière cruciale et est sans doute controversée. En gros, deux voies sont possibles: celle de l’utilisation d’un langage véritablement imprégné du paradigme objet (Java, C++,...) et qui fera la part belle à la créativité et à l’abstraction, ou celle d’un langage de script (JavaScript, Python, Php,...) qui se fondera davantage sur l’idée que le Web constitue une source de motivation relativement forte à programmer. Sans prendre immédiatement parti pour l’une ou l’autre voie, voici quelques éléments distinguant Java et JavaScript qui devraient pouvoir vous permettre de choisir en meilleure connaissance de cause entre les deux catégories d’outils. Deux exemples sont traités en JavaScript de manière à vous faire percevoir ces différences. Néanmoins, il ne s’agit pas ici de développer un autre cours sur JavaScript et les éléments du langage qui seront évoqués ne le seront que de manière très parcellaire. Pour une connaissance plus approfondie de JavaScript, je vous renvoie à la bibliographie. Un air de famille? La seule ressemblance des noms de ces deux langages suffit à faire émerger la question de savoir s’ils sont effectivement liés. Les spécialistes vous diront pourtant que Javascript n'est pas Java, que les ressemblances sont nombreuses mais que ces langages sont fondamentalement différents. Essayons donc de voir pourquoi ils sont différents et aussi, pour quelles raisons on peut les confondre. Les langages de script Un langage de script est, par vocation, un langage simple et faiblement typé qui va permettre la description rapide d’un scénario se déroulant sur le Net. Il peut s’agir d’un scénario se déroulant au chargement du document, comme il peut s’agir d’un scénario répondant à la réalisation d’un événement dont le déclencheur sera très souvent l’utilisateur. Les langages de script se distinguent par l’endroit où s’effectue leur interprétation. Deux solutions sont possibles: • c’est le navigateur qui interprète le script, ce qui présuppose que le script fait partie du document HTML reçu par le navigateur99; • le script est interprété sur le serveur Web qui génère la page à envoyer au client Web (le navigateur). 99 On parle de langage de script “client-side”. Chapitre 9 JavaScript, Java: quels rapports? - 138 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Un des avantages du langage de script “client-side” est qu’il est possible de détecter et d’exploiter des événements tels la sélection d’un bouton d’option, d’un élément dans une liste, le remplissage d’un formulaire, le clic de souris sur un bouton, etc. La validation de données à envoyer à un serveur est évidemment possible. Une de ses limites importantes (et bien compréhensible) c’est que ses scripts ne peuvent agir sur la mémoire de la machine cliente. Au mieux, il est possible d’écrire des cookies sur son disque dur. JavaScript En fait, Javascript est le langage de script développé par Netscape en s’inspirant pas mal du langage Java. Pour cette raison, on peut constater que de nombreuses commandes adoptent une syntaxe ressemblante ou carrément identique. De même, chacun des langages fait référence à des méthodes semblables. Javascript peut apparaître comme une sorte de Java simplifié, mais les différences sont aussi d’une autre nature100. Il existe, bien entendu, plusieurs versions de JavaScript et de nombreux langages de script différents. Les navigateurs s’évertuent à les reconnaître. L’idée sous-jacente au développement de Javascript , c’est de permettre aux concepteurs de pages Web de développer des applications Internet sous la forme d’une extension des possibilités du HTML. Une page HTML comportant de nombreux éléments, le langage permet de modifier le comportement de ceux-ci. Il est clair que les possibilités offertes se limitent à ces modifications de comportements. La finalité reste l’affichage d’une page Web, quels que soient les traitements qui s’effectuent en arrièreplan. Une conséquence de ce qui précède est que le code d’un langage de script doit nécessairement être inclus dans le document HTML qu’il est généralement léger et orienté objet car il fait intervenir et interagir les éléments de la page Web. Le petit exemple qui suit montre qu’un script permet d’ajouter du code HTML au contenu d’un document de manière à ce que le navigateur puisse l’interpréter et en afficher les effets. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>Ajouter du contenu à une page</title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <script language="JavaScript"> 100 Voici une indication trouvée sur http://www.intranetjournal.com/faqs/jsfaq/gen1.html qui semble intéressante: JavaScript is a platform-independent, event-driven, interpreted programming language developed by Netscape Communications Corp. and Sun Microsystems. Originally called LiveScript (and still called LiveWireTM by Netscape in its compiled, server-side incarnation), JavaScript is affiliated with Sun's object-oriented programming language JavaTM primarily as a marketing convenience. They interoperate well but are technically, functionally and behaviorally very different. Chapitre 9 JavaScript, Java: quels rapports? - 139 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 document.writeln("<H1>JavaScript</H1><HR>"); document.writeln("<H3>Un script peut agir sur le contenu de la page affichée.<BR>"); document.writeln("Dans ce cas précis, le corps de la page est vide!</H3>"); document.writeln("<H2><FONT COLOR=#FF00000>N'importe quel bout de code \ HTML peut être généré par le script et interprété par le \ navigateur.</FONT></H2>"); </script> </head> <body> </body> </html> Vous pouvez constater que le corps de ce document HTML est vide. Pourtant, le navigateur produit l’affichage suivant Une première analyse de ce petit script nous permet de faire quelques observations. Le script est ici décrit dans l’en-tête du document (head). Il aurait très bien pu figurer dans le corps, l’effet aurait été semblable. L’interpréteur exécute les commandes au moment où il les rencontre. Chapitre 9 JavaScript, Java: quels rapports? - 140 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Ce sont les balises HTML <script> et </script> qui délimitent les instructions du script, la balise de tête ayant comme attribut, le langage de script utilisé, ce que le navigateur doit évidemment connaître. Nous pouvons remarquer une certaine similitude avec Java en remarquant que writeln est une méthode qui s’applique à l’objet document avec un paramètre qui est la chaîne de caractères contenant le bout de code HTML à interpréter. Il n’a pas été question ici de créer une classe, ni même de définir la méthode. Tout se passe comme si les classes et les méthodes étaient déjà en place, et cela, sans aucun appel à un éventuel package. A titre indicatif, le symbole \ permet de poursuivre l’instruction sur la ligne suivante sans conséquence sur son interprétation.. Pour ce qui est de JavaScript, comme l’interpréteur est côté client, le navigateur doit être configuré en conséquence. Vous pouvez être rassurés, ils le sont généralement tous par défaut. Toutefois, il faut quand même savoir que Microsoft a développé son propre langage de script appelé Jscript (ben voyons!) et que cette copie carbone de JavaScript n’est pas complètement fidèle101. Alors méfiance puisque chaque navigateur a une interprétation du script qui lui est propre! Lorsque l’interpréteur est côté serveur (Php, Perl, Python,...), le problème de la configuration ne se pose pas. Java et le HTML Quelle que soit sa souplesse et la facilité avec laquelle vous pouvez associer du code JavaScript à vos pages HTML102, ce langage ne vous permet pas de développer des applications classiques. Ce n’est pas un défaut, il n’a tout simplement pas été prévu pour ça. En revanche, Java est un véritable langage de programmation. Son développement est plutôt parallèle (et non consécutif) à celui du Web et s’il n’a pas été initialement conçu pour lui, il s’y est relativement bien adapté. Qu’en est-il donc de ses implications au niveau des applications Internet? Java peut être utilisé pour programmer des applets qui sont des programmes pouvant être référencés dans une page HTML. Cela signifie que le code de ces programmes peut être téléchargé et interprété sur la machine cliente pour autant que le navigateur le supporte. Et par défaut, la réponse est souvent positive. On distinguera donc les programmes Java de la manière suivante: • les applications sont des programmes indépendants qui s'exécutent directement sur une machine dans un environnement Java; 101 Extrait de http://www.swtech.com/script/javascript/diff/: Anyone that has tried to write JavaScript code that will run and look the same across different versions of Netscape Navigator and Microsoft Internet Explorer will know only too well that there are some huge differences and incompatibilities! 102 Le code Javascript, peut également être lu à partir d’un fichier texte dont l’extension est .js. Chapitre 9 JavaScript, Java: quels rapports? - 141 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 • les applets sont des programmes qui peuvent être téléchargés par un client-Web. Dans une page HTML les applets sont référencées entre les balises <applet> et </applet>. Le code reçu est interprété par la machine virtuelle intégrée dans le navigateur ou une machine virtuelle externe; • bien que la balise ne soit pas encore standardisée en HTML 4, on parle aussi de servlets pour désigner des programmes semblables aux applets, mais qui s'exécutent sur un serveur avant de retourner les résultats au client. On appelle une servlet directement par son URL ou on l’intègre dans un document HTML entre les balises <servlet> et </servlet>. Dans le second cas, le client reçoit d’abord le contenu du document précédent la balise <servlet>. La servlet est exécutée par le serveur, puis les résultats sont envoyés avec le reste du document. Dans le monde Java, les servlets correspondent aux scripts CGI. Un brin de comparaison Nous pouvons résumer les choses de la manière suivante: Javascript JAVA Code intégré dans le code HTML Code non intégré mais référencé dans le code HTML (applet) Code interprété par le navigateur Code source compilé et fourni à la JVM Domaine d’application limité au Net Domaine de programmation peu limité Accès immédiat aux objets du navigateur Pas d'accès aux objets du navigateur Le code JavaScript est donc immédiatement accessible à tout qui télécharge la page qui le contient. Le code Java est compilé, ce qui le rend inaccessible au premier abord (qui a dit qu’il était possible de décompiler? ;-). Contrairement aux programmes Java, les programmes JavaScript ne peuvent écrire en mémoire de masse. Au rayon des différences notoires, il est encore à signaler que JavaScript n’exige pas de déclaration des types des variables. En JavaScript, il est possible d’instancier les classes existantes mais impossible de créer de nouvelles classes. Ces classes existantes correspondent aux différents éléments liés à l’affichage d’une page Web et font partie d’une hiérarchie bien établie. Ces quelques différences importantes suffisent à prouver qu’il existe un fossé entre les deux langages, tant au niveau des concepts qu’au niveau des objectifs et des domaines d’application. Un autre exemple en JavaScript Notre comparaison des deux langages manque un peu d’illustrations. Alors voici un autre exemple qui justifie ce que nous venons de dire à leur propos. Chapitre 9 JavaScript, Java: quels rapports? - 142 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 Saisie de nombres et calcul d’une moyenne On se propose de traiter le problème suivant: l’internaute fourni des valeurs entières positives et lorsqu’il fournit la valeur 999, le script lui renvoie la valeur moyenne. Voici le code produisant la saisie des valeurs et l’affichage du résultat dans la page Web: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>Calculer une moyenne</title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> </head> <body> <H1>Calcul d'une moyenne</H1> <HR><BR> <script language="JavaScript"> var somme, compteur, nombre, n, moyenne; somme=0; compteur=0; nombre=window.prompt("Fournissez un nombre entier positif de moins de \ quatre chiffres\n(999 pour stopper)","999"); while(nombre!="999"){ n=parseInt(nombre); somme+=n; compteur++; nombre=window.prompt("Fournissez un nombre entier positif de moins \ de quatre chiffres\n(999 pour stopper)","999");} moyenne=somme/compteur; document.writeln("<H3>La moyenne des nombres introduits est " + Math.round(moyenne) +".</H3>"); document.writeln("<BR>Pour un autre calcul, commandez au navigateur de \ recharger cette page."); Chapitre 9 JavaScript, Java: quels rapports? - 143 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 </script> </body> </html> Ce code produit d’abord un affichage minimum et l’ouverture d’une petite fenêtre de saisie: Lorsque la valeur par défaut est fournie, la fenêtre de saisie se ferme et l’affichage de la page se complète avec le résultat. Cette application est très sommaire et ne tient pas compte de tout une série de précautions qu’il faudrait prendre: pas de valeur introduite, interface peu conviviale, etc. Son but est simplement de mettre en évidence le fait qu’un langage comme JavaScript s’intéresse essentiellement aux objets faisant partie de l’environnement des navigateurs: document, fenêtre,...), que son but, même s’il permet d’effectuer des calculs et des tests, est Chapitre 9 JavaScript, Java: quels rapports? - 144 - Programmer avec des objets Etienne Vandeput ©CeFIS 2002 principalement de produire des choses qui soient interprétables par un navigateur. Il n’autorise donc pas le développement de véritables applications. Signalons encore que de nombreuses choses sont implicites en JavaScript, telles le typage des variables, la création d’objets,... et que les classes d’objets sont prédéfinies de même que leurs méthodes. Toutes ces caractéristiques n’en font pas forcément un langage très pédagogique 103 mais il est séduisant car c’est un des moyens d’introduire le dynamisme dans les pages Web. Enfin, vous trouverez sur le Web de nombreux scripts déjà tout prêts à l’emploi. Les éditeurs HTML eux-mêmes sont capables d’en générer, sous la pression de quelques clics de souris. Quant à dire que programmer en JavaScript, c’est faire de la POO, il y a un pas que je n’ai guère le courage de franchir. 103 Enfin, ça c’est un avis très personnel! Chapitre 9 JavaScript, Java: quels rapports? - 145 - Programmer avec des objets Bibliographie K. ARNOLD, J. GOSLIN, D. HOLMES The Java Programming Language (third edition) Addison-Westley Reading 2000 DEITEL et DEITEL Comment Programmer en Java (troisième édition) Les Editions Reynald Goulet Inc. Québec 2000 C. S. HORSTMANN et G. CORNELL Au coeur de Java2 Vol 1 Notions fondamentales CampusPress France Paris 2000 C. S. HORSTMANN et G. CORNELL Au coeur de Java2 Vol 2 Fonctions avancées CampusPress France Paris 2000 DEITEL, DEITEL & NIETO Internet & World Wide Web, How to program Prentice-Hall New Jersey 2000 C. DELANNOY Programmer en Java Eyrolles Paris 2000 C. DELANNOY Exercices en Java Eyrolles Paris 2001 M. LAI Penser objet avec UML et Java (2ème édition) DUNOD Paris 2000 M. FOWLER UML CampusPress Le Tout en Poche Paris 2001 Etienne Vandeput ©CeFIS 2002 Programmer avec des objets Etienne Vandeput ©CeFIS 2002 D. BARETT, M. BROWN, D. LIVINGSTON JavaScript, DHTML & CSS CampusPress France Paris 2000 P. CHALEAT, D. CHARNAY Programmation HTML et JavaScript Eyrolles Paris 1998 G. Van ROSSUM Python Tutorial Release 2.1 F. Drake editor PythonLabs 2001 Client-Side JavaScript Guide v. 1.3 Netscape Communications Corporation 1999 http://developer.netscape.com/docs/manuals/js/client/jsguide/ClientGuideJS13.pdf