TP 5  2425

A) LIRE EN DÉTAIL (et poser des questions !)

Les abstractions

  1. 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 ?
  2. 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 ; !
  3. 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.
  4. 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.

  1. Positionnez cette nouvelle classe juste à gauche de la classe CompteTest.
  2. 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.
  3. 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.
  4. Un accesseur getSolde() qui servira plutôt à l'extérieur de la classe.
    Recompilez et exécutez la méthode _4 dans CompteTest .
  5. 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.
  6. 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 .
  7. 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 .
  8. 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 

  1. Positionnez cette nouvelle classe juste à gauche de la classe CompteCourantTest.
  2. 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.
  3. Pas d'attribut supplémentaire.
  4. Un constructeur « naturel » (à combien de paramètres ?) Que se passerait-il s'il était absent ?
    On ne peut toujours pas compiler pour le moment.
  5. 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)

  1. Positionnez cette nouvelle classe juste à gauche de la classe CompteRémunéréTest.
  2. 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.
  3. 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.
  4. 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 .
  5. 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.
  6. 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)

  1. Un CompteAnnuel est une sorte de CompteRémunéré ; donc ?
  2. Pas d'attribut supplémentaire.
  3. Un constructeur « naturel » (à combien de paramètres ?) Que se passerait-il s'il était absent ?
  4. 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)

  1. Un CompteMensuel est une sorte de CompteRémunéré ; donc ?
  2. Pas d'attribut supplémentaire.
  3. Un constructeur « naturel » (à combien de paramètres ?) Que se passerait-il s'il était absent ?
  4. 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 ?)

  1. Juste une procédure essai (pratique à utiliser) pour contenir les actions suivantes :
    Compilez et exécutez la méthode _3 dans UtilisationTest .
  2. Déclarer/créer un CompteCourant vC avec 1000 euros.
  3. Déclarer/créer un CompteAnnuel vA avec 1000 euros au taux (annuel) de 6%.
  4. Déclarer/créer un CompteMensuel vM avec 1000 euros au taux (mensuel) de 0,5%.
  5. Déclarer/créer un tableau de Compte initialisé avec vC, vA, et vM (possible en une seule ligne !) .
  6. 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

  1. 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() :
  2. Écrire ce qu'il faut pour que la classe Compte respecte l'interface Comparable. Donc, quelle méthode ?
  3. 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é ?
  4. 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.