TP 4.1 2425
Ce TP, indépendant des TP précédents, peut et doit être réalisé même s'ils ne sont pas terminés ;
par contre, ils doivent être terminés d'urgence en travail personnel !

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

L'Héritage entre classes

  1. Une classe B peut hériter d'une (et une seule) classe A ; pour cela, il faut juste ajouter une clause extends à la signature de la classe B, ce qui donne :
     public class B extends A { ... }
    Dans BlueJ, cette relation est matérialisée par une flèche avec une pointe en triangle non noirci.
    B est appelée une sous-classe de A et Aest appelée la super-classe de B.

    La classe B bénéficie alors de tous les attributs et de toutes les méthodes (non privées) de la classe A, et peut en ajouter.
    Ce qui veut dire qu'un objet B comportera, en plus de ses attributs, tous les attributs de A.
    Cette technique doit être utilisée uniquement si l'on peut dire que "B est une sorte de A". Un signe que l'héritage ne doit pas être utilisé entre B et A peut être qu'un attribut ou une méthode de A n'a pas de sens pour un objet de B.
    Par exemple, un oiseau est une sorte d'animal (puisqu'un oiseau possède au moins toutes les caractéristiques d'un animal), mais une maison n'est pas une sorte de rectangle avec un triangle au-dessus (car la maison ne possède pas toutes les caractéristiques d'un rectangle, comme calculer sa surface juste en multipliant une dimension par l'autre). Par contre, un carré peut être considéré comme une sorte de rectangle.

  2. Java autorise une variable vA déclarée de type Animal à contenir une référence vers un objet de type Oiseau (mais pas le contraire ! Pourquoi ?) :
    Animal vA = new Oiseau();
    Cela nous permet de définir 2 types pour une variable référence : le type déclaré (utilisé uniquement – et toujours – lors de la compilation) et le type constaté (utilisé uniquement – et toujours – à l'exécution).
    Dans notre exemple, le type déclaré de vA est Animal alors que son type constaté est Oiseau.
    Une conversion de type ne peut jamais changer le type constaté d'un objet ; elle permet de créer une nouvelle référence vers le même objet, mais avec un type déclaré différent, par exemple : Oiseau vO = (Oiseau)vA;
    vO et vA pointent sur le même objet, mais à la compilation, vA n'est utilisable que comme un Animal alors que vO est utilisable comme un Oiseau (par exemple, si on a ajouté des attributs et des méthodes dans la classe Oiseau).
    Il est à noter enfin qu'une conversion B vB = (B)vA; peut provoquer une erreur de compilation si la classe B n'hérite pas de la classe A, ou une erreur (ClassCastException) à l'exécution si vA ne pointe pas effectivement sur un objet de type B.

  3. TOUTES les classes héritent automatiquement de la classe Object du JDK (c'est la "mère" de toutes les classes).
    Cette classe contient notamment ces trois méthodes très utiles :
  4. Lorsqu'on redéfinit une méthode dans une sous-classe, on fait précéder la signature de @Override pour que le compilateur détecte nos éventuelles erreurs en vérifiant qu'une méthode de même signature existe bien dans une super-classe de la classe courante. (pourquoi "une" et pas "la" super-classe ?)
    Dans le cas où une méthode m de la super-classe a été redéfinie dans une sous-classe, this.m(); appelle automatiquement la méthode du type constaté de this (généralement la sous-classe) à la différence du compilateur (qui ne tient compte que du type déclaré pour décider de signaler une erreur de compilation).    

B) Il est important de faire chaque question de chaque exercice dans l'ordre, et de compiler/tester uniquement à chaque fois que l'énoncé vous le demande.
Répondre aux questions entre parenthèses est souvent nécessaire pour trouver comment résoudre l'exercice qui précède.

Les 3 classes de tests qui vous sont fournies utilisent beaucoup de techniques qui vous sont inconnues ; ce n'est donc pas la peine de perdre du temps à regarder leur code. Pour lancer une méthode de test, il suffit d'un clic-droit sur la classe "verte" ... Une fois que vous obtenez le succès aux tests automatiques fournis, vous devez tester les méthodes interactivement pour vérifier qu'elles fonctionnent comme désiré.
Remarque :
Pour éviter tout problème avec les tests automatiques, il vaut mieux respecter le style recommandé dans le Style Guide sur la page web de l'unité :

1) Écrire une nouvelle classe Point dans le paquetage tp du projet tp41e.jar ci-joint

  1. Cette classe n'aura de particularité liée à l'héritage qu'au point 5 ci-dessous. Pour la créer, il suffit de cliquer sur le bouton New Class et de supprimer les exemples de syntaxe Java qui s'y trouvent avant de passer au point 2.
  2. Deux attributs entiers aX et aY .
    Commencer par afficher et ne pas fermer la fenêtre de résultats des tests en allant dans le menu Voir/View et en cliquant sur Show Test results.
    Compiler la classe Point et exécuter la méthode commençant par test2 dans la classe PointTest. Si la barre est verte, passer à la suite ; sinon, cliquer sur le 1er test qui a échoué, et bien lire le message d'erreur sous la barre rouge. Si vous ne le comprenez pas, appelez un intervenant.
  3. a) Un constructeur « naturel » (à combien de paramètres ?).
    b) Un constructeur sans paramètre qui crée le point en (10,10) sans duplication de code (donc, rappel : comment appeler un autre constructeur de la même classe ?
    Comment sait-on quel constructeur va être appelé s'il y en a plusieurs dans la classe ?

    Recompiler et exécuter la méthode (commençant par) test3 comme expliqué ci-dessus en vert.
  4. Une procédure deplace à deux paramètres entiers (pDeltaX,pDeltaY) qui effectue la translation (Δx,Δy) du point courant.
    Si vous avez dû accéder à l'attribut aX, l'avez-vous bien écrit de la même façon que dans le constructeur naturel, par exemple ?
    Recompiler et exécuter la méthode test4.
  5. Redéfinir la fonction toString qui retourne le point sous la forme "(x,y)", par exemple "(10,15)". (Attention à respecter la signature de la méthode, relire le A.3 ci-dessus, et signaler la redéfinition au compilateur, relire le A.4 ci-dessus).
    Recompiler et exécuter test5.
  6. Une procédure affiche qui affiche le point sous la même forme, mais entre "<<" et ">>", par exemple "<<(10,15)>>", et sans duplication de code !
    Recompiler et exécuter test6. Pourrait-on ne pas explicitement appeler la méthode toString() ? (relire le A.3 ci-dessus)

2) Écrire une nouvelle classe PointColore (sans accent, sans modifier la classe Point)

  1. Un PointColoré est une sorte de Point ; comment faut-il donc l'indiquer au compilateur ? (relire le A.1 ci-dessus)
    Compiler la classe PointColore et éloigner les 2 classes dans le diagramme des classes de BlueJ pour voir la flèche d'héritage.
  2. Un attribut aCouleur stocké dans une String d'un seul caractère (dans la suite, on notera les couleurs juste avec leur initiale, par exemple : "N" pour Noir) .
    Recompiler la classe PointColore et exécuter la méthode commençant par test2 dans la classe PointColoreTest.
  3. Un constructeur « naturel » (à combien de paramètres ?, donc combien y a-t-il de valeurs (attributs) dans chaque objet PointColoré ? Relire le A.1 ci-dessus)
    Essayer d'écrire l'initialisation des attributs comme d'habitude ; comment peut-on choisir x et y si on ne peut pas accéder aux coordonnées ?
    + Donc, comment appeler un constructeur de la super-classe ? ==> super(paramètres_attendus_par_le_constructeur_appelé);
    Cette instruction ne peut apparaître que dans un constructeur et en première instruction.

    Recompiler et exécuter la méthode (commençant par) test0 comme expliqué ci-dessus en vert.
  4. Un constructeur à 2 paramètres qui force la couleur à "N" (comme Noir) sans duplication de code
  5. Un constructeur sans paramètres qui crée un point Noir en (10,10) sans duplication de code
    Si l'instruction que vous avez écrite comporte 2 virgules, peut-on l'écrire avec une seule ? Et si vous l'avez écrite avec une seule, peut-on l'écrire avec deux ?
    Recompiler et exécuter la méthode (commençant par) test3 comme expliqué ci-dessus en vert.
  6. Redéfinir la fonction toString qui retourne le point sous la forme "couleur:(x,y)", par exemple "R:(23,12)" (Attention ! Il n'y a aucun espace dans ces String), sans duplication de code (lire l'aide ci-dessous) et en signalant la redéfinition au compilateur (relire le A.4 ci-dessus).
    + Donc, comment appeler sur l'objet courant une méthode de même signature dans la super-classe ? ==> super.methode(paramètres);

    Recompiler et exécuter la méthode test4.

