ENPC - Objets et Patterns - Corrigés des exercices de la séance 5

Exercice 1: redéfinition de la méthode same()

Intuitivement, puisqu'on a une méthode
    public boolean same(Disque d)
dans la classe Disque, on aimerait avoir une méthode
    public boolean same(Anneau d)
dans la classe Anneau. Cette méthode pourrait alors être la suivante.

  /**
   * Si l'anneau courrant vu comme un disque a même centre et même
   * rayon (super.same()) et que les deux objets on même rayon
   * interne, alors cette méthode same retourne vrai. Sinon elle
   * retourne faux.
   */
  public boolean same(Anneau a) {
    return (super.same(a) && (rayonInterne == a.rayonInterne));
  }

Le problème qui se pose alors est le suivant. Lorsqu'on demande à un anneau de se comparer avec un disque, la réponse peut être vraie ce qui n'est pas acceptable. Par exemple:

  Point p = new Point();
  Disque d1 = new Disque(p, 1);
  Disque d2 = new Disque(p, 1);
  Anneau a1 = new Anneau(p, 1, 2);
  Anneau a2 = new Anneau(p, 1, 2);
  System.out.println(d1.same(d2)); // affiche true
  System.out.println(a1.same(a2)); // affiche true
  System.out.println(d1.same(a1)); // affiche true: acceptable
  System.out.println(a1.same(d1)); // affiche true: inacceptable

Or, un anneau de rayon 1 et de rayon interne 2 qui se compare à un disque de rayon 1 DOIT tester le rayon interne et dire qu'il n'y est pas "identique". La raison de ce problème est que, dans la classe Anneau, il y a désormais le choix entre appliquer deux méthodes, l'une acceptant un Anneau en argument (celle que l'on vient de définir) et l'autre acceptant un Disque en argument (celle qui est héritée de la classe Disque). Ainsi, il est possible de faire un test d'identité avec same en réduisant les deux objets à leurs caractéristiques communes. En fait, au lieu de redéfinir la méthode same dans Anneau, on l'a surchargée.

Dans la classe Disque, la méthode same() a le profil suivant:
    public boolean same(Disque d)
Pour redéfinir cette méthode dans la classe Anneau, il est indispensable de lui donner exactement le même profil. En effet, si elle a un profil légèrement différent tel que:
    public boolean same(Anneau d)
alors il ne s'agit pas d'une redéfinition mais d'une surcharge, c'est à dire que les deux méthodes cohabitent dans la classe Anneau: la première est y est héritée et la seconde y est définie. Ainsi, il faudrait donc définir same() dans la classe Anneau par une méthode de profil:
    public boolean same(Disque d)
Le problème, c'est qu'on ne peut alors plus considérer systématiquement l'argument d comme un anneau. La solution à ce problème est expliquée avec une redéfinition de la méthode equals() provenant de la classe Object. Nous y reviendrons ci-dessous.

Exercice 2: héritage des méthodes de la classe Object

Toutes les méthodes de la classe Object sont héritées (implicitement) dans toutes les classes. Celle qui est utilisée le plus souvent est toString(). Par exemple, si Disque et Anneau ne définissent aucune méthode toString(), cellehéritée de Object est utilisée:

  Anneau a1 = new Anneau(p, 1, 2); 
  System.out.println(a1.toString()); // affiche Anneau@a0139918   
  Object o = new Object();
  System.out.println(o.toString());  // affiche java.lang.Object@afef9918 

