TP 5 2425
A) LIRE EN DÉTAIL (et poser des questions !)
Les abstractions
- Classe abstraite
Lorsque des classes F1, F2, F3 héritent d'une
classe M, on peut souhaiter que la classe M ne soit pas instanciable et qu'on ne
puisse créer des instances que des classes filles.
En Java, il suffit de déclarer la classe M abstraite et le compilateur interdira
l'instanciation. La syntaxe consiste à ajouter abstract entre public et le nom de la classe.
A quoi peuvent bien servir des constructeurs
dans une telle classe puisqu'elle ne peut pas être instanciée ?
Aide : La classe peut posséder des attributs
; quand seront-ils initialisés, et comment/par qui ?
- Méthode abstraite
Dans une classe FormeGéométrique, on ne saurait pas
écrire les instructions d'une procédure dessine() . Par contre, dans ses sous-classes Carré ou Cercle, on sait quelle
procédure graphique appeler (Rectangle pour tracer un
carré, Ellipse2D pour tracer un cercle, ou autre chose pour tracer une autre forme).
Mais si l'on a écrit une application comme un éditeur graphique, on veut
pouvoir appeler la procédure dessine() sur n'importe
quelle forme géométrique, y compris un Triangle ou un Hexagone qui seraient des formes ajoutées par
la suite.
Il faut donc pouvoir déclarer une procédure dessine() dans la classe FormeGéométrique tout en ne sachant pas écrire ses instructions !
En Java, il suffit de déclarer la méthode dessine abstraite et le compilateur nous en interdira
l'exécution et nous permettra de ne pas en écrire le corps tout de
suite, mais seulement dans les sous-classes. La syntaxe consiste à ajouter
abstract après public dans la signature de la
méthode et à remplacer son corps (tout ce qui n'est pas la
signature) par un simple point-virgule. Attention ! Un corps
vide { } ou {;} est ≠ d'une absence de
corps ; !
- Si dans une classe
C, il existe une méthode abstraite, ou bien si dans une super-classe de C,
il existe une méthode abstraite non redéfinie dans C, alors la classe C
doit être déclarée abstraite (==> abstract) car objet.methode()doit toujours pouvoir fonctionner si objet a pu être instancié. (on
a donc une classe abstraite par obligation --du compilateur-- alors qu'au
1. on avait une classe abstraite par choix --du programmeur--)
Se souvenir que lorsqu'une méthode m() existe dans plusieurs classes et sous-classes
(grâce à la redéfinition), c'est toujours le type constaté qui compte pour
déterminer à l'exécution la méthode m() de quelle classe va être exécutée.
- Interface (utile pour le dernier
exercice de ce TP)
A. Définition
Dans la javadoc du JDK, on voit sur la gauche dans la liste des classes des
noms de types qui commencent aussi par une majuscule mais qui ne sont pas des
classes : on les appelle des interfaces. Même s'il y a des nouveautés à partir
de java 8, dans cette unité nous considèrerons qu'une interface a deux
contraintes : ne posséder aucun attribut d'instance (donc aucun
constructeur) et ne posséder que des méthodes abstraites. Cela sert
généralement à décrire le comportement commun que devront posséder certaines
classes qui prétendront respecter ce "contrat", on dit
"implémenter cette interface". Ce concept est très utilisé dans le
JDK (gestion des évènements et collections d'objets, notamment).
B. Utilisation
1) Implémenter une interface
Une classe n'hérite pas d'une interface
(il n'y a ni attributs ni corps de méthodes !).
Par contre, on peut s'engager à respecter une (ou
plusieurs) interface(s), c'est-à-dire à implémenter le corps de
toutes les méthodes déclarées dans cette(ces) interface(s).
Si elle ne redéfinit
pas toutes les méthodes de l'interface, il restera donc des méthodes abstraites
non redéfinies, et on se retrouve au point 3. ci-dessus.
2) Déclarer que la classe implémente une interface ⇒ 1)
La syntaxe consiste à utiliser non pas le mot extends mais le mot implements à la fin de la signature de la classe, suivi de
l'(les) interface(s) que la classe s'engage à respecter.
C. Création
Bien entendu, on peut aussi créer ses propres
interfaces. Il suffit de déclarer une interface dont le nom se termine
souvent en able (= capable de) et de remplacer
les 2 mots abstract
class par un seul
mot interface. A l'intérieur, 100% des méthodes
seront automatiquement public
abstract, ces 2 mots
seront donc inutiles. Si on souhaite y ajouter des constantes, 100% des
attributs seront automatiquement public static final, ces 3 mots
seront donc inutiles.
B) Il est important de faire chaque question de chaque exercice dans l'ordre,
et de compiler/tester lorsque c'est indiqué.
L'objectif
des exercices suivants est de permettre de gérer différentes sortes de comptes bancaires (dont le
solde est exprimé en euros) selon qu'ils sont rémunérés ou pas, et s'ils le
sont, selon que les intérêts seront comptabilisés chaque mois ou seulement une
fois par an.
Toutes les questions (se terminant par un ?) sont utiles à votre
apprentissage.
Le diagramme de classes ci-contre peut vous aider à comprendre
les relations entre les classes qu'on vous demande de créer.
|
|
1) Écrire la classe Compte dans le
projet tp51e comme expliqué ci-dessous :
- Ouvrir le fichier tp51e.jar dans BlueJ.
- Si une fenêtre s'ouvre avec une classe verte V,
double-cliquer sur <go up>.
- Fermer la fenêtre [veref] qui s'était initialement ouverte.
- Faire la suite du TP dans la fenêtre avec les 6 classes de test vertes.
- Positionnez cette nouvelle classe
juste à gauche de la classe CompteTest.
- Un attribut réel aSolde (pour simplifier l'exercice, on
n'ajoutera pas les attributs "évidents", tels que le numéro ou
le nom et le prénom du titulaire du compte)
Compilez la classe Compte, déclenchez Test all/Tout tester sur la classe CompteTest
et ne fermez plus la fenêtre qui apparaît (vous pouvez l'agrandir).
Les erreurs sont normales puisque la classe est à peine commencée ;
si les méthodes finissant par _1 et par _2 sont en vert,
passez à la suite ; sinon, cliquez sur la méthode qui ne passe pas
et lisez bien le message d'erreur sous la
barre rouge. Si vous ne le comprenez pas, appelez un intervenant.
- Un constructeur
« naturel » (à combien de paramètres ?)
Recompilez et exécutez la méthode (finissant
par) _3 dans CompteTest , comme expliqué ci-dessus en vert.
- Un accesseur getSolde() qui servira plutôt à
l'extérieur de la classe.
Recompilez et exécutez la méthode _4 dans CompteTest .
- L'objectif de ce 4ème
point est de créer un modificateur un peu particulier, car il ne se
contentera pas de mettre simplement dans l'attribut la valeur qu'on lui
passe en paramètre. Respectez bien les 4 étapes ci-dessous dans l'ordre.
a) Écrire d'abord un modificateur setSolde(), classique pour le moment.
b) Modifiez maintenant ce modificateur pour qu'il soit privé
(il vaut mieux ne pas pouvoir modifier le solde d'un compte bancaire à sa
guise). Du coup, renommez-le affecte (pour éviter toute confusion avec un
modificateur classique).
c) Modifiez son comportement pour qu'il affecte au solde la
valeur de son paramètre arrondi aux centimes ; il appellera donc la nouvelle
fonction suivante (à recopier, mais à comprendre, au moins sur
un ou deux exemples) :
private static double arrondi2(
final double pR )
{
double vR =
Math.abs( pR );
int vI = (int)(vR * 1000);
if ( vI%10 >=
5 ) vR = vR + 0.01;
return
Math.copySign( ((int)(vR*100))/100.0, pR );
} // arrondi2(.)
Si vous n'êtes pas encore allé regarder la javadoc de la méthode copySign (copier le signe), faites-le maintenant.
d) Modifiez le
constructeur pour qu'il affecte le
solde en tenant compte de l'arrondi.
Recompilez et exécutez la méthode _5
dans CompteTest .
Dans les procédures ci-dessous de cette classe, il ne faudra plus
directement modifier l'attribut, mais systématiquement appeler la
procédure affecte pour garantir qu'il est toujours correctement arrondi.
- Deux procédures : debite et credite qui respectivement
retranche
/ ajoute
au compte le montant passé en paramètre.
(n'y a-t-il pas une phrase écrite en rouge ci-dessus ?)
Recompilez et exécutez la méthode _6
dans CompteTest .
- Une procédure capitaliseUnAn sans paramètre ; comme il y
aura des comptes rémunérés ou non, son comportement dépendra de chaque
sorte de compte ; donc, peut-on écrire les instructions ?
Relire le A.2.
Quelle conséquence pour la classe a la déclaration particulière de
cette procédure ?
Relire le A.3.
Pour information, cette procédure est destinée à être appelée une
fois par an sur TOUS les comptes, quelle que soit leur sorte, pour
créditer chaque compte avec les intérêts accumulés pendant un an (possiblement
0 si c'est un compte non rémunéré).
Recompilez et exécutez la méthode _7
dans CompteTest .
- Une procédure bilanAnnuel qui :
a) affiche "solde=" suivi de la valeur du solde
courant,
b) puis appelle capitaliseUnAn (qui peut potentiellement modifier la valeur
du solde),
c) puis affiche " / après capitalisation, solde= " suivi de la nouvelle valeur du
solde courant.
Recompilez et exécutez la méthode _8
dans CompteTest .
2) Écrire la classe CompteCourant
- Positionnez cette nouvelle classe
juste à gauche de la classe CompteCourantTest.
- Un CompteCourant est une sorte de Compte ; donc ?
On s'en
servira pour gérer un compte non rémunéré.
Compilez la classe CompteCourant.
L'erreur de compilation est normale pour le moment.
- Pas d'attribut supplémentaire.
- Un constructeur
« naturel » (à combien de paramètres ?) Que se
passerait-il s'il était absent ?
On ne peut toujours pas compiler pour le moment.
- Redéfinir la procédure capitaliseUnAn() qui ici n'a rien à faire ... (information
que nous n'avions pas au 1.6 ci-dessus)
Recompilez, puis exécutez la méthode _4 dans CompteCourantTest.
Relancez Tout tester dans CompteTest.
3) Écrire la classe CompteRémunéré (sans
accent)
- Positionnez cette nouvelle classe
juste à gauche de la classe CompteRémunéréTest.
- Un CompteRémunéré est une sorte de Compte ; donc ?
Compilez la classe CompteRemunere . L'erreur de compilation est
normale : lisez le 3.5 ci-dessous et déduisez-en
immédiatement ce qu'il faut ajouter à ce que vous venez d'écrire pour
résoudre ce premier problème de compilation. La nouvelle erreur de compilation
ne sera résolue qu'une fois le constructeur écrit.
- Un attribut réel aTaux (représente
un pourcentage, par exemple : 1.0 pour 1% ).
L'erreur de compilation du point précédent demeure.
- Un constructeur
« naturel » (à combien de paramètres ?)
Compilez la classe CompteRemunere. Quand il
n'y a plus d'erreur, exécutez la méthode _3 dans CompteRemunereTest .
- Un accesseur getTaux() qui servira plutôt à
l'extérieur de la classe.
Recompilez et exécutez la méthode _4 dans CompteRemunereTest .
Relancez Tout tester dans CompteTest.
- Redéfinir la procédure capitaliseUnAn() ; comme il y aura des
rémunérations de compte calculées mensuellement ou annuellement, son
comportement dépendra de la sorte de compte rémunéré (Annuel ou
Mensuel) ; donc,
peut-on écrire les instructions ? (attention ! contrairement à CompteCourant, affirmer ici qu'il n'y aura
aucune instruction à exécuter pour tout compte rémunéré est faux ...)
Une fois que cette déclaration se compile correctement, essayer de la
mettre en commentaire.
Pourquoi cela ne change-t-il rien ?
Relancez Tout tester dans CompteTest.
4) Écrire la classe CompteAnnuel (ne pas compiler avant d'avoir écrit le 4.4)
- Un CompteAnnuel est une sorte de CompteRémunéré ; donc ?
- Pas d'attribut supplémentaire.
- Un constructeur
« naturel » (à combien de paramètres ?) Que se
passerait-il s'il était absent ?
- Redéfinir la procédure capitaliseUnAn()qui applique en une seule fois
le taux (considéré comme annuel) pour créditer les intérêts.
Compilez et exécutez la méthode _4 dans CompteAnnuelTest .
Relancez Tout tester dans CompteRemunereTest.
5) Écrire la classe CompteMensuel (ne pas compiler avant d'avoir écrit le 5.4)
- Un CompteMensuel est une sorte de CompteRémunéré ; donc ?
- Pas d'attribut supplémentaire.
- Un constructeur
« naturel » (à combien de paramètres ?) Que se
passerait-il s'il était absent ?
- Redéfinir la procédure capitaliseUnAn()qui appliquera 12 fois
successivement le taux (considéré comme mensuel) pour créditer
les intérêts.
Contrainte dans tout cet exercice : Ne pas utiliser de boucle.
Donc écrire et appeler une procédure auxiliaire récursive capitaliseUnMois(nbMois)à un seul paramètre : le
nombre de mois restant à capitaliser.
Compilez et exécutez la méthode _4 dans CompteMensuelTest .
Relancez Tout tester dans CompteRemunereTest.
6) Écrire la classe Utilisation (juste pour
essayer les classes précédentes ; donc ?)
- Juste une procédure essai (pratique à utiliser) pour contenir les actions
suivantes :
Compilez et exécutez la méthode _3 dans UtilisationTest .
- Déclarer/créer un CompteCourant vC avec 1000 euros.
- Déclarer/créer un CompteAnnuel vA avec 1000 euros au taux (annuel)
de 6%.
- Déclarer/créer un CompteMensuel vM avec 1000 euros au taux (mensuel)
de 0,5%.
- Déclarer/créer un tableau de Compte initialisé avec vC, vA, et vM (possible en une seule ligne
!) .
- Dans une boucle parcourant tous
les éléments du tableau, effectuez le bilanAnnuel de chaque compte.
Les deux derniers comptes n'ont pas le même solde ? Comprenez pourquoi.
Compilez et exécutez la méthode _4 dans UtilisationTest .
Relancez Tout tester dans CompteAnnuelTest et CompteMensuelTest.
7) Interface Comparable
- On souhaite maintenant que les
comptes soient comparables uniquement d'après leur solde. L'interface Comparable existe dans le JDK. Pourquoi s'imposer de la
respecter alors qu'on pourrait en créer une autre ? Parce-que le JDK nous
fournit des outils manipulant des objets Comparable, dont notamment une méthode de
tri.
Cette interface nous impose donc de définir la méthode int compareTo(Object).
Pour simplifier les spécifications des concepteurs du langage Java sur la
méthode compareTo() :
- Cette fonction doit retourner
un entier négatif/nul/positif (généralement
-1/0/+1) selon que l'objet courant est inférieur/égal/supérieur à l'objet passé en paramètre.
- Contrairement à la méthode equals, il n'est pas demandé de
vérifier que l'objet passé en paramètre a le même type constaté que
l'objet courant. Dans le cas contraire, il y aura simplement une erreur (ClassCastException).
- Pour être tout à fait exact,
la javadoc v11 indique un paramètre de type T (et non Object) pour compareTo, ce qui fait appel au concept
de généricité. Mais nous n'étudierons ce concept que lors de la prochaine
séquence (à propos des collections).
- Écrire ce qu'il faut pour que la
classe Compte respecte l'interface Comparable. Donc, quelle méthode ?
- Pour ne pas avoir à programmer en détail
la fonction compareTo(), il est possible d'appeler la
méthode compareTo() déjà écrite dans la classe Double du JDK (noter la majuscule
et regarder la documentation !). Il suffit de créer un objet Double à partir du double qu'on veut comparer.
- Sinon, il faudrait écrire
encore plus de lignes que dans equals.
- Est-il nécessaire de redéfinir compareTo() dans CompteCourant si l'on désire comparer des CompteCourant ? De même, est-ce nécessaire
pour des CompteRémunéré ?
- Compléter la méthode Utilisation.essai() pour qu'elle affiche les résultats de compareTo() entre les 3 sortes de Compte, avant
la boucle du 6.6 ci-dessus.
Après cette boucle,
ajouter un CompteCourant vC2 avec 1060 euros, et le comparer aux 3 autres
comptes. Obtenez-vous bien 1, 0, -1 ?
Que se passe-t-il si vous écrivez ... vC.compareTo( "Ceci n'est pas un
compte" ) ... ?
Compilez et exécutez la méthode _5 dans UtilisationTest .
Relancez Tout tester dans CompteCourantTest.