3) Écrire une nouvelle classe Utilisation (qui n'a pas vocation à être instanciée ; quelles conséquences pour les méthodes ?)

  1. Pas d'attribut, juste une procédure essai (sans paramètre) pour contenir les actions 2. 3. 4. suivantes :
    Compiler la classe Utilisation et exécuter la méthode commençant par test4 dans la classe UtilisationTest.
    + Si vous avez une erreur dans les tests vous signalant qu' essai n'est pas static, voici l'explication :
    L'énoncé dit que la classe Utilisation n'a pas vocation à être instanciée ; donc, on ne créera pas d'objet de cette classe ; donc, comment pourra-t-on appeler la procédure essai (qui, comme toutes les méthodes vues jusqu'à présent est une "méthode d'instance", donc a besoin d'une instance pour qu'on puisse l'appeler dessus) ?
    Il faudrait pouvoir appeler essai directement sur la classe Utilisation, donc pouvoir définir une "méthode de classe", qui n'a besoin d'aucune instance, puisqu'on pourrait l'appeler directement sur la classe).
    C'est justement le cas des méthodes statiques (celles du JDK et celles qu'on peut créer).
    Dans le JDK, il suffit de regarder la javadoc pour voir le mot-clé static au début de la signature (au début car le mot-clé public n'est pas indiqué, puisque si la méthode était privée, on ne pourrait pas s'en servir).
    Dans vos classes, il suffit d'ajouter le mot-clé static juste après public pour transformer votre méthode en méthode de classe.
    Faites-le pour essai, compilez, et constatez la différence en faisant un clic droit sur la classe Utilisation !
    Ensuite, exécutez à nouveau test4... dans UtilisationTest, puis vérifiez qu' essai affiche bien ce qui est attendu.

  2. Déclarer/créer* un Point en (10,15) et l'afficher.

  3. Déclarer/créer* un PointColoré rouge en (23,12) et l'afficher. (faut-il une procédure affiche() dans PointColoré ? Relire le A.1 ci-dessus)

  4. Déplacer le PointColoré, l'afficher à nouveau. (faut-il une procédure deplace() dans PointColoré ? Relire le A.1 ci-dessus)
    * Ne pas utiliser le constructeur par défaut.

  5. Lancez/exécutez la procédure essai() et vérifiez les affichages.
    Sont-ils conformes à ce que vous attendiez ? Sinon, améliorez. Si oui, passez à la suite.

