next up previous contents index
Next: 8 Les interfaces Up: Java: Le langage Previous: 6 Classes et Objets

Subsections

7 Héritage

 

7.1 Introduction

Un des grands intérêts des langages orienté objet, c'est de pouvoir définir des dépendances entres classes. Cela permet, en particulier, de réaliser des programmes parfaitement modulaires en disposant de modules réutilisatsables.

En reprenant l'exemple de notre classe Date, supposons que l'on veuille définir une classe DateEvénement qui associe à une date donnée, un évènement qui la caractérise. La solution triviale serait de définir la classe DateEvénement en redéfinition entièrement cette classe et ce en prenant exemple sur la classe Date et en rajoutant les nouvelles fonctionnalités que l'on juge utile.

 
classe DateEvenement {
    private int jour, mois, année ;
    private String event = null ;
    public DateEvenement(int m, int m, int j, String e) {
        jour = j ; mois = m ; année = a ; event = e ;
    }
    public affecter(int m, int m, int j, String e) {
        jour = j ; mois = m ; année = a ; event = e ;
    }
    ...
    public void imprimer() {
        System.out.println(jour + "/" + mois + "/" + année + "->" + event) ;
    }
}
Cette approche est à l'opposé de l'esprit de la programmation objet et du génie logiciel. Dans la mesure où l'on dispose déjà de la classe, les langages orientés objets offre un moyen bien plus simple pour définir cette nouvelle classe. Il s'agit de l'héritage .

L'héritage est une caractéristique des langages orientés objet. Une classe obtenue par héritage possèdent la totalité des membres de la classe de base ainsi toutes ses méthodes. Une classe B peut donc se définir par rapport une autre classe A. On dira que la sous classe B hérite des attributs et fonctionnalités de la classe de base A. L'ensemble des classes sont organisés de manière hiérarchique permettant de structurer l'ensemble des informations manipulées par un programme.

Limiter une sous classe aux caractéristiques de la classe de base n'a évidemment aucun intérêt. Une sous classe complètent, en quelque sorte, la classe de base. Elle se doit de définir des attributs et des services supplémentaires. Elle pourra modifier, éventuellement, le comportement d'une des méthodes de la classe de base.

Reprenons notre exemple de la classe DateEvenement : cette classe possède beaucoup de caractéristiques de la classe. En ce qui concerne les champs, seul un champ supplémentaire event est rajoutée à cette classe. Quant aux méthodes, le constructeur et les autres méthodes de cette nouvelle classe doivent être complétés ou adaptées en fonction de l'ajout de ce nouveau champ. Une partie importante du code écrit pour la classe Date reste inchangée. On peut donc définir la classe DateEvenement par dérivation ou extension de la classe Date et ce avec le mot clé extends.

 
class DateEvenement extends Date {
   ...
}
Avec de cette définition, les objets de la classe DateEvenement possèdent les champs et méthodes de la classe Date. Il appartient au concepteur de cette nouvelle de définir les champs et méthodes propre à cette classe. Dans notre exemple, la classe DateEvenement doit définir un champ event et les méthodes d'accès et de modification de ce champ.
 
class DateEvenement extends Date {
    private String event = null ;
    ...
    public String quelEvent() { return event ; }
    ...
}

7.2 Constructeur de la sous classe

7.2.1 Invocation du constructeur de la classe de base

Lorsqu'on définit une classe dérivée , il faut s'assurer que, lors de la création des objets de cette nouvelle classe, les champs propres à cette classe dérivée ainsi que les champs de la classe de base soient initialisés correctement. Les champs de la classe Date sont des champs privés, la sous classe ne peut, en aucun cas, se charger toute seule de l'initialisation de ces membres. Les constructeurs d'une classe dérivée devront forcément utiliser, pour les champs qui ne sont propres à cette classe, les constructeurs de la classe de base. Pour invoquer le constructeur de la classe de base, on fera appel à l'instruction super(...). Un constructeur d'une classe dérivée se compose généralement deux parties : celle concernant les champs de la classe de base et celle concernant les champs propres de la classe dérivée.

