ENPC - Programmation -
Séance 5
La programmation orientée objet
La programmation objet, c'est à dire celle qu'on est sensé
mettre en oeuvre lorsqu'on écrit des programmes avec un langage à
objet (ou langage orienté objet) tel que Java, fait
intervenir différents concepts tels que celui de classe,
d'objet, de responsabilité, d'encapsulation,
etc.
Le premier concept auquel on est confronté lorsqu'on écrit un
programme est celui de classe: le mot-clé class, dans
class MaClasse { ... }
permet de définir (entre les accolades) les membres de la
classe MaClasse. Les membres sont principalement les
méthodes (quelques fois appelées fonctions) et les
champs (aussi appelés attributs).
- Une classe représente et définit plusieurs choses en même temps:
- Un modèle pour la création de toutes ses instances (tout objet
est l'instance d'une classe). En ce sens, une classe est une sorte
de moule dans lequel on coule un objet lors de sa
création (cette création est effectuée par l'opérateur new
suivi d'un constructeur qui a le même nom que la
classe.
Exemple: Personne jean = new Personne("Jean");
Ici, jean est le nom d'une variable qui contient la
référence à l'objet créé et "Jean" est
une chaîne de caractère (String) utilisée comme argument du
constructeur de la classe Personne.
- Une unité de compilation. Cela signifie que la
compilation d'un ensemble de fichiers produit un bytecode de classe
(un fichier Toto.class) par classe définie dans le code
source (par mot-clé ...class Toto { ... }.
- Un type qui sera le type assigné à tout objet de cette
classe. La notion de type est importante car elle permet au
compilateur de signaler des incohérences dans un programme. Par
exemple, si une méthode est définie dans la classe
Personne avec un paramètre de type Personne
(disons discuterAvec(Personne p) {...}), alors le
compilateur détecte une erreur si cette méthode est appelée avec un
argument d'un autre type. Exemple:
Brouette b = new Brouette();
Personne bob = new Personne("Robert");
jean.discuterAvec(b); // Erreur de compilation
jean.discuterAvec(bob); // Correct
Dans la dernière instruction, la méthode discuterAvec est
appelée sur l'objet jean (qu'on appelle la cible ou
le receveur de la méthode) avec comme argument l'objet
bob.
- D'une manière générale, une classe décrit ce que tous ses
objets ont en commun. C'est à dire le type, bien sûr, mais plus
précisément leurs membres, c'est à dire d'une part ce qui permet de
décrire l'état de chacun des objets de cette classe (les
variables d'instance, ou champs, ou encore
attributs) et d'autre part les fonctionnalités dont chacun
d'entre eux dispose (c'est à dire les méthodes).
- Un comportement habituel en programmation orientée objet est
décrit ci-dessous:
- Définir (décrire) une classe, c'est à dire:
- son nom (qui sera à la fois le nom de la classe et celui du
type correspondant)
- les variables d'instance dont disposeront chacune de ses
instances: chaque objet de cette classe disposera de ses propres
occurrences de ces variables d'instance qui décriront son état
(vraissemblablement différent de l'état des autres objets de la
même classe).
- les méthodes (fonctionnalités) qui seront offertes à toute
instance de cette classe.
- Créer des objets (instances) de cette classe, qui vont pouvoir
"vivre" avec chacun leur propre état (représenté par les valeurs de
leurs variables d'instance) et qui pourront communiquer entre eux
ou avec des objets d'autres classes via leurs méthodes
respectives.
- Un programme exécutable (ou plus exactement dont le bytecode
produit par la phase de compilation est exécutable par une JVM) est
une classe qui dispose d'une méthode dite principale ayant la
forme suivante:
public static void main(String[] args) { ... }
La classe Point
Nous considérons ici la classe Point des objets qui sont des
points dans un plan à deux dimensions. Les instances de cette classe
disposent donc, pour décrire leur état, de deux champs, x et
y, de type double.
Pour créer un point, on dispose d'un constructeur qui accepte
les deux coordonnées du point que l'on veut créer.
Un constructeur est une méthode un peu
particulière puisqu'il doit avoir exactement le nom de la classe (avec
sa majuscule) et qu'il n'a pas de type de retour (en effet, lorsqu'un
tel constructeur est associé à l'opérateur new, il retourne
la référence de l'objet (l'instance) qui vient d'être alloué et
celle-ci est donc typée Point dans notre exemple.
Par ailleurs, on souhaite pouvoir
effectuer deux opérations sur n'importe quel point:
- le déplacer, par la méthode public void translate(double dx,
double dy) et
- en obtenir une représentation sous la forme d'une chaîne de
caractères, par la méthode public String toString().
Le fichier Point.java accessible à l'URL
http://www-igm.univ-mlv.fr/~duris/ENPC/Point.java contient le code
initial de cette classe. Récupérez-le, sauvegardez-le et testez-le.
Autour des constructeurs
-
Modifier les paramètres formels du constructeur pour leur donner les
noms x et y au lieu de leX et
leY. Quel est le problème?
Dans une classe, le mot clé
this représente la référence à l'instance courante de
cette classe. L'utilisation de ce mot-clé est donc interdite dans
toute portion de code statique.
-
Mettre le constructeur et le code du main en
commentaire. Dans le main, créer un seul point par Point
p = new Point(); et l'afficher. Que s'est il passé? Enlever les
commentaires du constructeur et tester à nouveau. Quel est le
problème?
Lorsqu'aucun constructeur n'est
explicitement défini, le compilateur
crée un constructeur implicite également appelé constructeur par
défaut. Si un constructeur explicite est défini, ce constructeur
par défaut n'est plus accessible.
- Faire en sorte que les deux constructeurs Point(double x,
double y) et Point() cohabitent dans la classe
Point.
On dit que ces deux constructeurs sont
surchargés: ils ont le même nom mais sont différentiables par
la liste de leurs paramètres.
Autour de toString()
- Mettre en commentaire la définition de la méthode toString()
de la classe Point, recompiler et exécuter. Comment expliquer
ce comportement?
La méthode toString() définie dans
la classe Point est une redéfinition de la méthode
toString() de la classe Object. En l'absence de la
première, c'est la méthode de Object, héritée dans
Point, qui est utilisée. Par défaut, elle affiche le nom de
la classe de l'objet représenté par la référence et une valeur
hexadécimale qui est un code représentant cet objet (hashcode).
- Après avoir décommenté la méthode toString() dans
le programme, remplacer dans le main l'instruction
System.out.println(p.toString()); par
System.out.println(p);. Que se passe-t-il?
Lorsque son argument n'est pas un type
primitif, mais une référence à un objet d'une classe, la méthode
println appelle automatiquement la méthode
toString() sur cette référence avant d'effectuer l'affichage
de la chaîne de caractère résultant de cet appel.
Autour de l'égalité
- Qu'affiche le programme suivant et comment expliquer ce résultat?
Point p1 = new Point();
Point p2 = new Point();
System.out.println(p1 == p2);
L'opérateur == agit en général sur les types
primitifs. Lorsqu'il est utilisé sur des références à des objets, il
teste l'égalité des références aux objets. En d'autre termes, il
teste les valeurs qui ont été retournées par des appels à
l'opérateur new. En ce sens, les deux points p1 et
p2 qui ont été créés correspondent à deux références
différentes, même si elles réfèrent des objets identiques
(leur état est identique).
-
Définir une méthode public boolean same(Point p) qui
retourne true si le paramètre p à même x
et même y que le point auquel on applique la méthode et
false sinon. Ainsi, si on appelle p1.same(p2) dans
l'exemple ci-dessus, le résulat doit être true.
Surcharge de méthode
Surchargez, dans la classe Point, la méthode public
void translate(double dx, double dy) avec une méthode public
void translate(double d) qui déplace à la fois x et
y de la valeur d. Testez cette méthode.
Tout comme les constructeurs, les méthodes
peuvent être surchargées: cela signifie que plusieurs méthodes ayant
le même nom cohabitent dans la même classe. Elle doivent se distinguer
par des listes de paramètres différents.
Attention: dans une classe, deux méthodes ne peuvent pas avoir
le même nom et les même paramètres, même si elles ont des types de
retour différents.
Classe disque
On veut maintenant définir la classe des disques, un disque étant
défini par son centre, qui est un Point, et par son rayon,
que l'on représentera par un double.
Le fait qu'un disque soit construit à
partir d'un point (instance d'une classe existant déjà) s'appelle de
la composition.
- Définir la classe Disque avec ses champs centre
et rayon.
- Définir un constructeur acceptant en argument un Point
et un double pour initialiser le disque créé.
- Définir un constructeur sans argument initialisant le centre du
disque au point par défaut (celui construit par le constructeur de
point sans argument) et le rayon à 1.
- Définir une méthode toString() dans la classe
Disque qui donne une représentation sous la forme:
Disque - centre: (0.0,0.0) rayon: 1.0
Il est important ici d'utiliser la méthode
toString() de la classe Point: c'est de la
délégation (dans l'affichage d'un disque, l'affichage de son
centre qui est un point est délégué à la classe Point
qui a la responsabilité de l'affichage de n'importe laquelle de ses
instances.
- Dans le même ordre d'idées, définir dans la classe
Disque une méthode public void
translate(double dx, double dy) qui déplace un disque.
- Définir une méthode retournant la surface d'un disque.
- Définir une méthode testant l'égalité de deux Disques.
sont décrits ici
Je rappelle que les exercices donnés à la fin de chaque feuille de
séance doivent être effectués soit au cours de la séance, soit en
travail personnel, et les fichiers source (.java) correspondant
doivent m'être envoyés par mail à l'adresse
Etienne.Duris[at]univ-mlv.fr. Ces exercices sont corrigés
et constituent des notes de contrôle continu qui comptent dans
l'évaluation du module.
- Définir les classes Point et Disque décrites
ci-dessus avec toutes leurs méthodes.
- Définir une méthode isIn dans la classe Disque,
qui accepte un Point en argument, qui retourne true
si le point est dans le disque et false sinon.
- Définir une méthode howManyIn dans la classe
Disque, qui accepte un tableau de Points en
argument et qui retourne le nombre de Points du tableau qui
sont dans le disque auquel la méthode est appliquée.