4) Égalité de 2 points (exercice très important)

Pour l'instant, écrire p1.equals(p2) revient à écrire p1==p2 (comme expliqué au 2ème point du A.3 ci-dessus).
Nous voudrions que la méthode equals compare les attributs des 2 points pour répondre true quand ils sont tous égaux, même si p1≠p2, c'est-à-dire que les deux points ne sont pas à la même adresse.

Pour que notre méthode de test d'égalité soit reconnue par toutes les méthodes déjà programmées dans le JDK (par exemple, dire si un élément appartient à une collection de données), nous devons forcément redéfinir la méthode equals de la classe Object, et pas inventer une nouvelle méthode (relire les A.3 et A.4 ci-dessus).

  1. Voici, pour information, ce que disent les spécifications des concepteurs du langage Java sur la méthode equals :
  2. Redéfinir la fonction equals dans Point (Attention à la signature, c'est une redéfinition, donc ?), en suivant l'un des deux énoncés ci-dessous
    Recompiler la classe Point et exécuter la méthode commençant par test7 dans la classe PointTest.

    ÉNONCÉ SANS EXPLICATIONS (mais écrire les instructions dans l'ordre a. b. c. d., donc en remontant dans le code) :
    1. Cas général : sauter 3 lignes, puis écrire les comparaisons d'attributs en supposant que le paramètre est un Point, puis
    2. Cas n°3 : (juste avant) tester l'hypothèse contraire (c-à-d si ce n'est pas un Point), puis
    3. Cas n°2 : (juste avant) tester le cas qui peut empêcher l'appel de méthode au cas n°3, puis
    4. Cas n°1 : (juste avant) tester le cas trivial qui rend inutile le reste de la fonction (réflexivité) !

    ÉNONCÉ (pas à pas) AVEC EXPLICATIONS :
    1. Écrire la signature de la méthode précédée de @Override et suivie de {return true;/*a modifier*/} puis compiler.
      Corriger la signature en cas d'erreur de compilation.
    2. Cas général : remplacer le true précédent par la comparaison 2 à 2 de chaque attribut de l'objet courant et du paramètre, puis compiler.
      Aide : Il est possible de combiner 2 conditions avec un ET logique grâce à l'opérateur && .
      Expliquer l'erreur de compilation. Ajouter la conversion de référence : Point vP = (Point)pObj;
      (relire le A.2) Corriger les comparaisons précédentes, puis compiler.
    3. Cas n°3 (juste avant l'instruction que l'on vient d'écrire) : Vérifier que pObj est bien un Point pour avoir le droit d'écrire la conversion précédente. Sinon, directement retourner FAUX.
      Aide : Une fonction de la classe Object décrite au A.3. peut vous être utile ...
    4. Cas n°2 (juste avant) : A-t-on toujours le droit d'écrire pObj.methode() quelle que soit la valeur de pObj ?
      Sinon, tester le cas "interdit" et retourner directement FAUX.
    5. Cas n°1 (tout au début) : Dans quel cas peut-on directement retourner VRAI sans avoir à exécuter le reste de la fonction (réflexivité) ?

  3. Redéfinir la fonction equals dans PointColoré  (en évitant la duplication de code)
    Recompiler la classe PointColore et exécuter la méthode commençant par test5 dans la classe PointColoreTest.
    Aide : Si on y appelle la fonction equals de Point, cela va-t-il vérifier que pObj est un Point ou un PointColoré ?

  4. Compléter la méthode Utilisation.essai() pour qu'elle teste les 2 versions d'equals ci-dessus, dans des exemples bien choisis pour tester les différents cas possibles.

5) Relisez attentivement la section "A) Héritage entre classes" ci-dessus

Si vous ne comprenez pas 100% de ce qui y est écrit, posez des questions !


tp41e.jar


Re: TP 4.1
par Denis BUREAU, jeudi 30 janvier 2020, 14:01
 

Un étudiant a écrit :

J'ai une question sur les méthodes statiques.
Dans le tp3.3, il est noté qu'une méthode statique ne peut appeler que des méthodes statiques,
pourtant dans le tp4.1, la méthode statique esssai() appelle affiche et equals() qui ne sont pas statiques.


Re: TP 4.1
par Denis BUREAU, jeudi 30 janvier 2020, 14:06
 

La phrase du TP 3.3 était ambigüe, je l'ai supprimée.

Rien ne vous empêche d'appeler des méthodes non statiques sur des objets que vous avez créés.
Vous ne pouvez simplement pas appeler une méthode non statique sur l'objet courant, vu qu'il n'y en a pas ...