L'invocation de super(...) doit être la première instruction du constructeur de la classe dérivée.

 
class DateEvenement extends Date {
    ...
    public DateEvenement(int j, int m, int année, int e) {
        super(j, m, a) ;        // appel au constructeur de la classe de base
        event = e ;             // Initialisation des champs propres de la classe dérivée.
    }
    ...
}
L'ordre dans lequel les différents constructeurs et initialisations sont effectués est le suivant :

7.2.2 Constructeur par défaut

   Si le constructeur de la classe dérivée n'invoque pas le constructeur de la classe de base explicitement avec l'instruction super(...), Java fait quand même appel au constructeur, sans argument, de la classe de base : super(). Un constructeur définit comme suit
 
public DateEvenement(String e) {event = e ; }
est automatiquement transformé en
 
public DateEvenement(String e) { super() ; event = e ; }
Dans le cas où un tel constructeur n'existe pas dans la classe de base, une erreur de compilation est générée. Il existe un cas où l'absence de l'instruction super(...) ne conduit pas cet appel implicite : celui où le corps du constructeur commence par l'instruction this(...).

Si aucun constructeur n'est défini dans la classe dérivée, un constructeur sans argument est quant même invoqué. Tout se passe comme si un constructeur implicite était défini ; constructeur de la forme :

 
public DateEvenement() { super() ; }

7.2.3 L'enchaînement des constructeurs

  Pour tout objet crée, le constructeur de la classe de base est invoqué qui lui a son tour invoque le constructeur de sa classe de base et ainsi de suite. Il existe donc en enchaînement d'invocation de constructeurs. Cette cascade d'appels aux constructeurs s'arrête dès que l'on atteint le constructeur de la classe Object.

La classe Object est la mère de toutes les classes ; toute classe est dérivée directement ou indirectement de la classe Object. Ainsi, lors de la création d'un objet, le premier constructeur invoqué est celui de la classe Object suivi des autres constructeurs dans l'ordre de la hiérarchie de dérivation des classes.

7.2.4 Redéfinition des champs

 

Les champs déclarés dans la classes dérivée sont toujours des champs supplémentaires. Si l'on définit un champ dans la sous classe ayant le même nom qu'un champ de la classe de base, il existera deux champs de même noms. le nom de champ désignera toujours le champ déclaré dans la classe dérivée. Pour avoir accès au champ de la classe de base, il faudra changer le type de la référence pointant sur l'objet ou en utilisant le mot clé super.

 
class A {
    public int i ;
    ...
}
class B extends A {
    public int i ;
    ...
    public void uneMethode() {
        i = 0 ;                    // i est le champ défini dans la classe B
        this.i = 0 ;               // i est le champ défini dans la classe B
        super.i = 1 ;              // i est le champ défini dans la classe A
        ( (A) this ).i  = 1        // i est le champ défini dans la classe A
        ...
        }
}
Cette technique peut s'appliquer en cascade de la manière suivante :
 
class C extends B {
    public int i ;
    ...
    public void uneMethode() {
        i = 0 ;                     // i est le champ défini dans la classe C
        this.i = 0 ;                // i est le champ défini dans la classe C
        super.i = 1 ;               // i est le champ défini dans la classe B
        ( (B) this ).i  = 1         // i est le champ défini dans la classe B
        ( (A) this ).i  = 1         // i est le champ défini dans la classe A
        ...
    }
}
Par contre, l'instruction suivante est incorrecte :
 
super.super.i = 1 ;                  //  Incorrect syntaxiquement !
Tout comme l'utilisation du mot clé this, le mot clé super ne peut être utilisé dans les méthodes static.

7.3 Redéfinition des méthodes

 

7.3.1 Méthodes d'instances

On n'est, évidemment pas, tenu de déclarer des nouveaux champs dans une classe dérivée : il est tout possible que l'on dérive une classe pour uniquement modifier les méthodes de la classe de base. Par exemple, si l'on voulait une classe nouvelle classe DateAnglais qui ne diffère de la classe Date que par le format d'impression de la date (impression du mois avant celui du jour), il suffirait de définir une classe dérivée de la classe Date et de redéfinir la méthode imprimer pour cette nouvelle classe.

