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