Previous Up Next

B.2  Obscur objet

Dans cette section, nous examinons la dialectique question de la classe et de l’objet.

B.2.1  Utilisation des objets

Sans même construire des objets nous mêmes, nous en utilisons forcément, car l’environnement d’exécution de Java est principalement en style objet. Autrement dit on sait déjà que les objets ont des méthodes.

  1. Les tableaux sont presque des objets.
  2. Les chaînes String sont des objets.
  3. La bibliothèque construit des objets dont nous appelons les méthodes, voir out.println().

Les objets possèdent aussi des champs également appelés variables d’instance, auxquels on accède par la notation en « . » comme pour les variables de classes. Par exemple, si t est un tableau t.length est la taille du tableau.

La bibliothèque de Java emploie énormément les objets. Par exemple, le point est un objet de la classe Point du package java.awt. On crée un nouveau point par un appel de constructeur dont la syntaxe est :

Point p = new Point () ; // Point origine

On peut aussi créer un point en donnant explicitement ses coordonnées (entières) au constructeur :

Point p = new Point (100,100) ;

On dit que le constructeur de la classe Point est surchargé (overloaded), c’est-à-dire qu’il y a en fait deux constructeurs qui se distinguent par le type de leurs arguments. Les méthodes, statiques ou non, peuvent également être surchargées.

On peut ensuite accéder aux champs d’un point à l’aide de la notation usuelle. Les points ont deux champs x et y qui sont leurs coordonnées horizontales et verticales :

if (p.x == p.y)
  System.out.println("Le point est sur la diagonale");

Les points possèdent aussi des méthodes, par exemple la méthode distance, qui calcule la distance euclidienne entre deux points, renvoyée comme un flottant double précision double. Un moyen assez compliqué d’afficher une approximation de √2 est donc

  System.out.println(new Point ().distance(new Point (1, 1))) ;

Les objets sont reliés aux classes de deux façons :

Un premier exemple (assez extrême) de cette distinction est donné par null. La valeur null n’a ni classe d’origine (elle n’est pas créé par new), ni champs, ni méthodes, ni rien, mais alors rien du tout. En revanche, null appartient à toutes les classes-types.

B.2.2  Fabrication des objets

Notre idée est de montrer comment créer des objets très similaires aux points de la section précédente. On crée très facilement une classe des paires (d’entiers) de la façon suivante :

class Pair {
  int x ; int y ;

  Pair () { this(0,0) ; }

  Pair (int x, int y) { this.x = x ; this.y = y ; }

  double distance(Pair p) {
    int dx = p.x - this.x ;
    int dy = p.y - this.y ;
    return Math.sqrt (dx*dx + dy*dy) ; // Math.sqrt est la racine carrée
  }
}

Nous avons partout explicité les accès aux variables d’instance et par this.x et this.y. Nous nous sommes aussi laissés aller à employer la notation this(0,0) dans le constructeur sans arguments, cela permet de partager le code entre constructeurs.

On remarque que les champs x et y ne sont pas introduits par le mot-clé static. Chaque objet possède ses propres champs, le programme

  Pair p1 = new Pair (0, 1) ;
  Pair p2 = new Pair (2, 3) ;

  System.out.println(p1.x + ", " + p2.y) ;

affiche « 0, 3 », ce qui est somme toute peu surprenant. De même, la méthode distance est propre à chaque objet, ce qui est logique. En effet, si p est une autre paire, les distances p1.distance(p) et p2.distance(p) n’ont pas de raison particulières d’être égales. Rien n’empêche de mettre des membres static dans une classe à créer des objets. Le concepteur de la classe Pair peut par exemple se dire qu’il est bien dommage de devoir créer deux objets si on veut simplement calculer une distance. Il pourrait donc inclure la méthode statique suivante dans sa classe Pair.

  static double distance(int x1, int y1, int x2, int y2) {
    int dx = x2-x1, dy = y2-y1 ;
    return Math.sqrt(dx*dx+dy*dy) ;
  }

Il serait alors logique d’écrire la méthode distance dynamique plutôt ainsi :

  double distance(Pair p) { return distance(this.x, this.y, p.x, p.y) ; }

Où bien sûr, distance (this.x,… se comprend comme Pair.distance (this.x,

B.2.3  Héritage des objets, sous-typage

L’héritage dans toute sa puissance sera abordé au cours suivant. Mais nous pratiquons déjà l’héritage sans le savoir. En effet, les objets des classes que nous écrivons ne démarrent pas tout nus dans la vie. Toute classe hérite implicitement (pas besoin de extends, voir la section B.1.5) de la classe Object. Les objets de la classe Object (et donc tous les objets) possèdent quelques méthodes, dont la fameuse méthode toString. Par conséquent, le code suivant est accepté par le compilateur, et s’exécute sans erreur. Tout se passe exactement comme si nous avions écrit une méthode toString, alors que cette méthode est en fait héritée.

   Pair p = new Pair (1, 0) ;
   String repr = p.toString() ;
   System.out.print(repr) ;

Ce que renvoie la méthode toString des Object n’est pas bien beau.

Pair@10b62c9

On reconnaît le nom de la classe Pair suivi d’un nombre en hexadécimal qui est plus ou moins l’adresse dans la mémoire de l’objet dont on a appelé la méthode toString.

Mais nous pouvons redéfinir (override) la méthode toString dans la classe Pair.

 // public car il faut respecter la déclaration initiale
  public String toString() {
    return "(" + x + ", " + y + ")" ;
  }

Et l’affichage de p.toString() produit cette fois ci le résultat bien plus satisfaisant (1, 0).

Même si c’est un peu bizarre, il n’est au fond pas très surprenant que lorsque nous appelons la méthode toString de l’intérieur de la classe Pair comme nous le faisons ci-dessus, ce soit la nouvelle méthode toString qui est appelée. Mais écrivons maintenant plus directement :

  System.out.print(p) ;

Et nous obtenons une fois encore l’affichage (1, 0). Or, nous aurons beau parcourir la liste des neuf définitions de méthodes print surchargées de la classe PrintStream, de print(boolean b) à print(Object obj), nous n’avons bien évidemment aucune chance d’y trouver une méthode print(Pair p).

Mais un objet de la classe Pair peut aussi être vu comme un objet de la classe Object. C’est le sous-typage : le type des objets Pair est un sous-type du type des objets Object (penser sous-ensemble : PairObject). En Java, l’héritage entraîne le sous-typage, on dit alors plus synthétiquement que Pair est une sous-classe de Object.

On note que le compilateur procède d’abord à un sous-typage (il se dit qu’un objet Pair est un objet Object), pour pouvoir résoudre la surcharge (il sélectionne la bonne méthode print parmi les neuf possibles). La conversion de type vers un sur-type est assez discrète, mais on peut l’expliciter.

  System.out.print((Object)p) ; // Change le type explicitement

C’est donc finalement la méthode print(Object obj) qui est appelée. Mais à l’intérieur de cette méthode, on ne sait pas que obj est en fait un objet Pair. En simplifiant un peu, le code de cette méthode est équivalent à ceci

public void print (Object obj) {
  if (obj == null) {
    this.print("null") ;
  } else {
    this.print(obj.toString()) ;
  }
}

C’est-à-dire que, au cas de null près, la méthode print(Object obj) appelle print(String s) avec comme argument obj.toString(). Et là, il y a une petite surprise : c’est la méthode toString() de la classe d’origine (la « vraie » classe de obj) qui est appelée, et non pas la méthode toString() des Object. C’est ce que l’on appelle parfois la liaison tardive.


Previous Up Next