La redéfinition d'une méthode consiste à fournir une implantation différente de la méthode de même signature fournie par la classe de base. Dans cet exemple, la méthode imprimer des classes Date et DateAnglais ont la même signature ; celle de la classe DateAnglais redéfinit celle de la classe Date.

 
class DateAnglais extends Date {
    public void imprimer() {
        System.out.println(quelMois() + "/" + quelJour() + "/" + quelAnnée() + "/" + event) ;
        }
}
La rédéfinition d'une méthode ne concerne que méthodes ayant la même signature dans la classe de base et la sous classe.

La redéfinition des méthodes est un mécanisme puissant : le parallèle avec la redéfinition des champs peut être trompeur. En effet, lorsqu'une méthode est redéfinie, on ne peut invoquer la méthode définie dans la classe de base par un simple changement de type de la référence.

 
class Fruit {
    public void quiEsTu() { System.out.println("Je suis un fruit") ;
}
class Pomme extends Fruit {
    public void quiEsTu() { System.out.println("Je suis une pomme") ;
}
class poire extends fruit {
    public void quiEsTu() { System.out.println("Je suis une poire") ;
}
class test {
    public void main(String args[]) {
        Pomme pm = new Pomme() ;
        Poire pr = new Poire() ;
        Fruit f ;
        f.quiEsTu() ;                   // Je suis un fruit
        f = (Fruit)pm ;
        f.quiEsTu() ;                   // Je suis une pomme
        f = (Fruit)pr ;
        f.quiEsTu() ;                   // Je suis une poire
    }
}
Dans cet exemple, même en changement de le type de la référence de l'objet Pomme et Poire en une référence vers un Fruit, la méthode quiEsTu invoqué est toujours celle de l'objet référencé. Ce comportement peut paraître surprenant de prime abord ; mais on se rend vite compte ce comportement est tout à fait souhaitable. Par exemple, si l'on dispose d'un tableau de fruits (des pommes et des poires), les éléments de ce tableau sont des pommes et des poires mais sont définis dans la définition du tableau, comme des fruits. Pourtant, sans ce comportement, il nous serait impossible de connaître la nature exacte des fruits rangés dans le tableau.

7.3.2 Méthodes de la classe de base

Pour avoir accès à une méthode redéfinie de la classe de base, à l'intérieur d'une méthode de la classe dérivée, il faudra utiliser le mot clé super. Comme pour les champs redéfinis, il suffit de préfixer le nom de méthode par le mot clé super pour invoquer la méthode de la classe de base.

 
class DateEvenement extends Date {
    private String event = null ;
    public DateEvenement(int j, int m, int a, int e) {super(j, m, a) ; event = e ;   }
    public void imprimer() { super.imprimer();  System.out.println(e); }
    ...
}

7.3.3 Méthodes static

  Une méthode static peut également être redéfinie par une autre méthode static. Par contre, une static ne peut être redéfinie en une méthode non static.

7.4 Destructeurs