Celle-ci affiche normalement le nom de la classe de l'objet (avec son package) suivie d'une valeur hexadécimale représentant un code pour la référence à cette instance (le hashcode de cette instance qui, par défaut, est l'adresse de la référence en mémoire).

Il faut également noter que écrire
    System.out.println(a1.toString());
ou
    System.out.println(a1);
produit exactement la même chose. En fait, lorsque la méthode println() reçoit en argument quelque chose qui est une référence (autre chose qu'un type primitif), elle appelle la méthod toString() sur cette référence pour savoir quelle chaîne de caractère elle doit afficher. Il est donc très important de redéfinir la méthode toString() dans toutes les classes que l'on crée, afin que lorsqu'on affiche une instance de cette classe, ce soit "notre" méthode toString() qui soit utilisée plutôt que celle de la classe Object.

Exercice 3: égalité entre objets avec equals()

La méthode equals() de la classe Object est donc héritée dans toutes les classes, comme la méthode toString(). Plutôt que la méthode same() qui nous posait des problèmes, nous alons donc utiliser cette méthode equals(). Néanmoins, comme pour la méthode toString(), le comportement qu'elle définit dans la classe Object ne nous convient pas pour toutes les classes dans lesquelles elle est héritée. Ce comportement par défaut est de tester l'égalité des références aux objets (et non l'état des objets qui sont accessibles par les références). Par exemple:

  Object o1 = new Object();
  Object o2 = new Object();
  System.out.println(o1.equals(o1));  // affiche true mais
  System.out.println(o1.equals(o2));  // affiche false
  Anneau a1 = new Anneau(p, 1, 2);
  Anneau a2 = new Anneau(p, 1, 2);
  System.out.println(a2.equals(a2));  // affiche true mais
  System.out.println(a1.equals(a2));  // affiche false

Aussi, ce que l'on veut pour les classes Disque et Anneau, c'est tester l'égalité de chacun des champs des objets pour affirmer qu'ils sont "égaux". Il nous faut donc redéfinir la méthode equals() mais en faisant attention à lui donner exactement le même profil (si le type de l'argument change, ce n'est plus une redéfinition, mais une surcharge).

Nous allons utiliser l'opérateur instanceof qui teste si une variable contient un objet qui est d'un type donné (ou de l'un de ses sous-types). D'autres explications sur cet opérateur sont données dans l'exercice suivant.

Ainsi, dans la classe Disque:

  /**
   * Redéfinition de la méthode equals() héritée de la classe Object.
   * Renvoie vrai si o contient un Disque qui a même centre et même
   * rayon que this.
   */
  public boolean equals(Object o) {
    // si l'instance référencée par o n'est pas un Disque, alors pas égaux
    if (!(o instanceof Disque))
      return false;
    // sinon, on récupère ce disque par un cast et on teste chaque champ
    Disque d = (Disque) o;
    return ((centre.same(d.centre)) && (rayon == d.rayon));
  }

Et, dans la classe Anneau:

  /**
   * Redéfinition de la méthode equals() héritée de Disque.
   * Renvoie vrai si o contient un Anneau qui a même centre, même
   * rayon et même rayon interne que this.
   */
  public boolean equals(Object o) {
    // si l'instance référencée par o n'est pas un Anneau, alors pas égaux
    if (!(o instanceof Anneau))
      return false;
    // sinon, on récupère cet anneau par un cast
    Anneau a = (Anneau) o;
    // on fait appel à super.equals() pour tester le centre et le rayon
    // et on teste localement le rayon interne
    return ((super.equals(a)) && (rayonInterne == a.rayonInterne));
  }

Un autre avantage de la redéfinition de la méthode equals() héritée de la classe Object est qu'elle peut être utilisée sur n'importe quel type de variable (tous les types sont des sous-type du type Object). C'est le mécanisme de liasion tardive (résolution dynamique de méthode) qui assure qu'à l'exécution c'est la méthode redéfinie la plus précise qui sera utilisée. Exemple:

  Object o1 = new Anneau(p, 1, 2);
  Object o2 = new Anneau(p, 1, 2);
  Object o3 = new Disque(p, 1);
  System.out.println(o1.equals(o1));  // affiche true
  System.out.println(o1.equals(o3));  // affiche false
  System.out.println(o3.equals(o1));  // affiche true 

Le premier affichage ci-dessus est vrai, car c'est la méthode equals() de la classe Anneau (type de l'objet contenu dans o1) qui est appelée. Le second affichage est faux car c'est la même méthode qui est appelée, celle de Anneau, or l'argument n'est qu'un Disque et non un Anneau. Le dernier affichage est vrai car, même si ce sont les mêmes objets que l'on compare, c'est à o3 qu'on a "demandé" la comparaison. Or o3 contient un objet de la classe Disque et c'est sa méthode equals() qui est appelée. Celle-ci vérifie que l'argument (o1) contient bien un Disque, ce qui est vrai puisque un Anneau est un Disque, puis teste uniquement le centre et le rayon. Comme ils sont égaux, elle dit qu'à son sens o1 contien un objet qui lui est identique en tout point (même si o1 possède des caractéristiques supplémentaires qu'elle ne connaît pas).

Exercice 4: test du type d'une référence contenue dans une variable (instanceof)

L'opérateur instanceof permet de connaître le type dynamique (au moment de l'exécution) de la référence contenue dans une variable. Cela est quelquefois utile puisqu'une variable peut contenir une référence de n'importe quel sous-type du type déclaré.

En résumé, l'instruction v instanceof C vaut true si la référence contenue dans la variable v a été créée par un new C(...) ou bien par un new D(...) ou D est une sous-classe de C. Le programme suivant illustre bien ce phénomène.

public class Aleatoire {
  public static void main(String[] args) {
    Object o;
    if (Math.random()<0.5)
      o = new Disque();
    else
      o = new Anneau();
    if (o instanceof Anneau)
      System.out.println("C'est un Anneau");
    else
      System.out.println("C'est un Disque");

    if (o instanceof Disque) 
      System.out.println("Dans tout les cas, c'est un Disque "+
			 "puisque un Anneau est aussi un Disque");
  }
}

Exercice 5: interface (Mesurable)

L'interface Mesurable représente un "contrat" qui dit que toute classe implémentant cette interface devra fournir une méthode surface():

public interface Mesurable {
  public double surface();
}

Si une classe C implémente Mesurable, elle devient alors une sorte de Mesurable et C devient ainsi une sous-classe (ou un sous type) de Mesurable. Cela permet, par exemple, de stocker une référence à une instance de C dans une variable déclarée de type Mesurable:

Mesurable m = new C();

De la même manière, l'expression m instanceof Mesurable retournera true puisque à l'exécution, l'instance contenue dans m est une d'un sous-type de Mesurable.

Nos classes Disque et Anneau disposent déjà chacune d'une méthode surface(). Néanmoins, elles ne sont pas des sous-classes de Mesurable tant qu'elles n'ont pas explicitement revendiquées qu'elles implémentaient cette interface. Nous allons donc uniquement modifier leur déclaration comme ceci:

public class Disque implements Mesurable {
  // ... le reste de la classe Disque est identique
} 
public class Anneau extends Disque implements Mesurable {
  // ... le reste de la classe Anneau est identique
}

Désormais, il est possible de stocker des instances de Disque et Anneau dans des variables déclarées de type Mesurable. De la même façon, lorsqu'on manipule une variable de type Mesurable, on n'a pas besoin de savoir si elle contient un Disque, un Anneau ou autre chose. Ce que l'on sait, c'est que l'on peut lui faire tout ce que déclare savoir faire l'interface Mesurable, c'est à dire la méthode surface().

Exercice 6: somme de Mesurable

public class Test {
  /**
   * Cette méthode retourne la somme des surfaces des objets du
   * tableau tab qu'elle accepte en argument.
   */
  public static double somme(Mesurable[] tab) {
    double som = 0;
    for(int i=0; i<tab.length; i++) {
      som = som + tab[i].surface();
    }
    return som;
  }
  public static void main(String[] args) {
    Point p = new Point();
    Anneau a = new Anneau(p, 1, 2);
    Disque d = new Disque(p, 3);
    Mesurable[] t = new Mesurable[]{a,d};
    System.out.println("La somme des mesurables du tableau est: " +
		       Test.somme(t));
  }
}

Exercice 7: classe Carre

La classe Carre n'a rien de compliqué en soi. On veut seulement qu'elle implémente l'interface Mesurable et pour celà, elle doit définir une méthode surface().

public class Carre implements Mesurable {
  private Point coin;
  private double cote;
  public Carre(Point coin, double cote) {
    this.coin = coin;
    this.cote = cote;
  }
  public Carre() {
    this.coin = new Point();
    this.cote = 0;
  }
  public double surface() {
    return cote * cote;
  }
}

Il est alors tout à fait possible d'utiliser notre méthode somme() sur un tableau qui contient des Disque, des Anneau ou des Carre: ils ont tous en commun leur super-type Mesurable:

  Point p = new Point();
  Anneau a = new Anneau(p, 1, 2);
  Disque d = new Disque(p, 3);
  Carre c = new Carre(p, 5);
  Mesurable[] t = new Mesurable[]{a,d,c};
  System.out.println("La somme des mesurables du tableau est: " +
                     Test.somme(t));


Etienne.Duris[at]univ-mlv.fr - © École Nationale des Ponts et Chaussées - Decembre 2000 - http://www-igm.univ-mlv.fr/~duris/ENPC/index2000.html