  Contrairement aux constructeurs, les destructeurs ne sont invoqués en chaîne. Il appartient au programmeur, s'il le juge utile, de réaliser cette chaîne de destructeur lui-même et ce à l'aide du mot clé super.
 
class B extends A {
    public finalize() {
        super.finalize() ;
        // Code de finalize pour la classe dérivée....
        ...
    }
}
Rappelons que dans la majorité des acs, on ne se souciera pas des destructeurs puisque Java dispose d'un système de récupération de mémoire automatique.

7.5 Méthodes et classes finales

   

Une méthode est final si elle ne peut être redéfinie dans aucune des sous classes. Ainsi, le concepteur de la classe de base exprime son souhait de figer définitivement l'implantation de cette méthode.

 
class Date {
    private int jour, mois, année ;
    ...
    public final quelJour() { return jour ; }
    ...
}
On peut également décider figer la définition d'une classe entière en la déclarant final. Cela implique qu'il ne sera plus possible de dériver une nouvelle classe à partir de cette dernière. Et comme on peut dériver cette classe, il est évidemment plus possible de rédéfinir les méthodes de cette classe dans une sous classe.
 
final class DateEvenement {  ... }
L'intérêt de déclarer final les classes et les méthodes est fournir un module sûr. On est assuré que le comportement ne peut être modifié. Une classe finale est une classe en qui on peut avoir confiance. Par contre, celui réduit la souplesse et l'extensibilité des modules proposés. Une meilleure approche est parfois de définir toutes les méthodes finales sans déclarer final la classe elle-même. Ainsi, on pourra dériver de nouvelles classes mais changer avoir la possibilités de changer le comportement des méthodes de la classe de base.

Un second intérêt des méthodes et classes finales est le souci d'efficacité. Les méthodes et classes finales permettent au compilateur de produire du code optimisé. En particulier, la détermination statique des méthodes peut être effectuée dans ce cas et parfois même des évaluations partielles de votre programme.

7.7 Conversion entre classes et sous classes

 

Une opération de cast permet de modifier le type d'une référence. Les changement de nature ne permise que dans des cas bien précis.

Un opération de cast a le droit d'affiner le type d'une référence. Par exemple, une référence vers un objet de type Date peut être changé en une référence vers un objet de type DateEvénement. Ce dont il est question ici, ce n'est que la perception que la machine Java a de la référence. L'objet référencé est toujours le même ; on essaye tout juste de faire croire à la machine Java que l'objet référencé est d'une autre nature. En aucun cas, l'objet lui ne subit de modification par cette opération de cast.

On ne peut changer le type d'une référence en une référence vers un objet d'une classe dérivée que si l'on est sûr que l'objet référencé est bien du type prétendu. Une référence vers un objet de type DateEvénement peut être changé en une référence vers un objet de type Date que si l'on est assuré que l'objet référencé est effectivement un objet de la classe DateEvénement. Sinon, l'erreur ClassCastException est générée.  

 
Date d;
DateEvenement de;
...
de = d ;                         // Erreur de comilation !
d = de ;                         // O.K.
de = (DateEvenement)d ;          // O.K. d contient une référence vers de
                                 // un objet de type DateEvenement
On trouvera de amples détails, dans le chapitre 12, sur l'ensemble des conversions permises par le langage Java .

7.8 Classes et méthodes abstraites

           

On peut parfois utiliser les classes pour définir, non pas un type d'objet bien précis, mais un concept. On pourrait, par exemple, définira une classe Forme avec des méthodes abstraites comme superficie, dessiner, tourner, etc. On ne sait pas implanter la méthode superficie dans le cas d'une forme générale. Par contre, une fois connue une forme précise (un carré, un cercle, etc.), on sait implanter cette méthode pour cette forme. Autrement dit, un objet de type forme n'a aucun intérêt en soi. Les objets utiles sont les carrés, les cercles, etc. Par contre, il existe souvent des caractéristiques et comportements communs à toutes les formes.

Le concepteur de la classe Forme définira donc cette classe en la qualifiant d'abstraite (abstract). Il regroupera dans cette classe tous les champs communs à toutes les formes ainsi que les méthodes pour lesquelles une implantation est possible. Quant aux méthodes qui ne peuvent être implantées et qui doivent absolument l'être pour des formes bien précises, il les qualifiera d'abstraites.

Une méthode abstraite est une méthode dont la définition est supposée être donnée par redéfinition dans les classes dérivées. On considère dans ce cas là que la classe de base ne peut fournir une méthode par défaut.

Il ne sera jamais possible créer un objet de type Forme. Les objets qui sont susceptible d'exister sont des formes bien précises : des carrés, des cercles, des lignes, etc. Ces formes effectives, seront des objets des classes obtenus en dérivant la classe Forme. Les classe Carrée, Cercle, Ligne etc. seront des classes dérivées de la classe Forme.

Une classe abstraite est une classe partiellement implantée i.e. que certaines des méthodes sont abstraites. Le langage Java impose de qualifier la classe d'abstraite lorsqu'une de ses méthode est abstraite.

Tout ceci permet de regrouper des données et méthodes communes dans une classe et de spécifier les méthodes qu'une classe dérivée de celle-ci doit absolument implanter. Si l'on définit une sous classes sans implanter toutes les méthodes abstraites de la classe de base, une erreur de compilation est générée.

 
abstract class Forme {
    ...
    public abstract void superficie() ;
    ...
    }
class Carrée extends Forme {
    ...
    public void superficie () { ... }
    ...
}
La redondance de ce qualifier abstract, à la fois pour la méthode et pour la classe, est là pour permettre au programmeur de déterminer au premier coup d'oeil, en voyant la classe de savoir s'il existe une méthode abstraite dans cette classe. En effet, il ne sera pas nécessaire de parcourir l'ensemble des méthodes d'une classe pour savoir s'il s'agit d'une classe abstraite. Le mot clé abstract devant obligatoirement figurer dans l'entête de la classe, il sera donc facile de repérer les classes abstraites. Il n'est pas permis de créer des objets d'une classe abstraite.

7.9 La classe Object

La classe Object est la classe de base de toutes les autres classes. Autrement dit, toutes les classes que l'on définit sont implicitement des sous classes de la classe Object. Les méthodes définies dans cette classe Object peuvent donc être utilisées ou redéfinies. On peut classer les méthodes publiques de la classe Object en deux catégories :

 public String toString ()

La méthode toString est utilisée pour donner une représentation textuelle d'un objet. Pour reprendre notre exemple de la classe Date, au lieu d'avoir une méthode spécifique pour afficher une date avec un format bien particulier avec la méthode afficher, on aurait plutôt intérêt à redéfinir la méthode toString pour la classe Date. Ainsi, on pourra afficher un objet de type Date comme tout autre objet avec la méthode System.out.print.
 
public String toString() { return jour  + " /" + mois + " /" +année; }

 public boolean equals ()

Comme la méthode toString, la méthode equals rend la valeur true si deux l'objet sur lequel la méthode equals est invoquée est égale à l'objet passé en paramètre. Qu'est ce que l'égalité ? La notion de l'égalité dépend des objets concernés. Par défaut, la sémantique de la méthode equals fournie dans la classe Object est l'égalité entre les valeurs des références : les deux références désignent le même objet. Cette méthode equals est redondante par rapport au simple test d'égalité que l'on effectue avec l'opérateur ==. Par exemple, on pourrait vouloir définir une méthode equals pour la classe Date dont la sémantique porte, non pas sur les valeurs des références mais, sur les valeurs des champs des objets. Il suffit alors de redéfinir la méthode equals dans la classe Date comme suit :
 
public boolean equals(Object  d) {
    if  ( d != null && d instanceof Date) {
        return this.jour == d.jour && this.mois == d.mois && this.année == d.année;
    }
    else return false;
}
Avec cette redéfinition de la méthode equals appliquée à deux objets de type Date rend la valeur true lorsque les champs jour, mois et année ont respectivement les mêmes valeurs.

 public final native Class getClass ()

La méthode getClass est une méthode final qui ne peut donc pas être redéfinie. Elle retourne la classe à laquelle appartient l'objet sur lequel elle s'applique. La valeur retournée est un objet de la classe java.lang.Class (voir [*]) qui dispose d'un certain nombre de méthode pour examiner et extraire des informations sur les classes.
 
class test {
  public static void main(String args[]) {
    Date d = new DateEvenement(25,12,1997,"Noel 97");
    System.out.println(d.getClass());
  }
}
produit l'affichage suivant~:
class DateEvenement
On remarquera qu'il affiche bien DateEvenement et non pas Date.

 

newInstance

 public native int hashCode ()

La méthode hashCode rend un entier représentant le code de hachage de l'objet sur lequel s'applique la méthode. Cet entier est généralement unique pour chaque objet. A priori, il n'est pas nécessaire de redéfinir cette méthode pour les classes que l'on définit. Il existe certaines applications où l'on aimerait faire correspondre un même entier à un ensemble d'objets d'une classe : il s'agit une relation d'équivalence sur les objets. Nous verrons plus loin les détails d'implantation des hashcodes avec la classe HashTableclasse.HashTable.
 protected native Object clone () throws CloneNotSupportedException
La méthode clone retourne une copie de l'objet sur lequel la méthode est invoquée. L'implantation de la méthode clone pour les objets d'une classe que l'on définit. L'attitude d'une classe face au clonange des objets peuvent être variée :

 

A TERMINER

 protected void finalize () throws Throwable

 

A faire


next up previous contents index
Next: 8 Les interfaces Up: Java: Le langage Previous: 6 Classes et Objets
Touraivane
6/12/1998